@terraware/web-components 4.2.21 → 4.2.22

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 (93) hide show
  1. package/components/VirtualWalkthrough/Annotation.d.ts.map +1 -1
  2. package/components/VirtualWalkthrough/Annotation.js +10 -0
  3. package/components/VirtualWalkthrough/BoundaryWall.d.ts +11 -0
  4. package/components/VirtualWalkthrough/BoundaryWall.d.ts.map +1 -0
  5. package/components/VirtualWalkthrough/BoundaryWall.js +35 -0
  6. package/components/VirtualWalkthrough/TfXrNavigation.d.ts +42 -6
  7. package/components/VirtualWalkthrough/TfXrNavigation.d.ts.map +1 -1
  8. package/components/VirtualWalkthrough/TfXrNavigation.js +109 -9
  9. package/components/VirtualWalkthrough/TfXrNavigation.test.js +110 -0
  10. package/components/VirtualWalkthrough/VirtualWalkthroughViewer.d.ts.map +1 -1
  11. package/components/VirtualWalkthrough/VirtualWalkthroughViewer.js +50 -3
  12. package/components/VirtualWalkthrough/VrAnnotationPanel.d.ts +9 -0
  13. package/components/VirtualWalkthrough/VrAnnotationPanel.d.ts.map +1 -0
  14. package/components/VirtualWalkthrough/VrAnnotationPanel.js +22 -0
  15. package/components/VirtualWalkthrough/XrAnnotationInteraction.d.ts +6 -0
  16. package/components/VirtualWalkthrough/XrAnnotationInteraction.d.ts.map +1 -0
  17. package/components/VirtualWalkthrough/XrAnnotationInteraction.js +15 -0
  18. package/components/VirtualWalkthrough/XrGazeDwell.d.ts +6 -0
  19. package/components/VirtualWalkthrough/XrGazeDwell.d.ts.map +1 -0
  20. package/components/VirtualWalkthrough/XrGazeDwell.js +15 -0
  21. package/components/VirtualWalkthrough/XrPointerRay.d.ts +7 -0
  22. package/components/VirtualWalkthrough/XrPointerRay.d.ts.map +1 -0
  23. package/components/VirtualWalkthrough/XrPointerRay.js +15 -0
  24. package/components/VirtualWalkthrough/boundary-wall.d.ts +86 -0
  25. package/components/VirtualWalkthrough/boundary-wall.d.ts.map +1 -0
  26. package/components/VirtualWalkthrough/boundary-wall.js +331 -0
  27. package/components/VirtualWalkthrough/boundary-wall.test.js +203 -0
  28. package/components/VirtualWalkthrough/vr-annotation-panel-layout.d.ts +6 -0
  29. package/components/VirtualWalkthrough/vr-annotation-panel-layout.d.ts.map +1 -0
  30. package/components/VirtualWalkthrough/vr-annotation-panel-layout.js +22 -0
  31. package/components/VirtualWalkthrough/vr-annotation-panel-layout.test.js +26 -0
  32. package/components/VirtualWalkthrough/vr-annotation-panel.d.ts +40 -0
  33. package/components/VirtualWalkthrough/vr-annotation-panel.d.ts.map +1 -0
  34. package/components/VirtualWalkthrough/vr-annotation-panel.js +312 -0
  35. package/components/VirtualWalkthrough/xr-annotation-candidates.d.ts +18 -0
  36. package/components/VirtualWalkthrough/xr-annotation-candidates.d.ts.map +1 -0
  37. package/components/VirtualWalkthrough/xr-annotation-candidates.js +38 -0
  38. package/components/VirtualWalkthrough/xr-annotation-interaction.d.ts +13 -0
  39. package/components/VirtualWalkthrough/xr-annotation-interaction.d.ts.map +1 -0
  40. package/components/VirtualWalkthrough/xr-annotation-interaction.js +59 -0
  41. package/components/VirtualWalkthrough/xr-annotation-targeting.d.ts +10 -0
  42. package/components/VirtualWalkthrough/xr-annotation-targeting.d.ts.map +1 -0
  43. package/components/VirtualWalkthrough/xr-annotation-targeting.js +36 -0
  44. package/components/VirtualWalkthrough/xr-annotation-targeting.test.js +66 -0
  45. package/components/VirtualWalkthrough/xr-button-arming.d.ts +14 -0
  46. package/components/VirtualWalkthrough/xr-button-arming.d.ts.map +1 -0
  47. package/components/VirtualWalkthrough/xr-button-arming.js +19 -0
  48. package/components/VirtualWalkthrough/xr-button-arming.test.js +35 -0
  49. package/components/VirtualWalkthrough/xr-button-hold.d.ts +20 -0
  50. package/components/VirtualWalkthrough/xr-button-hold.d.ts.map +1 -0
  51. package/components/VirtualWalkthrough/xr-button-hold.js +43 -0
  52. package/components/VirtualWalkthrough/xr-button-hold.test.js +57 -0
  53. package/components/VirtualWalkthrough/xr-exit-button.d.ts +21 -4
  54. package/components/VirtualWalkthrough/xr-exit-button.d.ts.map +1 -1
  55. package/components/VirtualWalkthrough/xr-exit-button.js +98 -37
  56. package/components/VirtualWalkthrough/xr-face-buttons.d.ts +15 -0
  57. package/components/VirtualWalkthrough/xr-face-buttons.d.ts.map +1 -0
  58. package/components/VirtualWalkthrough/xr-face-buttons.js +31 -0
  59. package/components/VirtualWalkthrough/xr-face-buttons.test.js +82 -0
  60. package/components/VirtualWalkthrough/xr-gaze-dwell-state.d.ts +17 -0
  61. package/components/VirtualWalkthrough/xr-gaze-dwell-state.d.ts.map +1 -0
  62. package/components/VirtualWalkthrough/xr-gaze-dwell-state.js +39 -0
  63. package/components/VirtualWalkthrough/xr-gaze-dwell-state.test.js +61 -0
  64. package/components/VirtualWalkthrough/xr-gaze-dwell.d.ts +30 -0
  65. package/components/VirtualWalkthrough/xr-gaze-dwell.d.ts.map +1 -0
  66. package/components/VirtualWalkthrough/xr-gaze-dwell.js +173 -0
  67. package/components/VirtualWalkthrough/xr-interactive-ui.d.ts +13 -0
  68. package/components/VirtualWalkthrough/xr-interactive-ui.d.ts.map +1 -0
  69. package/components/VirtualWalkthrough/xr-interactive-ui.js +25 -0
  70. package/components/VirtualWalkthrough/xr-interactive-ui.test.js +165 -0
  71. package/components/VirtualWalkthrough/xr-pointer-ray.d.ts +10 -0
  72. package/components/VirtualWalkthrough/xr-pointer-ray.d.ts.map +1 -0
  73. package/components/VirtualWalkthrough/xr-pointer-ray.js +24 -0
  74. package/components/VirtualWalkthrough/xr-progress-pie.d.ts +10 -0
  75. package/components/VirtualWalkthrough/xr-progress-pie.d.ts.map +1 -0
  76. package/components/VirtualWalkthrough/xr-progress-pie.js +30 -0
  77. package/components/VirtualWalkthrough/xr-progress-pie.test.js +79 -0
  78. package/components/VirtualWalkthrough/xr-ray-quad.d.ts +13 -0
  79. package/components/VirtualWalkthrough/xr-ray-quad.d.ts.map +1 -0
  80. package/components/VirtualWalkthrough/xr-ray-quad.js +29 -0
  81. package/components/VirtualWalkthrough/xr-ray-quad.test.js +37 -0
  82. package/components/VirtualWalkthrough/xr-scene-bounds.d.ts +17 -0
  83. package/components/VirtualWalkthrough/xr-scene-bounds.d.ts.map +1 -0
  84. package/components/VirtualWalkthrough/xr-scene-bounds.js +41 -0
  85. package/components/VirtualWalkthrough/xr-scene-bounds.test.js +70 -0
  86. package/components/VirtualWalkthrough/xr-teleport-gesture.d.ts +30 -0
  87. package/components/VirtualWalkthrough/xr-teleport-gesture.d.ts.map +1 -0
  88. package/components/VirtualWalkthrough/xr-teleport-gesture.js +45 -0
  89. package/components/VirtualWalkthrough/xr-teleport-gesture.test.js +90 -0
  90. package/hooks/useXr.d.ts +2 -0
  91. package/hooks/useXr.d.ts.map +1 -1
  92. package/hooks/useXr.js +10 -6
  93. package/package.json +1 -1
