@terraware/web-components 5.0.1 → 5.0.3

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/SplatModel.js +2 -1
  2. package/components/VirtualWalkthrough/SplatRevealRain.d.ts +3 -1
  3. package/components/VirtualWalkthrough/SplatRevealRain.d.ts.map +1 -1
  4. package/components/VirtualWalkthrough/SplatRevealRain.js +15 -8
  5. package/components/VirtualWalkthrough/VirtualWalkthroughViewer.d.ts +12 -1
  6. package/components/VirtualWalkthrough/VirtualWalkthroughViewer.d.ts.map +1 -1
  7. package/components/VirtualWalkthrough/VirtualWalkthroughViewer.js +42 -5
  8. package/components/VirtualWalkthrough/useRestartOnVr.d.ts +16 -0
  9. package/components/VirtualWalkthrough/useRestartOnVr.d.ts.map +1 -0
  10. package/components/VirtualWalkthrough/useRestartOnVr.js +38 -0
  11. package/components/VirtualWalkthrough/xr-annotation-candidate-buffer.d.ts +32 -0
  12. package/components/VirtualWalkthrough/xr-annotation-candidate-buffer.d.ts.map +1 -0
  13. package/components/VirtualWalkthrough/xr-annotation-candidate-buffer.js +88 -0
  14. package/components/VirtualWalkthrough/xr-annotation-candidates.d.ts +8 -12
  15. package/components/VirtualWalkthrough/xr-annotation-candidates.d.ts.map +1 -1
  16. package/components/VirtualWalkthrough/xr-annotation-candidates.js +11 -32
  17. package/components/VirtualWalkthrough/xr-annotation-targeting.d.ts.map +1 -1
  18. package/components/VirtualWalkthrough/xr-annotation-targeting.js +9 -4
  19. package/components/VirtualWalkthrough/xr-exit-button-mask.d.ts +11 -0
  20. package/components/VirtualWalkthrough/xr-exit-button-mask.d.ts.map +1 -0
  21. package/components/VirtualWalkthrough/xr-exit-button-mask.js +41 -0
  22. package/components/VirtualWalkthrough/xr-exit-button.d.ts +3 -16
  23. package/components/VirtualWalkthrough/xr-exit-button.d.ts.map +1 -1
  24. package/components/VirtualWalkthrough/xr-exit-button.js +30 -93
  25. package/components/VirtualWalkthrough/xr-gaze-dwell.d.ts +14 -9
  26. package/components/VirtualWalkthrough/xr-gaze-dwell.d.ts.map +1 -1
  27. package/components/VirtualWalkthrough/xr-gaze-dwell.js +72 -90
  28. package/components/VirtualWalkthrough/xr-progress-pie-material.d.ts +24 -0
  29. package/components/VirtualWalkthrough/xr-progress-pie-material.d.ts.map +1 -0
  30. package/components/VirtualWalkthrough/xr-progress-pie-material.js +174 -0
  31. package/components/VirtualWalkthrough/xr-progress-pie.d.ts +7 -7
  32. package/components/VirtualWalkthrough/xr-progress-pie.d.ts.map +1 -1
  33. package/components/VirtualWalkthrough/xr-progress-pie.js +8 -29
  34. package/hooks/useXr.d.ts.map +1 -1
  35. package/hooks/useXr.js +6 -4
  36. package/package.json +1 -1
@@ -1,8 +1,10 @@
1
- import { Script, Vec3, Quat, XRTYPE_VR, Texture, FILTER_LINEAR, ADDRESS_CLAMP_TO_EDGE, Mesh, StandardMaterial, Color, BLEND_NORMAL, CULLFACE_NONE, MeshInstance, LAYERID_IMMEDIATE } from 'playcanvas';
1
+ import { Script, Vec3, Quat, XRTYPE_VR, Texture, FILTER_LINEAR, ADDRESS_CLAMP_TO_EDGE, MeshInstance, LAYERID_IMMEDIATE } from 'playcanvas';
2
2
  import { ButtonArmingLatch } from './xr-button-arming.js';
3
3
  import { INITIAL_HOLD_STATE, advanceHold } from './xr-button-hold.js';
4
+ import { drawExitButtonMask } from './xr-exit-button-mask.js';
4
5
  import { FaceButtonPressTracker, secondaryFaceButtonPressed } from './xr-face-buttons.js';
