@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.
- package/components/VirtualWalkthrough/Annotation.d.ts.map +1 -1
- package/components/VirtualWalkthrough/Annotation.js +10 -0
- package/components/VirtualWalkthrough/BoundaryWall.d.ts +11 -0
- package/components/VirtualWalkthrough/BoundaryWall.d.ts.map +1 -0
- package/components/VirtualWalkthrough/BoundaryWall.js +35 -0
- package/components/VirtualWalkthrough/TfXrNavigation.d.ts +42 -6
- package/components/VirtualWalkthrough/TfXrNavigation.d.ts.map +1 -1
- package/components/VirtualWalkthrough/TfXrNavigation.js +109 -9
- package/components/VirtualWalkthrough/TfXrNavigation.test.js +110 -0
- package/components/VirtualWalkthrough/VirtualWalkthroughViewer.d.ts.map +1 -1
- package/components/VirtualWalkthrough/VirtualWalkthroughViewer.js +50 -3
- package/components/VirtualWalkthrough/VrAnnotationPanel.d.ts +9 -0
- package/components/VirtualWalkthrough/VrAnnotationPanel.d.ts.map +1 -0
- package/components/VirtualWalkthrough/VrAnnotationPanel.js +22 -0
- package/components/VirtualWalkthrough/XrAnnotationInteraction.d.ts +6 -0
- package/components/VirtualWalkthrough/XrAnnotationInteraction.d.ts.map +1 -0
- package/components/VirtualWalkthrough/XrAnnotationInteraction.js +15 -0
- package/components/VirtualWalkthrough/XrGazeDwell.d.ts +6 -0
- package/components/VirtualWalkthrough/XrGazeDwell.d.ts.map +1 -0
- package/components/VirtualWalkthrough/XrGazeDwell.js +15 -0
- package/components/VirtualWalkthrough/XrPointerRay.d.ts +7 -0
- package/components/VirtualWalkthrough/XrPointerRay.d.ts.map +1 -0
- package/components/VirtualWalkthrough/XrPointerRay.js +15 -0
- package/components/VirtualWalkthrough/boundary-wall.d.ts +86 -0
- package/components/VirtualWalkthrough/boundary-wall.d.ts.map +1 -0
- package/components/VirtualWalkthrough/boundary-wall.js +331 -0
- package/components/VirtualWalkthrough/boundary-wall.test.js +203 -0
- package/components/VirtualWalkthrough/vr-annotation-panel-layout.d.ts +6 -0
- package/components/VirtualWalkthrough/vr-annotation-panel-layout.d.ts.map +1 -0
- package/components/VirtualWalkthrough/vr-annotation-panel-layout.js +22 -0
- package/components/VirtualWalkthrough/vr-annotation-panel-layout.test.js +26 -0
- package/components/VirtualWalkthrough/vr-annotation-panel.d.ts +40 -0
- package/components/VirtualWalkthrough/vr-annotation-panel.d.ts.map +1 -0
- package/components/VirtualWalkthrough/vr-annotation-panel.js +312 -0
- package/components/VirtualWalkthrough/xr-annotation-candidates.d.ts +18 -0
- package/components/VirtualWalkthrough/xr-annotation-candidates.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-annotation-candidates.js +38 -0
- package/components/VirtualWalkthrough/xr-annotation-interaction.d.ts +13 -0
- package/components/VirtualWalkthrough/xr-annotation-interaction.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-annotation-interaction.js +59 -0
- package/components/VirtualWalkthrough/xr-annotation-targeting.d.ts +10 -0
- package/components/VirtualWalkthrough/xr-annotation-targeting.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-annotation-targeting.js +36 -0
- package/components/VirtualWalkthrough/xr-annotation-targeting.test.js +66 -0
- package/components/VirtualWalkthrough/xr-button-arming.d.ts +14 -0
- package/components/VirtualWalkthrough/xr-button-arming.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-button-arming.js +19 -0
- package/components/VirtualWalkthrough/xr-button-arming.test.js +35 -0
- package/components/VirtualWalkthrough/xr-button-hold.d.ts +20 -0
- package/components/VirtualWalkthrough/xr-button-hold.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-button-hold.js +43 -0
- package/components/VirtualWalkthrough/xr-button-hold.test.js +57 -0
- package/components/VirtualWalkthrough/xr-exit-button.d.ts +21 -4
- package/components/VirtualWalkthrough/xr-exit-button.d.ts.map +1 -1
- package/components/VirtualWalkthrough/xr-exit-button.js +98 -37
- package/components/VirtualWalkthrough/xr-face-buttons.d.ts +15 -0
- package/components/VirtualWalkthrough/xr-face-buttons.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-face-buttons.js +31 -0
- package/components/VirtualWalkthrough/xr-face-buttons.test.js +82 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell-state.d.ts +17 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell-state.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell-state.js +39 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell-state.test.js +61 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell.d.ts +30 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-gaze-dwell.js +173 -0
- package/components/VirtualWalkthrough/xr-interactive-ui.d.ts +13 -0
- package/components/VirtualWalkthrough/xr-interactive-ui.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-interactive-ui.js +25 -0
- package/components/VirtualWalkthrough/xr-interactive-ui.test.js +165 -0
- package/components/VirtualWalkthrough/xr-pointer-ray.d.ts +10 -0
- package/components/VirtualWalkthrough/xr-pointer-ray.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-pointer-ray.js +24 -0
- package/components/VirtualWalkthrough/xr-progress-pie.d.ts +10 -0
- package/components/VirtualWalkthrough/xr-progress-pie.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-progress-pie.js +30 -0
- package/components/VirtualWalkthrough/xr-progress-pie.test.js +79 -0
- package/components/VirtualWalkthrough/xr-ray-quad.d.ts +13 -0
- package/components/VirtualWalkthrough/xr-ray-quad.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-ray-quad.js +29 -0
- package/components/VirtualWalkthrough/xr-ray-quad.test.js +37 -0
- package/components/VirtualWalkthrough/xr-scene-bounds.d.ts +17 -0
- package/components/VirtualWalkthrough/xr-scene-bounds.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-scene-bounds.js +41 -0
- package/components/VirtualWalkthrough/xr-scene-bounds.test.js +70 -0
- package/components/VirtualWalkthrough/xr-teleport-gesture.d.ts +30 -0
- package/components/VirtualWalkthrough/xr-teleport-gesture.d.ts.map +1 -0
- package/components/VirtualWalkthrough/xr-teleport-gesture.js +45 -0
- package/components/VirtualWalkthrough/xr-teleport-gesture.test.js +90 -0
- package/hooks/useXr.d.ts +2 -0
- package/hooks/useXr.d.ts.map +1 -1
- package/hooks/useXr.js +10 -6
- 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"}
|