@@ -0,0 +1,61 @@
1
+ import { INITIAL_DWELL_STATE, advanceDwell } from './xr-gaze-dwell-state';
2
+ const THRESHOLD = 1.5;
3
+ describe('advanceDwell', () => {
4
+ it('stays reset when there is no gaze target', () => {
5
+ const r = advanceDwell({
6
+ targetIndex: 2,
7
+ elapsed: 1.0
8
+ }, null, 0.1, THRESHOLD);
9
+ expect(r.state).toEqual(INITIAL_DWELL_STATE);
10
+ expect(r.progress).toBe(0);
11
+ expect(r.justOpened).toBe(false);
12
+ });
13
+ it('resets to zero when the target changes', () => {
14
+ const r = advanceDwell({
15
+ targetIndex: 0,
16
+ elapsed: 1.0
17
+ }, 1, 0.1, THRESHOLD);
18
+ expect(r.state).toEqual({
19
+ targetIndex: 1,
20
+ elapsed: 0
21
+ });
22
+ expect(r.progress).toBe(0);
23
+ expect(r.justOpened).toBe(false);
24
+ });
25
+ it('accumulates dwell time while the same target is held', () => {
26
+ const r = advanceDwell({
27
+ targetIndex: 3,
28
+ elapsed: 0.5
29
+ }, 3, 0.25, THRESHOLD);
30
+ expect(r.state).toEqual({
31
+ targetIndex: 3,
32
+ elapsed: 0.75
33
+ });
34
+ expect(r.progress).toBeCloseTo(0.5, 5);
35
+ expect(r.justOpened).toBe(false);
36
+ });
37
+ it('fires justOpened once when crossing the threshold and clamps elapsed', () => {
38
+ const r = advanceDwell({
39
+ targetIndex: 3,
40
+ elapsed: 1.4
41
+ }, 3, 0.5, THRESHOLD);
42
+ expect(r.justOpened).toBe(true);
43
+ expect(r.progress).toBe(1);
44
+ expect(r.state).toEqual({
45
+ targetIndex: 3,
46
+ elapsed: THRESHOLD
47
+ });
48
+ });
49
+ it('does not re-fire while the target stays held past the threshold', () => {
50
+ const r = advanceDwell({
51
+ targetIndex: 3,
52
+ elapsed: THRESHOLD
53
+ }, 3, 0.5, THRESHOLD);
54
+ expect(r.justOpened).toBe(false);
55
+ expect(r.progress).toBe(1);
56
+ expect(r.state).toEqual({
57
+ targetIndex: 3,
58
+ elapsed: THRESHOLD
59
+ });
60
+ });
61
+ });
@@ -0,0 +1,30 @@
1
+ import { Script } from 'playcanvas';
2
+ export declare class XrGazeDwell extends Script {
3
+ static scriptName: string;
4
+ /** Index of the annotation whose panel is currently open, or -1. Excluded from gaze targets so
5
+ * dwell opens OTHER annotations while one is open (and never re-dwells the open one). */
6
+ activeIndex: number;
7
+ private _dwell;
8
+ private _pieEntity?;
9
+ private _pieCanvas?;
10
+ private _pieTexture?;
11
+ private _pieMaterial?;
12
+ private _pieMesh?;
13
+ private _drawnProgress;
14
+ private _headPos;
15
+ private _headRot;
16
+ private _viewOffset;
17
+ private _forward;
18
+ private _scratchScale;
19
+ private _isVrActive;
20
+ initialize(): void;
21
+ private _createQuad;
22
+ /** Redraws the pie fill for the given dwell progress (0..1) onto the texture canvas. */
23
+ private _drawPie;
24
+ private _trackHead;
25
+ update(dt: number): void;
26
+ private _showPie;
27
+ private _hidePie;
28
+ destroy(): void;
29
+ }
30
+ //# sourceMappingURL=xr-gaze-dwell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xr-gaze-dwell.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-gaze-dwell.ts"],"names":[],"mappings":"AAAA,OAAO,EAWL,MAAM,EAKP,MAAM,YAAY,CAAC;AAqBpB,qBAAa,WAAY,SAAQ,MAAM;IACrC,MAAM,CAAC,UAAU,SAAiB;IAElC;6FACyF;IACzF,WAAW,SAAM;IAEjB,OAAO,CAAC,MAAM,CAAmC;IAEjD,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,UAAU,CAAC,CAAoB;IACvC,OAAO,CAAC,WAAW,CAAC,CAAU;IAC9B,OAAO,CAAC,YAAY,CAAC,CAAmB;IACxC,OAAO,CAAC,QAAQ,CAAC,CAAO;IACxB,OAAO,CAAC,cAAc,CAAM;IAE5B,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,UAAU;IAuCV,OAAO,CAAC,WAAW;IAYnB,wFAAwF;IACxF,OAAO,CAAC,QAAQ;IAWhB,OAAO,CAAC,UAAU;IAgBlB,MAAM,CAAC,EAAE,EAAE,MAAM;IAmCjB,OAAO,CAAC,QAAQ;IAmBhB,OAAO,CAAC,QAAQ;IAOhB,OAAO;CAeR"}
@@ -0,0 +1,173 @@
1
+ import { ADDRESS_CLAMP_TO_EDGE, BLEND_NORMAL, CULLFACE_NONE, Color, Entity, FILTER_LINEAR, LAYERID_IMMEDIATE, Mesh, MeshInstance, Quat, Script, StandardMaterial, Texture, Vec3, XRTYPE_VR } from 'playcanvas';
2
+ import { HOTSPOT_HALF_EXTENT, collectAnnotationHitCandidates } from './xr-annotation-candidates';
3
+ import { nearestAnnotationHit } from './xr-annotation-targeting';
4
+ import { INITIAL_DWELL_STATE, advanceDwell } from './xr-gaze-dwell-state';
5
+ import { drawProgressPie } from './xr-progress-pie';
6
+
7
+ /** Multiplier on the hotspot's world radius for gaze targeting. */
8
+ const GAZE_HIT_RADIUS_PAD = 5;
9
+
10
+ /** Seconds of continuous gaze required to open an annotation. */
11
+ const DWELL_SECONDS = 1.25;
12
+
13
+ /** Progress-pie quad radius as a multiple of the hotspot's rendered half-extent (covers the hotspot). */
14
+ const PIE_RADIUS_SCALE = 1.25;
15
+
16
+ /** Progress-pie texture resolution. */
17
+ const PIE_TEXTURE_SIZE = 128;
18
+ const LOCAL_FORWARD = new Vec3(0, 0, -1);
19
+ export class XrGazeDwell extends Script {
20
+ static scriptName = 'xrGazeDwell';
21
+
22
+ /** Index of the annotation whose panel is currently open, or -1. Excluded from gaze targets so
23
+ * dwell opens OTHER annotations while one is open (and never re-dwells the open one). */
24
+ activeIndex = -1;
25
+ _dwell = INITIAL_DWELL_STATE;
26
+ _drawnProgress = -1;
27
+ _headPos = new Vec3();
28
+ _headRot = new Quat();
29
+ _viewOffset = new Vec3();
30
+ _forward = new Vec3();
31
+ _scratchScale = new Vec3();
32
+ _isVrActive = () => this.app.xr?.active === true && this.app.xr?.type === XRTYPE_VR;
33
+ initialize() {
34
+ this._pieCanvas = document.createElement('canvas');
35
+ this._pieCanvas.width = PIE_TEXTURE_SIZE;
36
+ this._pieCanvas.height = PIE_TEXTURE_SIZE;
37
+ this._pieTexture = new Texture(this.app.graphicsDevice, {
38
+ name: 'xr-gaze-dwell-pie',
39
+ width: PIE_TEXTURE_SIZE,
40
+ height: PIE_TEXTURE_SIZE,
41
+ addressU: ADDRESS_CLAMP_TO_EDGE,
42
+ addressV: ADDRESS_CLAMP_TO_EDGE,
43
+ minFilter: FILTER_LINEAR,
44
+ magFilter: FILTER_LINEAR,
45
+ mipmaps: true
46
+ });
47
+ this._pieTexture.setSource(this._pieCanvas);
48
+ const material = new StandardMaterial();
49
+ material.useLighting = false;
50
+ material.emissive = new Color(1, 1, 1);
51
+ material.emissiveMap = this._pieTexture;
52
+ material.opacityMap = this._pieTexture;
53
+ material.opacityMapChannel = 'a';
54
+ material.blendType = BLEND_NORMAL;
55
+ material.depthTest = false;
56
+ material.depthWrite = false;
57
+ material.cull = CULLFACE_NONE;
58
+ material.update();
59
+ this._pieMaterial = material;
60
+ this._pieMesh = this._createQuad();
61
+ const meshInstance = new MeshInstance(this._pieMesh, material);
62
+ this._pieEntity = new Entity('xr-gaze-dwell-pie');
63
+ this._pieEntity.addComponent('render', {
64
+ meshInstances: [meshInstance],
65
+ layers: [LAYERID_IMMEDIATE]
66
+ });
67
+ this._pieEntity.enabled = false;
68
+ this.entity.addChild(this._pieEntity);
69
+ }
70
+ _createQuad() {
71
+ const h = HOTSPOT_HALF_EXTENT;
72
+ const mesh = new Mesh(this.app.graphicsDevice);
73
+ mesh.setPositions([-h, -h, 0, h, -h, 0, h, h, 0, -h, h, 0]);
74
+ mesh.setNormals([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]);
75
+ mesh.setUvs(0, [0, 1, 1, 1, 1, 0, 0, 0]);
76
+ mesh.setIndices([0, 1, 2, 0, 2, 3]);
77
+ mesh.update();
78
+ return mesh;
79
+ }
80
+
81
+ /** Redraws the pie fill for the given dwell progress (0..1) onto the texture canvas. */
82
+ _drawPie(progress) {
83
+ const ctx = this._pieCanvas?.getContext('2d');
84
+ if (!ctx) {
85
+ return;
86
+ }
87
+ ctx.clearRect(0, 0, PIE_TEXTURE_SIZE, PIE_TEXTURE_SIZE);
88
+ drawProgressPie(ctx, PIE_TEXTURE_SIZE, progress);
89
+ this._pieTexture?.upload();
90
+ }
91
+ _trackHead() {
92
+ const views = this.app.xr?.views?.list;
93
+ if (!views || views.length === 0) {
94
+ return false;
95
+ }
96
+ this._headPos.set(0, 0, 0);
97
+ for (const view of views) {
98
+ view.viewInvOffMat.getTranslation(this._viewOffset);
99
+ this._headPos.add(this._viewOffset);
100
+ }
101
+ this._headPos.mulScalar(1 / views.length);
102
+ this._headRot.setFromMat4(views[0].viewInvOffMat);
103
+ return true;
104
+ }
105
+ update(dt) {
106
+ if (!this._pieEntity) {
107
+ return;
108
+ }
109
+ if (!this._isVrActive() || !this._trackHead()) {
110
+ this._dwell = INITIAL_DWELL_STATE;
111
+ this._hidePie();
112
+ return;
113
+ }
114
+ const activeName = `annotation-${this.activeIndex}`;
115
+ const candidates = collectAnnotationHitCandidates(this.app, GAZE_HIT_RADIUS_PAD).filter(candidate => candidate.entity.name !== activeName);
116
+ this._headRot.transformVector(LOCAL_FORWARD, this._forward);
117
+ const target = nearestAnnotationHit(this._headPos, this._forward, candidates);
118
+ const result = advanceDwell(this._dwell, target, dt, DWELL_SECONDS);
119
+ this._dwell = result.state;
120
+ if (target !== null && result.progress > 0 && result.progress < 1) {
121
+ this._showPie(candidates[target].entity, result.progress);
122
+ } else {
123
+ this._hidePie();
124
+ }
125
+ if (result.justOpened && target !== null) {
126
+ const {
127
+ entity,
128
+ script
129
+ } = candidates[target];
130
+ const camera = this.app.root.findByName('camera');
131
+ const screen = camera?.camera?.worldToScreen(entity.getPosition());
132
+ script.onVrOpenCallback(screen?.x ?? 0, screen?.y ?? 0);
133
+ }
134
+ }
135
+ _showPie(entity, progress) {
136
+ if (!this._pieEntity) {
137
+ return;
138
+ }
139
+ if (Math.abs(progress - this._drawnProgress) > 0.001) {
140
+ this._drawnProgress = progress;
141
+ this._drawPie(progress);
142
+ }
143
+
144
+ // The unit quad has half-extent HOTSPOT_HALF_EXTENT, so scale it to reach the desired world size.
145
+ entity.getWorldTransform().getScale(this._scratchScale);
146
+ const scale = this._scratchScale.x * PIE_RADIUS_SCALE;
147
+ this._pieEntity.enabled = true;
148
+ this._pieEntity.setLocalScale(scale, scale, scale);
149
+ this._pieEntity.setPosition(entity.getPosition());
150
+ this._pieEntity.setRotation(this._headRot);
151
+ }
152
+ _hidePie() {
153
+ if (this._pieEntity && this._pieEntity.enabled) {
154
+ this._pieEntity.enabled = false;
155
+ this._drawnProgress = -1;
156
+ }
157
+ }
158
+ destroy() {
159
+ if (this._pieEntity) {
160
+ if (this._pieEntity.render) {
161
+ this._pieEntity.render.meshInstances = [];
162
+ }
163
+ this._pieEntity.destroy();
164
+ this._pieEntity = undefined;
165
+ }
166
+ this._pieMesh?.destroy();
167
+ this._pieMaterial?.destroy();
168
+ this._pieTexture?.destroy();
169
+ this._pieMesh = undefined;
170
+ this._pieMaterial = undefined;
171
+ this._pieTexture = undefined;
172
+ }
173
+ }
@@ -0,0 +1,13 @@
1
+ import { Vec3 } from 'playcanvas';
2
+ /** True when the ray points at an openable annotation hotspot. */
3
+ export declare const rayHitsAnnotationHotspot: (app: any, origin: Vec3, direction: Vec3) => boolean;
4
+ /** True when the ray points at the open VR annotation panel. False when no panel is mounted. */
5
+ export declare const rayHitsAnnotationPanel: (app: any, origin: Vec3, direction: Vec3) => boolean;
6
+ /** True when the ray points at the VR exit button. */
7
+ export declare const rayHitsExitButton: (app: any, origin: Vec3, direction: Vec3) => boolean;
8
+ /**
9
+ * True when the ray points at anything the trigger is meant to operate. Teleport consults this so a
10
+ * select that lands on UI doesn't also move the rig.
11
+ */
12
+ export declare const rayHitsInteractiveUi: (app: any, origin: Vec3, direction: Vec3) => boolean;
13
+ //# sourceMappingURL=xr-interactive-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xr-interactive-ui.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-interactive-ui.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAOlC,kEAAkE;AAClE,eAAO,MAAM,wBAAwB,GAAI,KAAK,GAAG,EAAE,QAAQ,IAAI,EAAE,WAAW,IAAI,KAAG,OACoB,CAAC;AAExG,gGAAgG;AAChG,eAAO,MAAM,sBAAsB,GAAI,KAAK,GAAG,EAAE,QAAQ,IAAI,EAAE,WAAW,IAAI,KAAG,OAIhF,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,iBAAiB,GAAI,KAAK,GAAG,EAAE,QAAQ,IAAI,EAAE,WAAW,IAAI,KAAG,OAI3E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,GAAG,EAAE,QAAQ,IAAI,EAAE,WAAW,IAAI,KAAG,OAGpC,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { VrAnnotationPanel } from './vr-annotation-panel';
2
+ import { HIT_RADIUS_PAD, collectAnnotationHitCandidates } from './xr-annotation-candidates';
3
+ import { nearestAnnotationHit } from './xr-annotation-targeting';
4
+ import { XrExitButton } from './xr-exit-button';
5
+
6
+ /** True when the ray points at an openable annotation hotspot. */
7
+ export const rayHitsAnnotationHotspot = (app, origin, direction) => nearestAnnotationHit(origin, direction, collectAnnotationHitCandidates(app, HIT_RADIUS_PAD)) !== null;
8
+
9
+ /** True when the ray points at the open VR annotation panel. False when no panel is mounted. */
10
+ export const rayHitsAnnotationPanel = (app, origin, direction) => {
11
+ const script = app.root.findByName('vr-annotation-panel')?.script?.get(VrAnnotationPanel.scriptName);
12
+ return typeof script?.rayHitsPanel === 'function' && script.rayHitsPanel(origin, direction) === true;
13
+ };
14
+
15
+ /** True when the ray points at the VR exit button. */
16
+ export const rayHitsExitButton = (app, origin, direction) => {
17
+ const script = app.root.findByName('xr-exit-button')?.script?.get(XrExitButton.scriptName);
18
+ return typeof script?.rayHitsButton === 'function' && script.rayHitsButton(origin, direction) === true;
19
+ };
20
+
21
+ /**
22
+ * True when the ray points at anything the trigger is meant to operate. Teleport consults this so a
23
+ * select that lands on UI doesn't also move the rig.
24
+ */
25
+ export const rayHitsInteractiveUi = (app, origin, direction) => rayHitsAnnotationHotspot(app, origin, direction) || rayHitsAnnotationPanel(app, origin, direction) || rayHitsExitButton(app, origin, direction);
@@ -0,0 +1,165 @@
1
+ import { Vec3 } from 'playcanvas';
2
+ import { VrAnnotationPanel } from './vr-annotation-panel';
3
+ import { XrExitButton } from './xr-exit-button';
4
+ import { rayHitsAnnotationHotspot, rayHitsAnnotationPanel, rayHitsExitButton, rayHitsInteractiveUi } from './xr-interactive-ui';
5
+
6
+ // Jest cannot parse the PlayCanvas ESM scripts that xr-annotation-candidates imports.
7
+ jest.mock('playcanvas/scripts/esm/annotations.mjs', () => ({
8
+ Annotation: {
9
+ scriptName: 'annotation'
10
+ }
11
+ }));
12
+ const ORIGIN = new Vec3(0, 0, 5);
13
+ const FORWARD = new Vec3(0, 0, -1);
14
+
15
+ /** Fake annotation hotspot entity: unit quad at `position`, scale 1, with the stock open callback. */
16
+ const hotspot = position => ({
17
+ getPosition: () => position,
18
+ getWorldTransform: () => ({
19
+ getScale: out => out.set(1, 1, 1)
20
+ }),
21
+ script: {
22
+ get: () => ({
23
+ onVrOpenCallback: () => undefined
24
+ })
25
+ }
26
+ });
27
+ /**
28
+ * Fake app whose `findByName` serves only the entities the helpers look up. Each script stub returns
29
+ * its fake only for the matching `scriptName`, so a lookup by the wrong name misses - the way a real
30
+ * `Script#get` would - instead of silently succeeding.
31
+ */
32
+ const fakeApp = ({
33
+ hotspots,
34
+ panelHit,
35
+ panelHasScript = true,
36
+ exitHit
37
+ }) => ({
38
+ root: {
39
+ findByName: name => {
40
+ if (name === 'annotations-root') {
41
+ return hotspots ? {
42
+ children: hotspots
43
+ } : null;
44
+ }
45
+ if (name === 'vr-annotation-panel') {
46
+ if (panelHit === undefined) {
47
+ return null;
48
+ }
49
+ return {
50
+ script: {
51
+ get: scriptName => scriptName === VrAnnotationPanel.scriptName ? panelHasScript ? {
52
+ rayHitsPanel: () => panelHit
53
+ } : {} : undefined
54
+ }
55
+ };
56
+ }
57
+ if (name === 'xr-exit-button') {
58
+ return exitHit === undefined ? null : {
59
+ script: {
60
+ get: scriptName => scriptName === XrExitButton.scriptName ? {
61
+ rayHitsButton: () => exitHit
62
+ } : undefined
63
+ }
64
+ };
65
+ }
66
+ return null;
67
+ }
68
+ }
69
+ });
70
+ describe('rayHitsAnnotationHotspot', () => {
71
+ it('is true when the ray enters a hotspot sphere', () => {
72
+ const app = fakeApp({
73
+ hotspots: [hotspot(new Vec3(0, 0, 0))]
74
+ });
75
+ expect(rayHitsAnnotationHotspot(app, ORIGIN, FORWARD)).toBe(true);
76
+ });
77
+ it('is false when the ray misses every hotspot', () => {
78
+ const app = fakeApp({
79
+ hotspots: [hotspot(new Vec3(0, 8, 0))]
80
+ });
81
+ expect(rayHitsAnnotationHotspot(app, ORIGIN, FORWARD)).toBe(false);
82
+ });
83
+ it('is false when there is no annotations root', () => {
84
+ expect(rayHitsAnnotationHotspot(fakeApp({}), ORIGIN, FORWARD)).toBe(false);
85
+ });
86
+
87
+ // Pins the derived radius (HOTSPOT_HALF_EXTENT 0.5 * scale 1 * HIT_RADIUS_PAD 2.5 = 1.25) against
88
+ // drift: a hotspot at (0,0,0) or (0,8,0) would pass the two tests above for nearly any radius.
89
+ it('is true just inside the derived hit radius', () => {
90
+ const app = fakeApp({
91
+ hotspots: [hotspot(new Vec3(0, 1.2, 0))]
92
+ });
93
+ expect(rayHitsAnnotationHotspot(app, ORIGIN, FORWARD)).toBe(true);
94
+ });
95
+ it('is false just outside the derived hit radius', () => {
96
+ const app = fakeApp({
97
+ hotspots: [hotspot(new Vec3(0, 1.3, 0))]
98
+ });
99
+ expect(rayHitsAnnotationHotspot(app, ORIGIN, FORWARD)).toBe(false);
100
+ });
101
+ });
102
+ describe('rayHitsAnnotationPanel', () => {
103
+ it('delegates to the panel script', () => {
104
+ expect(rayHitsAnnotationPanel(fakeApp({
105
+ panelHit: true
106
+ }), ORIGIN, FORWARD)).toBe(true);
107
+ expect(rayHitsAnnotationPanel(fakeApp({
108
+ panelHit: false
109
+ }), ORIGIN, FORWARD)).toBe(false);
110
+ });
111
+ it('is false when no panel is mounted', () => {
112
+ expect(rayHitsAnnotationPanel(fakeApp({}), ORIGIN, FORWARD)).toBe(false);
113
+ });
114
+ it('is false when the panel entity has no hit-test method', () => {
115
+ const app = fakeApp({
116
+ panelHit: true,
117
+ panelHasScript: false
118
+ });
119
+ expect(rayHitsAnnotationPanel(app, ORIGIN, FORWARD)).toBe(false);
120
+ });
121
+ });
122
+ describe('rayHitsExitButton', () => {
123
+ it('delegates to the exit button script', () => {
124
+ expect(rayHitsExitButton(fakeApp({
125
+ exitHit: true
126
+ }), ORIGIN, FORWARD)).toBe(true);
127
+ expect(rayHitsExitButton(fakeApp({
128
+ exitHit: false
129
+ }), ORIGIN, FORWARD)).toBe(false);
130
+ });
131
+ it('is false when no exit button is mounted', () => {
132
+ expect(rayHitsExitButton(fakeApp({}), ORIGIN, FORWARD)).toBe(false);
133
+ });
134
+ });
135
+ describe('rayHitsInteractiveUi', () => {
136
+ it('is false when the ray hits nothing interactive', () => {
137
+ const app = fakeApp({
138
+ hotspots: [hotspot(new Vec3(0, 8, 0))],
139
+ panelHit: false,
140
+ exitHit: false
141
+ });
142
+ expect(rayHitsInteractiveUi(app, ORIGIN, FORWARD)).toBe(false);
143
+ });
144
+ it('is true for a hotspot hit', () => {
145
+ const app = fakeApp({
146
+ hotspots: [hotspot(new Vec3(0, 0, 0))],
147
+ panelHit: false,
148
+ exitHit: false
149
+ });
150
+ expect(rayHitsInteractiveUi(app, ORIGIN, FORWARD)).toBe(true);
151
+ });
152
+ it('is true for a panel hit', () => {
153
+ expect(rayHitsInteractiveUi(fakeApp({
154
+ panelHit: true
155
+ }), ORIGIN, FORWARD)).toBe(true);
156
+ });
157
+ it('is true for an exit button hit', () => {
158
+ expect(rayHitsInteractiveUi(fakeApp({
159
+ exitHit: true
160
+ }), ORIGIN, FORWARD)).toBe(true);
161
+ });
162
+ it('is false in an empty scene', () => {
163
+ expect(rayHitsInteractiveUi(fakeApp({}), ORIGIN, FORWARD)).toBe(false);
164
+ });
165
+ });
@@ -0,0 +1,10 @@
1
+ import { Color, Script } from 'playcanvas';
2
+ /** Default ray colour (cyan) — bright enough to read against the scene. */
3
+ export declare const DEFAULT_RAY_COLOR: Color;
4
+ export declare class XrPointerRay extends Script {
5
+ static scriptName: string;
6
+ color: Color;
7
+ private _end;
8
+ update(): void;
9
+ }
10
+ //# sourceMappingURL=xr-pointer-ray.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xr-pointer-ray.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-pointer-ray.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAQ,MAAM,YAAY,CAAC;AAKjD,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,OAA0B,CAAC;AAEzD,qBAAa,YAAa,SAAQ,MAAM;IACtC,MAAM,CAAC,UAAU,SAAkB;IAEnC,KAAK,QAA6B;IAElC,OAAO,CAAC,IAAI,CAAc;IAE1B,MAAM;CAaP"}
@@ -0,0 +1,24 @@
1
+ import { Color, Script, Vec3 } from 'playcanvas';
2
+
3
+ /** Length of the drawn pointer ray, in world meters. */
4
+ const RAY_LENGTH = 5;
5
+
6
+ /** Default ray colour (cyan) — bright enough to read against the scene. */
7
+ export const DEFAULT_RAY_COLOR = new Color(0.25, 0.8, 1);
8
+ export class XrPointerRay extends Script {
9
+ static scriptName = 'xrPointerRay';
10
+ color = DEFAULT_RAY_COLOR.clone();
11
+ _end = new Vec3();
12
+ update() {
13
+ if (this.app.xr?.active !== true) {
14
+ return;
15
+ }
16
+ const sources = this.app.xr?.input?.inputSources ?? [];
17
+ for (const source of sources) {
18
+ const origin = source.getOrigin();
19
+ const direction = source.getDirection();
20
+ this._end.copy(direction).mulScalar(RAY_LENGTH).add(origin);
21
+ this.app.drawLine(origin, this._end, this.color, false);
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Draws a circular progress pie into the `size` x `size` region at the context origin: a faint
3
+ * full-circle track under a wedge that sweeps clockwise as `progress` goes 0 -> 1. A `progress`
4
+ * above 1 simply sweeps a full circle. Neither clears the context nor uploads a texture, so callers
5
+ * can composite it between their own layers. Saves and restores the drawing state it touches and
6
+ * leaves the path clean on exit, so callers can stroke or fill right after without an explicit
7
+ * `beginPath()` of their own.
8
+ */
9
+ export declare const drawProgressPie: (ctx: CanvasRenderingContext2D, size: number, progress: number) => void;
10
+ //# sourceMappingURL=xr-progress-pie.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xr-progress-pie.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-progress-pie.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,wBAAwB,EAAE,MAAM,MAAM,EAAE,UAAU,MAAM,SAsB5F,CAAC"}
@@ -0,0 +1,30 @@
1
+ /** Sweep starts at 12 o'clock. */
2
+ const PIE_START_ANGLE = -Math.PI / 2;
3
+
4
+ /**
5
+ * Draws a circular progress pie into the `size` x `size` region at the context origin: a faint
6
+ * full-circle track under a wedge that sweeps clockwise as `progress` goes 0 -> 1. A `progress`
7
+ * above 1 simply sweeps a full circle. Neither clears the context nor uploads a texture, so callers
8
+ * can composite it between their own layers. Saves and restores the drawing state it touches and
9
+ * leaves the path clean on exit, so callers can stroke or fill right after without an explicit
10
+ * `beginPath()` of their own.
11
+ */
12
+ export const drawProgressPie = (ctx, size, progress) => {
13
+ const c = size / 2;
14
+ const r = c * 0.92;
15
+ ctx.save();
16
+ ctx.beginPath();
17
+ ctx.arc(c, c, r, 0, Math.PI * 2);
18
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.18)';
19
+ ctx.fill();
20
+ if (progress > 0) {
21
+ ctx.beginPath();
22
+ ctx.moveTo(c, c);
23
+ ctx.arc(c, c, r, PIE_START_ANGLE, PIE_START_ANGLE + progress * Math.PI * 2);
24
+ ctx.closePath();
25
+ ctx.fillStyle = 'rgba(64, 200, 255, 0.85)';
26
+ ctx.fill();
27
+ }
28
+ ctx.restore();
29
+ ctx.beginPath();
30
+ };
@@ -0,0 +1,79 @@
1
+ import { drawProgressPie } from './xr-progress-pie';
2
+ const TRACK_FILL = 'rgba(255, 255, 255, 0.18)';
3
+ const WEDGE_FILL = 'rgba(64, 200, 255, 0.85)';
4
+
5
+ /** Minimal 2D-context stand-in that records the arcs drawn and the fill colour used for each. */
6
+ const stubContext = () => {
7
+ const arcs = [];
8
+ const fills = [];
9
+ const ctx = {
10
+ fillStyle: '',
11
+ save: () => undefined,
12
+ restore: () => undefined,
13
+ beginPath: () => undefined,
14
+ moveTo: () => undefined,
15
+ closePath: () => undefined,
16
+ arc: (_x, _y, radius, start, end) => {
17
+ arcs.push({
18
+ radius,
19
+ start,
20
+ end
21
+ });
22
+ },
23
+ fill: () => {
24
+ fills.push(ctx.fillStyle);
25
+ }
26
+ };
27
+ return {
28
+ ctx: ctx,
29
+ arcs,
30
+ fills
31
+ };
32
+ };
33
+ describe('drawProgressPie', () => {
34
+ it('draws only the full-circle track at zero progress', () => {
35
+ const {
36
+ ctx,
37
+ arcs,
38
+ fills
39
+ } = stubContext();
40
+ drawProgressPie(ctx, 128, 0);
41
+ expect(arcs).toHaveLength(1);
42
+ expect(arcs[0].end - arcs[0].start).toBeCloseTo(Math.PI * 2);
43
+ expect(fills).toEqual([TRACK_FILL]);
44
+ });
45
+ it('sweeps a quarter turn clockwise from the top at quarter progress', () => {
46
+ const {
47
+ ctx,
48
+ arcs
49
+ } = stubContext();
50
+ drawProgressPie(ctx, 128, 0.25);
51
+ expect(arcs).toHaveLength(2);
52
+ expect(arcs[1].start).toBeCloseTo(-Math.PI / 2);
53
+ expect(arcs[1].end).toBeCloseTo(0);
54
+ });
55
+ it('sweeps a full turn at full progress', () => {
56
+ const {
57
+ ctx,
58
+ arcs
59
+ } = stubContext();
60
+ drawProgressPie(ctx, 128, 1);
61
+ expect(arcs[1].end - arcs[1].start).toBeCloseTo(Math.PI * 2);
62
+ });
63
+ it('fills the wedge over the track', () => {
64
+ const {
65
+ ctx,
66
+ fills
67
+ } = stubContext();
68
+ drawProgressPie(ctx, 128, 0.5);
69
+ expect(fills).toEqual([TRACK_FILL, WEDGE_FILL]);
70
+ });
71
+ it('scales the radius to the drawn size', () => {
72
+ const {
73
+ ctx,
74
+ arcs
75
+ } = stubContext();
76
+ drawProgressPie(ctx, 256, 0);
77
+ expect(arcs[0].radius).toBeCloseTo(128 * 0.92);
78
+ });
79
+ });
@@ -0,0 +1,13 @@
1
+ import { Vec3 } from 'playcanvas';
2
+ export interface QuadHit {
3
+ u: number;
4
+ v: number;
5
+ }
6
+ /**
7
+ * Intersects the ray with the rectangle centred at `center` and spanned by unit axes `right`/`up`,
8
+ * returning the hit in normalized quad coordinates (u,v in [-1,1]) or null. `u`/`v` divide the
9
+ * in-plane offset by `halfWidth`/`halfHeight`. Null when the ray is parallel to the plane, the
10
+ * plane is behind the origin, or the hit lands outside the rectangle.
11
+ */
12
+ export declare const rayQuadHit: (origin: Vec3, direction: Vec3, center: Vec3, right: Vec3, up: Vec3, halfWidth: number, halfHeight: number) => QuadHit | null;
13
+ //# sourceMappingURL=xr-ray-quad.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xr-ray-quad.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-ray-quad.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAElC,MAAM,WAAW,OAAO;IACtB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GACrB,QAAQ,IAAI,EACZ,WAAW,IAAI,EACf,QAAQ,IAAI,EACZ,OAAO,IAAI,EACX,IAAI,IAAI,EACR,WAAW,MAAM,EACjB,YAAY,MAAM,KACjB,OAAO,GAAG,IAqBZ,CAAC"}