5
- import { drawProgressPie } from './xr-progress-pie.js';
6
+ import { pieShaderProgress } from './xr-progress-pie.js';
7
+ import { createProgressPieMaterial, progressPieQuadMesh } from './xr-progress-pie-material.js';
6
8
 
7
9
  /**
8
10
  * Boolean ray-sphere hit test. Treats the ray as a half-line (t >= 0) and returns true when it
@@ -20,6 +22,9 @@ const raySphereIntersect = (origin, direction, center, radius) => {
20
22
  };
21
23
  const TEXTURE_SIZE = 256;
22
24
 
25
+ /** Opacity of the button while it is not being aimed at. */
26
+ const IDLE_OPACITY = 0.9;
27
+
23
28
  /** Seconds of continuous B/Y hold required to leave the session. Matches the gaze-dwell threshold. */
24
29
  const EXIT_HOLD_SECONDS = 1.25;
25
30
  class XrExitButton extends Script {
@@ -38,7 +43,6 @@ class XrExitButton extends Script {
38
43
  _headRotation = new Quat();
39
44
  _worldOffset = new Vec3();
40
45
  _faceButtons = new FaceButtonPressTracker();
41
- _drawnProgress = -1;
42
46
  _hold = INITIAL_HOLD_STATE;
43
47
  _arming = new ButtonArmingLatch();
44
48
  _isVrActive = () => this.app.xr?.active === true && this.app.xr?.type === XRTYPE_VR;
@@ -51,10 +55,7 @@ class XrExitButton extends Script {
51
55
  this._hovered = false;
52
56
  this._resetProgress();
53
57
  this.entity.setLocalScale(1, 1, 1);
54
- if (this._material) {
55
- this._material.opacity = 0.9;
56
- this._material.update();
57
- }
58
+ this._material?.setParameter('uOpacity', IDLE_OPACITY);
58
59
  };
59
60
  _onSelect = inputSource => {
60
61
  if (!this.entity.enabled) {
@@ -72,15 +73,19 @@ class XrExitButton extends Script {
72
73
  }
73
74
  return raySphereIntersect(origin, direction, this.entity.getPosition(), this.hitRadius);
74
75
  }
75
- _createCanvas() {
76
+
77
+ /** The button's static artwork, painted once. Only the sweep changes per frame, and that lives in
78
+ * the shader. */
79
+ _createMaskTexture() {
76
80
  const canvas = document.createElement('canvas');
77
81
  canvas.width = TEXTURE_SIZE;
78
82
  canvas.height = TEXTURE_SIZE;
79
- return canvas;
80
- }
81
- _createTexture(canvas) {
83
+ const ctx = canvas.getContext('2d');
84
+ if (ctx) {
85
+ drawExitButtonMask(ctx, TEXTURE_SIZE);
86
+ }
82
87
  const texture = new Texture(this.app.graphicsDevice, {
83
- name: 'xr-exit-button',
88
+ name: 'xr-exit-button-mask',
84
89
  width: TEXTURE_SIZE,
85
90
  height: TEXTURE_SIZE,
86
91
  addressU: ADDRESS_CLAMP_TO_EDGE,
@@ -92,85 +97,22 @@ class XrExitButton extends Script {
92
97
  texture.setSource(canvas);
93
98
  return texture;
94
99
  }
95
-
96
- /**
97
- * Repaints the button: dark disc, then the hold progress wedge, then the X arms on top so the
98
- * glyph stays legible over the fill. The pie is skipped entirely at zero progress so the idle
99
- * button doesn't wear the pie's faint track. Returns whether it painted, so callers can tell a
100
- * missing context from a successful repaint.
101
- */
102
- _drawButton(progress) {
103
- const ctx = this._context;
104
- if (!ctx) {
105
- return false;
106
- }
107
- const c = TEXTURE_SIZE / 2;
108
- ctx.clearRect(0, 0, TEXTURE_SIZE, TEXTURE_SIZE);
109
- ctx.fillStyle = 'rgba(20, 20, 20, 0.75)';
110
- ctx.beginPath();
111
- ctx.arc(c, c, c * 0.92, 0, Math.PI * 2);
112
- ctx.fill();
113
- if (progress > 0) {
114
- drawProgressPie(ctx, TEXTURE_SIZE, progress);
115
- }
116
- const arm = TEXTURE_SIZE * 0.24;
117
- ctx.strokeStyle = '#ffffff';
118
- ctx.lineWidth = TEXTURE_SIZE * 0.09;
119
- ctx.lineCap = 'round';
120
- ctx.beginPath();
121
- ctx.moveTo(c - arm, c - arm);
122
- ctx.lineTo(c + arm, c + arm);
123
- ctx.moveTo(c + arm, c - arm);
124
- ctx.lineTo(c - arm, c + arm);
125
- ctx.stroke();
126
- this._texture?.upload();
127
- return true;
128
- }
129
-
130
- /** Repaints only when the drawn wedge would visibly move. Only records the paint on success, so a
131
- * missing context doesn't fool the dedupe cache into thinking a repaint already happened. */
132
- _setProgress(progress) {
133
- if (Math.abs(progress - this._drawnProgress) <= 0.001) {
134
- return;
135
- }
136
- if (this._drawButton(progress)) {
137
- this._drawnProgress = progress;
138
- }
139
- }
140
100
  _resetProgress() {
141
101
  this._hold = INITIAL_HOLD_STATE;
142
- this._drawnProgress = -1;
143
- this._setProgress(0);
144
- }
145
- _createMesh() {
146
- const h = this.halfSize;
147
- const mesh = new Mesh(this.app.graphicsDevice);
148
- mesh.setPositions([-h, -h, 0, h, -h, 0, h, h, 0, -h, h, 0]);
149
- mesh.setNormals([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]);
150
- mesh.setUvs(0, [0, 1, 1, 1, 1, 0, 0, 0]);
151
- mesh.setIndices([0, 1, 2, 0, 2, 3]);
152
- mesh.update();
153
- return mesh;
102
+ this._material?.setParameter('uProgress', 0);
154
103
  }
155
104
  initialize() {
156
- this._canvas = this._createCanvas();
157
- this._context = this._canvas.getContext('2d') ?? undefined;
158
- this._texture = this._createTexture(this._canvas);
105
+ this._texture = this._createMaskTexture();
106
+ this._material = createProgressPieMaterial({
107
+ uniqueName: 'xr-exit-button',
108
+ maskMap: this._texture,
109
+ discColor: [20 / 255, 20 / 255, 20 / 255],
110
+ discAlpha: 0.75
111
+ });
112
+ this._material.setParameter('uOpacity', IDLE_OPACITY);
159
113
  this._resetProgress();
160
- const material = new StandardMaterial();
161
- material.useLighting = false;
162
- material.emissive = new Color(1, 1, 1);
163
- material.emissiveMap = this._texture;
164
- material.opacityMap = this._texture;
165
- material.opacityMapChannel = 'a';
166
- material.blendType = BLEND_NORMAL;
167
- material.depthTest = false;
168
- material.depthWrite = false;
169
- material.cull = CULLFACE_NONE;
170
- material.update();
171
- this._material = material;
172
- this._mesh = this._createMesh();
173
- const meshInstance = new MeshInstance(this._mesh, material);
114
+ this._mesh = progressPieQuadMesh(this.app.graphicsDevice, this.halfSize);
115
+ const meshInstance = new MeshInstance(this._mesh, this._material);
174
116
 
175
117
  // Immediate layer draws after the World layer (where the splats render) so the button is never
176
118
  // composited behind them; depthTest:false keeps it on top within the layer.
@@ -211,7 +153,7 @@ class XrExitButton extends Script {
211
153
  }
212
154
  const hold = advanceHold(this._hold, holdActive, dt, EXIT_HOLD_SECONDS);
213
155
  this._hold = hold.state;
214
- this._setProgress(hold.progress);
156
+ this._material?.setParameter('uProgress', pieShaderProgress(hold.progress));
215
157
  if (hold.justFired) {
216
158
  this.app.xr?.end();
217
159
  return;
@@ -220,10 +162,7 @@ class XrExitButton extends Script {
220
162
  this._hovered = hovered;
221
163
  const scale = hovered ? 1.15 : 1;
222
164
  this.entity.setLocalScale(scale, scale, scale);
223
- if (this._material) {
224
- this._material.opacity = hovered ? 1 : 0.9;
225
- this._material.update();
226
- }
165
+ this._material?.setParameter('uOpacity', hovered ? 1 : IDLE_OPACITY);
227
166
  }
228
167
  }
229
168
 
@@ -262,8 +201,6 @@ class XrExitButton extends Script {
262
201
  this._mesh = undefined;
263
202
  this._material = undefined;
264
203
  this._texture = undefined;
265
- this._canvas = undefined;
266
- this._context = undefined;
267
204
  }
268
205
  }
269
206
 
@@ -5,26 +5,31 @@ export declare class XrGazeDwell extends Script {
5
5
  * dwell opens OTHER annotations while one is open (and never re-dwells the open one). */
6
6
  activeIndex: number;
7
7
  private _dwell;
8
+ private _candidates;
8
9
  private _pieEntity?;
9
- private _pieCanvas?;
10
- private _pieTexture?;
11
10
  private _pieMaterial?;
12
11
  private _pieMesh?;
13
- private _drawnProgress;
12
+ private _pieMask?;
14
13
  private _headPos;
15
14
  private _headRot;
16
15
  private _viewOffset;
17
16
  private _forward;
18
17
  private _scratchScale;
19
18
  private _isVrActive;
19
+ /**
20
+ * A render component hands its mesh instances to a layer only when it flips from disabled to
21
+ * enabled, and hands over nothing if the layer cannot be resolved yet. The session events are the
22
+ * points where the layers are known to exist, so the pie flips there: start registers it, and end
23
+ * restores the transition for the next session.
24
+ */
25
+ private _onXrStart;
26
+ private _onXrEnd;
27
+ private _setPieEnabled;
20
28
  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
29
  update(dt: number): void;
26
- private _showPie;
27
- private _hidePie;
30
+ private _trackHead;
31
+ /** Sits the pie on the hotspot, square to the head. */
32
+ private _placePie;
28
33
  destroy(): void;
29
34
  }
30
35
  //# sourceMappingURL=xr-gaze-dwell.d.ts.map
@@ -1 +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"}
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,8 +1,10 @@
1
- import { Vec3, Script, Quat, XRTYPE_VR, Texture, FILTER_LINEAR, ADDRESS_CLAMP_TO_EDGE, StandardMaterial, Color, BLEND_NORMAL, CULLFACE_NONE, MeshInstance, Entity, LAYERID_IMMEDIATE, Mesh } from 'playcanvas';
2
- import { HOTSPOT_HALF_EXTENT, collectAnnotationHitCandidates } from './xr-annotation-candidates.js';
1
+ import { Vec3, Script, Quat, XRTYPE_VR, MeshInstance, Entity, LAYERID_IMMEDIATE } from 'playcanvas';
2
+ import { createAnnotationCandidateBuffer } from './xr-annotation-candidates.js';
3
3
  import { nearestAnnotationHit } from './xr-annotation-targeting.js';
4
4
  import { INITIAL_DWELL_STATE, advanceDwell } from './xr-gaze-dwell-state.js';
5
- import { drawProgressPie } from './xr-progress-pie.js';
5
+ import { pieShaderProgress } from './xr-progress-pie.js';
6
+ import { emptyMaskTexture, createProgressPieMaterial, progressPieQuadMesh } from './xr-progress-pie-material.js';
7
+ import { HOTSPOT_HALF_EXTENT } from './xr-annotation-candidate-buffer.js';
6
8
 
7
9
  /** Multiplier on the hotspot's world radius for gaze targeting. */
8
10
  const GAZE_HIT_RADIUS_PAD = 5;
@@ -12,9 +14,6 @@ const DWELL_SECONDS = 1.25;
12
14
 
13
15
  /** Progress-pie quad radius as a multiple of the hotspot's rendered half-extent (covers the hotspot). */
14
16
  const PIE_RADIUS_SCALE = 1.25;
15
-
16
- /** Progress-pie texture resolution. */
17
- const PIE_TEXTURE_SIZE = 128;
18
17
  const LOCAL_FORWARD = new Vec3(0, 0, -1);
19
18
  class XrGazeDwell extends Script {
20
19
  static scriptName = 'xrGazeDwell';
@@ -23,42 +22,48 @@ class XrGazeDwell extends Script {
23
22
  * dwell opens OTHER annotations while one is open (and never re-dwells the open one). */
24
23
  activeIndex = -1;
25
24
  _dwell = INITIAL_DWELL_STATE;
26
- _drawnProgress = -1;
25
+ _candidates = createAnnotationCandidateBuffer();
27
26
  _headPos = new Vec3();
28
27
  _headRot = new Quat();
29
28
  _viewOffset = new Vec3();
30
29
  _forward = new Vec3();
31
30
  _scratchScale = new Vec3();
32
31
  _isVrActive = () => this.app.xr?.active === true && this.app.xr?.type === XRTYPE_VR;
32
+
33
+ /**
34
+ * A render component hands its mesh instances to a layer only when it flips from disabled to
35
+ * enabled, and hands over nothing if the layer cannot be resolved yet. The session events are the
36
+ * points where the layers are known to exist, so the pie flips there: start registers it, and end
37
+ * restores the transition for the next session.
38
+ */
39
+ _onXrStart = () => {
40
+ this._dwell = INITIAL_DWELL_STATE;
41
+ this._setPieEnabled(this._isVrActive());
42
+ };
43
+ _onXrEnd = () => {
44
+ this._dwell = INITIAL_DWELL_STATE;
45
+ this._pieMaterial?.setParameter('uProgress', 0);
46
+ this._setPieEnabled(false);
47
+ };
48
+ _setPieEnabled(enabled) {
49
+ if (this._pieEntity) {
50
+ this._pieEntity.enabled = enabled;
51
+ }
52
+ }
33
53
  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
54
+ this._pieMask = emptyMaskTexture(this.app.graphicsDevice);
55
+ this._pieMaterial = createProgressPieMaterial({
56
+ uniqueName: 'xr-gaze-dwell-pie',
57
+ maskMap: this._pieMask
46
58
  });
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);
59
+ this._pieMesh = progressPieQuadMesh(this.app.graphicsDevice, HOTSPOT_HALF_EXTENT);
60
+ const meshInstance = new MeshInstance(this._pieMesh, this._pieMaterial);
61
+
62
+ // Never frustum-culled. The quad only takes a position once a sweep starts, and until then sits
63
+ // at the origin where culling would drop it from the render list - taking the shader compile it
64
+ // stays enabled for along with it. An always-submitted quad that discards every fragment costs
65
+ // less than that compile landing mid-gaze.
66
+ meshInstance.cull = false;
62
67
  this._pieEntity = new Entity('xr-gaze-dwell-pie');
63
68
  this._pieEntity.addComponent('render', {
64
69
  meshInstances: [meshInstance],
@@ -66,61 +71,31 @@ class XrGazeDwell extends Script {
66
71
  });
67
72
  this._pieEntity.enabled = false;
68
73
  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
74
 
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;
75
+ // Both orders happen: the walkthrough can mount into a session that has already started (as it
76
+ // does when VR is entered from outside the canvas), or mount first and wait for one.
77
+ this._setPieEnabled(this._isVrActive());
78
+ this.app.xr?.on('start', this._onXrStart);
79
+ this.app.xr?.on('end', this._onXrEnd);
104
80
  }
105
81
  update(dt) {
106
- if (!this._pieEntity) {
82
+ if (!this._pieEntity || !this._pieMaterial) {
107
83
  return;
108
84
  }
109
85
  if (!this._isVrActive() || !this._trackHead()) {
110
86
  this._dwell = INITIAL_DWELL_STATE;
111
- this._hidePie();
87
+ this._pieMaterial.setParameter('uProgress', 0);
112
88
  return;
113
89
  }
114
- const activeName = `annotation-${this.activeIndex}`;
115
- const candidates = collectAnnotationHitCandidates(this.app, GAZE_HIT_RADIUS_PAD).filter(candidate => candidate.entity.name !== activeName);
90
+ const candidates = this._candidates.collect(this.app, GAZE_HIT_RADIUS_PAD, `annotation-${this.activeIndex}`);
116
91
  this._headRot.transformVector(LOCAL_FORWARD, this._forward);
117
92
  const target = nearestAnnotationHit(this._headPos, this._forward, candidates);
118
93
  const result = advanceDwell(this._dwell, target, dt, DWELL_SECONDS);
119
94
  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();
95
+ const progress = pieShaderProgress(result.progress);
96
+ this._pieMaterial.setParameter('uProgress', progress);
97
+ if (progress > 0 && target !== null) {
98
+ this._placePie(candidates[target].entity);
124
99
  }
125
100
  if (result.justOpened && target !== null) {
126
101
  const {
@@ -132,30 +107,37 @@ class XrGazeDwell extends Script {
132
107
  script.onVrOpenCallback(screen?.x ?? 0, screen?.y ?? 0);
133
108
  }
134
109
  }
135
- _showPie(entity, progress) {
110
+ _trackHead() {
111
+ const views = this.app.xr?.views?.list;
112
+ if (!views || views.length === 0) {
113
+ return false;
114
+ }
115
+ this._headPos.set(0, 0, 0);
116
+ for (const view of views) {
117
+ view.viewInvOffMat.getTranslation(this._viewOffset);
118
+ this._headPos.add(this._viewOffset);
119
+ }
120
+ this._headPos.mulScalar(1 / views.length);
121
+ this._headRot.setFromMat4(views[0].viewInvOffMat);
122
+ return true;
123
+ }
124
+
125
+ /** Sits the pie on the hotspot, square to the head. */
126
+ _placePie(entity) {
136
127
  if (!this._pieEntity) {
137
128
  return;
138
129
  }
139
- if (Math.abs(progress - this._drawnProgress) > 0.001) {
140
- this._drawnProgress = progress;
141
- this._drawPie(progress);
142
- }
143
130
 
144
131
  // The unit quad has half-extent HOTSPOT_HALF_EXTENT, so scale it to reach the desired world size.
145
132
  entity.getWorldTransform().getScale(this._scratchScale);
146
133
  const scale = this._scratchScale.x * PIE_RADIUS_SCALE;
147
- this._pieEntity.enabled = true;
148
134
  this._pieEntity.setLocalScale(scale, scale, scale);
149
135
  this._pieEntity.setPosition(entity.getPosition());
150
136
  this._pieEntity.setRotation(this._headRot);
151
137
  }
152
- _hidePie() {
153
- if (this._pieEntity && this._pieEntity.enabled) {
154
- this._pieEntity.enabled = false;
155
- this._drawnProgress = -1;
156
- }
157
- }
158
138
  destroy() {
139
+ this.app.xr?.off('start', this._onXrStart);
140
+ this.app.xr?.off('end', this._onXrEnd);
159
141
  if (this._pieEntity) {
160
142
  if (this._pieEntity.render) {
161
143
  this._pieEntity.render.meshInstances = [];
@@ -165,10 +147,10 @@ class XrGazeDwell extends Script {
165
147
  }
166
148
  this._pieMesh?.destroy();
167
149
  this._pieMaterial?.destroy();
168
- this._pieTexture?.destroy();
150
+ this._pieMask?.destroy();
169
151
  this._pieMesh = undefined;
170
152
  this._pieMaterial = undefined;
171
- this._pieTexture = undefined;
153
+ this._pieMask = undefined;
172
154
  }
173
155
  }
174
156
 
@@ -0,0 +1,24 @@
1
+ import { GraphicsDevice, Mesh, ShaderMaterial, Texture } from 'playcanvas';
2
+ /** A camera-facing unit quad with y-down UVs, sized to `halfExtent` in local units. */
3
+ export declare const progressPieQuadMesh: (device: GraphicsDevice, halfExtent: number) => Mesh;
4
+ /**
5
+ * A single transparent texel, for pies that carry no static artwork of their own.
6
+ *
7
+ * The filters are set explicitly because the default minification filter samples a mipmap chain,
8
+ * which a single-level texture does not have. That combination is incomplete in WebGL, and what a
9
+ * shader reads from an incomplete texture is not something to rely on.
10
+ */
11
+ export declare const emptyMaskTexture: (device: GraphicsDevice) => Texture;
12
+ export interface ProgressPieMaterialOptions {
13
+ uniqueName: string;
14
+ /** Static artwork baked once: red masks a disc under the sweep, green a glyph over it. */
15
+ maskMap: Texture;
16
+ discColor?: [number, number, number];
17
+ discAlpha?: number;
18
+ }
19
+ /**
20
+ * Material for a progress pie. Everything that animates rides on the `uProgress` uniform, so a
21
+ * running sweep costs one uniform write per frame and no texture work at all.
22
+ */
23
+ export declare const createProgressPieMaterial: ({ uniqueName, maskMap, discColor, discAlpha, }: ProgressPieMaterialOptions) => ShaderMaterial;
24
+ //# sourceMappingURL=xr-progress-pie-material.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xr-progress-pie-material.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/xr-progress-pie-material.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,cAAc,EACd,IAAI,EAIJ,cAAc,EACd,OAAO,EACR,MAAM,YAAY,CAAC;AAoGpB,uFAAuF;AACvF,eAAO,MAAM,mBAAmB,GAAI,QAAQ,cAAc,EAAE,YAAY,MAAM,KAAG,IAUhF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,cAAc,KAAG,OAiBzD,CAAC;AAEF,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,yBAAyB,GAAI,gDAKvC,0BAA0B,KAAG,cA0B/B,CAAC"}