11import { useHookstate , useImmediateEffect , useMutableState } from '@ir-engine/hyperflux'
22import { endXRSession , requestXRSession } from '@ir-engine/spatial/src/xr/XRSessionFunctions'
33import Button from '@ir-engine/ui/src/primitives/tailwind/Button'
4- import React from 'react'
4+ import React , { useEffect , useRef , useState } from 'react'
55
66import EmulatedDevice from './js/emulatedDevice'
77import { EmulatorSettings , emulatorStates } from './js/emulatorStates'
@@ -15,15 +15,8 @@ import { WebXREventDispatcher } from '@ir-engine/spatial/tests/webxr/emulator/We
1515import { POLYFILL_ACTIONS } from '@ir-engine/spatial/tests/webxr/emulator/actions'
1616
1717export async function overrideXR ( args : { mode : 'immersive-vr' | 'immersive-ar' } ) {
18- // inject the webxr polyfill from the webxr emulator source - this is a script added by the bot
19- // globalThis.WebXRPolyfillInjection()
20-
2118 const { CustomWebXRPolyfill } = await import ( '@ir-engine/spatial/tests/webxr/emulator/CustomWebXRPolyfill' )
2219 new CustomWebXRPolyfill ( )
23- // override session supported request, it hangs indefinitely for some reason
24- ; ( navigator as any ) . xr . isSessionSupported = ( ) => {
25- return true
26- }
2720
2821 const deviceDefinition = {
2922 id : 'Oculus Quest' ,
@@ -74,7 +67,6 @@ const setup = async (mode: 'immersive-vr' | 'immersive-ar') => {
7467
7568 return device
7669}
77-
7870export const EmulatorDevtools = ( props : { mode : 'immersive-vr' | 'immersive-ar' } ) => {
7971 const xrState = useMutableState ( XRState )
8072 const xrActive = xrState . sessionActive . value && ! xrState . requestingSession . value
@@ -86,6 +78,24 @@ export const EmulatorDevtools = (props: { mode: 'immersive-vr' | 'immersive-ar'
8678 } )
8779 } , [ ] )
8880
81+ // Panel state
82+ const [ panelPosition , setPanelPosition ] = useState ( { x : window . innerWidth - 720 , y : 20 } )
83+ const [ panelSize , setPanelSize ] = useState ( { width : 700 , height : 900 } )
84+ const [ isDragging , setIsDragging ] = useState ( false )
85+ const [ isResizing , setIsResizing ] = useState ( false )
86+ const [ dragOffset , setDragOffset ] = useState ( { x : 0 , y : 0 } )
87+ const [ resizeDirection , setResizeDirection ] = useState ( '' )
88+ const [ isVisible , setIsVisible ] = useState ( false )
89+ const [ isMinimized , setIsMinimized ] = useState ( false )
90+
91+ const panelRef = useRef < HTMLDivElement > ( null )
92+ const resizeHandleRef = useRef < HTMLDivElement > ( null )
93+
94+ // Refs for performance optimization
95+ const dragPosRef = useRef ( panelPosition )
96+ const sizeRef = useRef ( panelSize )
97+ const rafRef = useRef < number | null > ( null )
98+
8999 const toggleXR = async ( ) => {
90100 if ( xrActive ) {
91101 endXRSession ( )
@@ -104,24 +114,192 @@ export const EmulatorDevtools = (props: { mode: 'immersive-vr' | 'immersive-ar'
104114 }
105115 }
106116
117+ const handleClosePanel = ( ) => setIsVisible ( false )
118+
119+ const handleMinimizePanel = ( ) => {
120+ setIsMinimized ( ! isMinimized )
121+ setPanelSize ( ( prev ) => ( {
122+ ...prev ,
123+ height : isMinimized ? 600 : 50
124+ } ) )
125+ }
126+
127+ // Mouse event handlers for dragging
128+ const handleMouseDown = ( e : React . MouseEvent ) => {
129+ const target = e . target as HTMLElement
130+ const isHeader = target . closest ( '.floating-panel-header' )
131+ const isPanel = target === panelRef . current
132+ const isResizeHandle = target . closest ( '.resize-handle' )
133+
134+ if ( isHeader || isPanel ) {
135+ e . preventDefault ( )
136+ setIsDragging ( true )
137+ const rect = panelRef . current ?. getBoundingClientRect ( )
138+ if ( rect ) {
139+ setDragOffset ( {
140+ x : e . clientX - rect . left ,
141+ y : e . clientY - rect . top
142+ } )
143+ }
144+ }
145+ }
146+
147+ const updatePanelPosition = ( x : number , y : number ) => {
148+ dragPosRef . current = { x, y }
149+ if ( rafRef . current === null ) {
150+ rafRef . current = requestAnimationFrame ( ( ) => {
151+ setPanelPosition ( { ...dragPosRef . current } )
152+ rafRef . current = null
153+ } )
154+ }
155+ }
156+
157+ const updatePanelSize = ( width : number , height : number ) => {
158+ sizeRef . current = { width, height }
159+ if ( rafRef . current === null ) {
160+ rafRef . current = requestAnimationFrame ( ( ) => {
161+ setPanelSize ( { ...sizeRef . current } )
162+ rafRef . current = null
163+ } )
164+ }
165+ }
166+
167+ const handleMouseMove = ( e : MouseEvent ) => {
168+ if ( isDragging ) {
169+ e . preventDefault ( )
170+ const newX = e . clientX - dragOffset . x
171+ const newY = e . clientY - dragOffset . y
172+
173+ const maxX = window . innerWidth - panelSize . width
174+ const maxY = window . innerHeight - panelSize . height
175+
176+ updatePanelPosition ( Math . max ( 0 , Math . min ( newX , maxX ) ) , Math . max ( 0 , Math . min ( newY , maxY ) ) )
177+ } else if ( isResizing ) {
178+ e . preventDefault ( )
179+ let width = sizeRef . current . width
180+ let height = sizeRef . current . height
181+
182+ if ( resizeDirection . includes ( 'e' ) ) {
183+ width = e . clientX - panelPosition . x
184+ }
185+ if ( resizeDirection . includes ( 's' ) ) {
186+ height = e . clientY - panelPosition . y
187+ }
188+
189+ width = Math . max ( 300 , Math . min ( width , window . innerWidth - panelPosition . x ) )
190+ height = Math . max ( 400 , Math . min ( height , window . innerHeight - panelPosition . y ) )
191+
192+ updatePanelSize ( width , height )
193+ }
194+ }
195+
196+ const handleMouseUp = ( ) => {
197+ setIsDragging ( false )
198+ setIsResizing ( false )
199+ setResizeDirection ( '' )
200+ if ( rafRef . current !== null ) {
201+ cancelAnimationFrame ( rafRef . current )
202+ rafRef . current = null
203+ }
204+ }
205+
206+ const handleResizeMouseDown = ( direction : string ) => ( e : React . MouseEvent ) => {
207+ e . preventDefault ( )
208+ e . stopPropagation ( )
209+ setIsResizing ( true )
210+ setResizeDirection ( direction )
211+ }
212+
213+ useEffect ( ( ) => {
214+ document . addEventListener ( 'mousemove' , handleMouseMove )
215+ document . addEventListener ( 'mouseup' , handleMouseUp )
216+ return ( ) => {
217+ document . removeEventListener ( 'mousemove' , handleMouseMove )
218+ document . removeEventListener ( 'mouseup' , handleMouseUp )
219+ }
220+ } , [ isDragging , isResizing , dragOffset , panelPosition , panelSize , resizeDirection ] )
221+
222+ if ( ! isVisible ) {
223+ return (
224+ < >
225+ < style type = "text/css" > { devtoolCSS . toString ( ) } </ style >
226+ < button className = "show-panel-btn" onClick = { ( ) => setIsVisible ( true ) } >
227+ Show XR Devtool
228+ </ button >
229+ </ >
230+ )
231+ }
232+
107233 return (
108234 < >
109235 < style type = "text/css" > { devtoolCSS . toString ( ) } </ style >
236+
110237 < div
111- id = "devtools"
112- className = "flex-no-wrap m-0 flex h-full h-full select-none flex-col overflow-hidden overflow-hidden bg-gray-900 text-xs text-gray-900"
238+ ref = { panelRef }
239+ className = { `floating-devtool-panel ${ isMinimized ? 'minimized' : '' } ` }
240+ style = { {
241+ left : panelPosition . x ,
242+ top : panelPosition . y ,
243+ width : panelSize . width ,
244+ height : panelSize . height ,
245+ cursor : isDragging ? 'grabbing' : 'grab'
246+ } }
247+ onMouseDown = { handleMouseDown }
113248 >
114- < div className = "flex-no-wrap z-50 flex h-10 select-none flex-row bg-gray-800 text-xs text-gray-900" >
115- < Button className = "my-1 ml-auto mr-6 px-10" onClick = { toggleXR } disabled = { xrState . requestingSession . value } >
116- { ( xrActive ? 'Exit ' : 'Enter ' ) + ( props . mode === 'immersive-ar' ? 'AR' : 'VR' ) }
117- </ Button >
118- { props . mode === 'immersive-ar' && (
119- < Button className = "my-1 ml-auto mr-6 px-10" onClick = { togglePlacement } disabled = { ! xrActive } >
120- Place Scene
249+ < div className = "floating-panel-header" style = { { cursor : isDragging ? 'grabbing' : 'grab' } } >
250+ < div className = "text-sm font-medium text-white" > XR Devtool Panel</ div >
251+ < div className = "flex gap-2" >
252+ < button
253+ className = "panel-control-btn bg-gray-600 text-white hover:bg-gray-500"
254+ onClick = { handleMinimizePanel }
255+ title = { isMinimized ? 'Maximize' : 'Minimize' }
256+ >
257+ { isMinimized ? '□' : '−' }
258+ </ button >
259+ < Button
260+ className = "bg-gray-600 px-3 py-1 text-xs text-white hover:bg-gray-500"
261+ onClick = { toggleXR }
262+ disabled = { xrState . requestingSession . value }
263+ >
264+ { ( xrActive ? 'Exit ' : 'Enter ' ) + ( props . mode === 'immersive-ar' ? 'AR' : 'VR' ) }
121265 </ Button >
122- ) }
266+ { props . mode === 'immersive-ar' && (
267+ < Button
268+ className = "bg-gray-600 px-3 py-1 text-xs text-white hover:bg-gray-500"
269+ onClick = { togglePlacement }
270+ disabled = { ! xrActive }
271+ >
272+ Place Scene
273+ </ Button >
274+ ) }
275+ < button
276+ className = "panel-control-btn bg-red-600 text-white hover:bg-red-700"
277+ onClick = { handleClosePanel }
278+ title = "Close"
279+ >
280+ ×
281+ </ button >
282+ </ div >
123283 </ div >
124- { deviceState . value && < Devtool device = { deviceState . value } /> }
284+
285+ < div className = "floating-panel-content" style = { { opacity : isMinimized ? 0 : 1 } } >
286+ < div className = "floating-panel-scroll" >
287+ { deviceState . value ? (
288+ < Devtool device = { deviceState . value } />
289+ ) : (
290+ < div className = "flex h-full items-center justify-center text-gray-400" >
291+ < div className = "text-center" >
292+ < div className = "mx-auto mb-2 h-8 w-8 animate-spin rounded-full border-b-2 border-gray-400" > </ div >
293+ < div > Initializing XR Devtool...</ div >
294+ </ div >
295+ </ div >
296+ ) }
297+ </ div >
298+ </ div >
299+
300+ { ! isMinimized && (
301+ < div ref = { resizeHandleRef } className = "resize-handle" onMouseDown = { handleResizeMouseDown ( 'se' ) } />
302+ ) }
125303 </ div >
126304 </ >
127305 )
0 commit comments