1
+
2
+ import { CanvasTexture , Mesh , Object3D , Shape , ShapeGeometry , LinearFilter , MeshBasicMaterial , PlaneGeometry , XRTargetRaySpace } from 'three' ;
3
+ import { Damper } from './Damper.js' ;
4
+ import { ModelScene } from './ModelScene.js' ;
5
+
6
+ const MAX_OPACITY = 0.6 ;
7
+ const PANEL_WIDTH = 0.1 ;
8
+ const PANEL_HEIGHT = 0.1 ;
9
+ const PANEL_CORNER_RADIUS = 0.02 ;
10
+
11
+ export class XRMenuPanel extends Object3D {
12
+ private panelMesh : Mesh ;
13
+ private exitButton : Mesh ;
14
+ private goalOpacity : number ;
15
+ private opacityDamper : Damper ;
16
+
17
+ constructor ( ) {
18
+ super ( ) ;
19
+
20
+ const panelShape = new Shape ( ) ;
21
+ const w = PANEL_WIDTH , h = PANEL_HEIGHT , r = PANEL_CORNER_RADIUS ;
22
+ // straight horizontal bottom edge and a rounded bottom-right corner with a radius of r
23
+ panelShape . moveTo ( - w / 2 + r , - h / 2 ) ;
24
+ panelShape . lineTo ( w / 2 - r , - h / 2 ) ;
25
+ panelShape . quadraticCurveTo ( w / 2 , - h / 2 , w / 2 , - h / 2 + r ) ;
26
+ // the right most line and the rounded up-right
27
+ panelShape . lineTo ( w / 2 , h / 2 - r ) ;
28
+ panelShape . quadraticCurveTo ( w / 2 , h / 2 , w / 2 - r , h / 2 ) ;
29
+ // the horizontal top edge and rounded up-left
30
+ panelShape . lineTo ( - w / 2 + r , h / 2 ) ;
31
+ panelShape . quadraticCurveTo ( - w / 2 , h / 2 , - w / 2 , h / 2 - r ) ;
32
+ // the left line and bottom left corner
33
+ panelShape . lineTo ( - w / 2 , - h / 2 + r ) ;
34
+ panelShape . quadraticCurveTo ( - w / 2 , - h / 2 , - w / 2 + r , - h / 2 ) ;
35
+
36
+ const geometry = new ShapeGeometry ( panelShape ) ;
37
+ const material = new MeshBasicMaterial ( {
38
+ color : 0x000000 ,
39
+ opacity : MAX_OPACITY ,
40
+ transparent : true
41
+ } ) ;
42
+
43
+ this . panelMesh = new Mesh ( geometry , material ) ;
44
+ this . panelMesh . name = 'MenuPanel' ;
45
+ this . add ( this . panelMesh ) ;
46
+
47
+
48
+ this . exitButton = this . createButton ( 'x' ) ;
49
+ this . exitButton . name = 'ExitButton' ;
50
+ this . exitButton . position . set ( 0 , 0 , 0.01 ) ;
51
+ this . add ( this . exitButton ) ;
52
+
53
+ this . opacityDamper = new Damper ( ) ;
54
+ this . goalOpacity = MAX_OPACITY ;
55
+ }
56
+
57
+ createButton ( label : string , options ?: {
58
+ width ?: number ;
59
+ height ?: number ;
60
+ fontSize ?: number ;
61
+ textColor ?: string ;
62
+ backgroundColor ?: string ;
63
+ fontFamily ?: string ;
64
+ } ) : Mesh {
65
+ const {
66
+ width = 0.05 ,
67
+ height = 0.05 ,
68
+ fontSize = 80 ,
69
+ textColor = 'white' ,
70
+ backgroundColor = 'transparent' ,
71
+ fontFamily = 'sans-serif'
72
+ } = options || { } ;
73
+
74
+ const canvasSize = 128 ;
75
+ const canvas = document . createElement ( 'canvas' ) ;
76
+ canvas . width = canvasSize ;
77
+ canvas . height = canvasSize ;
78
+ const ctx = canvas . getContext ( '2d' ) ! ;
79
+
80
+ // Background
81
+ if ( backgroundColor !== 'transparent' ) {
82
+ ctx . fillStyle = backgroundColor ;
83
+ ctx . fillRect ( 0 , 0 , canvasSize , canvasSize ) ;
84
+ }
85
+
86
+ // Text
87
+ ctx . fillStyle = textColor ;
88
+ ctx . font = `bold ${ fontSize } px ${ fontFamily } ` ;
89
+ ctx . textAlign = 'center' ;
90
+ ctx . textBaseline = 'middle' ;
91
+ ctx . fillText ( label , canvasSize / 2 , canvasSize / 2 ) ;
92
+
93
+ const texture = new CanvasTexture ( canvas ) ;
94
+ texture . needsUpdate = true ;
95
+ texture . minFilter = LinearFilter ;
96
+
97
+ const material = new MeshBasicMaterial ( { map : texture , transparent : true } ) ;
98
+ const geometry = new PlaneGeometry ( width , height ) ;
99
+ return new Mesh ( geometry , material ) ;
100
+ }
101
+
102
+
103
+ exitButtonControllerIntersection ( scene : ModelScene , controller : XRTargetRaySpace ) {
104
+ const hitResult = scene . hitFromController ( controller , this . exitButton ) ;
105
+ return hitResult ;
106
+ }
107
+
108
+ /**
109
+ * Set the box's visibility; it will fade in and out.
110
+ */
111
+ set show ( visible : boolean ) {
112
+ this . goalOpacity = visible ? MAX_OPACITY : 0 ;
113
+ }
114
+
115
+ /**
116
+ * Call on each frame with the frame delta to fade the box.
117
+ */
118
+ updateOpacity ( delta : number ) {
119
+ const material = this . panelMesh . material as MeshBasicMaterial ;
120
+ const currentOpacity = material . opacity ;
121
+ const newOpacity = this . opacityDamper . update ( currentOpacity , this . goalOpacity , delta , 1 ) ;
122
+ this . traverse ( ( child ) => {
123
+ if ( child instanceof Mesh ) {
124
+ const mat = child . material as MeshBasicMaterial ;
125
+ if ( mat . transparent ) mat . opacity = newOpacity ;
126
+ }
127
+ } ) ;
128
+ this . visible = newOpacity > 0 ;
129
+ }
130
+
131
+ dispose ( ) {
132
+ this . children . forEach ( child => {
133
+ if ( child instanceof Mesh ) {
134
+ // Dispose geometry first
135
+ if ( child . geometry ) {
136
+ child . geometry . dispose ( ) ;
137
+ }
138
+
139
+ // Handle material(s)
140
+ // Material can be a single Material or an array of Materials
141
+ const materials = Array . isArray ( child . material ) ? child . material : [ child . material ] ;
142
+
143
+ materials . forEach ( material => {
144
+ if ( material ) { // Ensure material exists before proceeding
145
+ // Dispose texture if it exists and is a CanvasTexture
146
+ // We specifically created CanvasTextures for buttons, so check for that type.
147
+ if ( 'map' in material && material . map instanceof CanvasTexture ) { // Check if 'map' property exists and is a CanvasTexture
148
+ material . map . dispose ( ) ;
149
+ }
150
+ // Dispose material itself
151
+ material . dispose ( ) ;
152
+ }
153
+ } ) ;
154
+ }
155
+ } ) ;
156
+
157
+ // Remove the panel itself from its parent in the scene graph
158
+ this . parent ?. remove ( this ) ;
159
+ }
160
+ }
161
+
0 commit comments