Files
modeling-app/src/components/EngineStream.tsx

294 lines
9.6 KiB
TypeScript
Raw Normal View History

2024-09-16 10:18:12 -04:00
import { MouseEventHandler, useEffect, useRef } from 'react'
2024-09-12 16:06:05 -04:00
import { useAppState } from 'AppState'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useModelingContext } from 'hooks/useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
import { btnName } from 'lib/cameraControls'
2024-09-16 10:18:12 -04:00
import { trap } from 'lib/trap'
2024-09-12 16:06:05 -04:00
import { sendSelectEventToEngine } from 'lib/selections'
2024-09-16 10:18:12 -04:00
import { kclManager, engineCommandManager } from 'lib/singletons'
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
2024-09-12 16:06:05 -04:00
import { useRouteLoaderData } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { IndexLoaderData } from 'lib/types'
import useEngineStreamContext, {
EngineStreamState,
EngineStreamTransition,
} from 'hooks/useEngineStreamContext'
2024-09-24 11:31:13 -04:00
import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from 'lib/timings'
2024-09-12 16:06:05 -04:00
export const EngineStream = () => {
const { setAppState } = useAppState()
const { overallState } = useNetworkContext()
2024-09-13 16:45:36 -04:00
const { settings } = useSettingsAuthContext()
2024-09-13 18:33:34 -04:00
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
2024-09-24 11:31:13 -04:00
const last = useRef<number>(Date.now())
2024-09-13 16:45:36 -04:00
const settingsEngine = {
theme: settings.context.app.theme.current,
enableSSAO: settings.context.app.enableSSAO.current,
highlightEdges: settings.context.modeling.highlightEdges.current,
showScaleGrid: settings.context.modeling.showScaleGrid.current,
2024-10-17 16:28:52 -04:00
cameraProjection: settings.context.modeling.cameraProjection.current,
2024-09-13 16:45:36 -04:00
}
2024-09-12 16:06:05 -04:00
const { state: modelingMachineState, send: modelingMachineActorSend } =
useModelingContext()
2024-09-12 16:06:05 -04:00
const engineStreamActor = useEngineStreamContext.useActorRef()
const engineStreamState = engineStreamActor.getSnapshot()
2024-09-13 16:45:36 -04:00
const streamIdleMode = settings.context.app.streamIdleMode.current
2024-09-12 16:06:05 -04:00
const configure = () => {
2024-09-13 18:33:34 -04:00
engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine,
modelingMachineActorSend,
settings: settingsEngine,
setAppState,
// It's possible a reconnect happens as we drag the window :')
onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream,
mediaStream,
2024-09-12 16:06:05 -04:00
})
},
2024-09-13 18:33:34 -04:00
})
}
2024-09-12 16:06:05 -04:00
2024-09-13 18:33:34 -04:00
useEffect(() => {
2024-09-13 16:45:36 -04:00
const play = () => {
engineStreamActor.send({
type: EngineStreamTransition.Play,
})
}
engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady,
play
)
2024-09-12 16:06:05 -04:00
return () => {
2024-09-13 16:45:36 -04:00
engineCommandManager.removeEventListener(
EngineCommandManagerEvents.SceneReady,
play
)
}
}, [])
2024-09-13 18:33:34 -04:00
useEffect(() => {
2024-09-24 11:31:13 -04:00
const video = engineStreamState.context.videoRef?.current
if (!video) return
const canvas = engineStreamState.context.canvasRef?.current
if (!canvas) return
2024-09-24 11:31:13 -04:00
new ResizeObserver(() => {
2024-10-17 16:28:52 -04:00
if (Date.now() - last.current < REASONABLE_TIME_TO_REFRESH_STREAM_SIZE)
return
2024-09-24 11:31:13 -04:00
last.current = Date.now()
2024-09-13 18:33:34 -04:00
if (
Math.abs(video.width - window.innerWidth) > 4 ||
Math.abs(video.height - window.innerHeight) > 4
) {
timeoutStart.current = Date.now()
2024-09-13 18:33:34 -04:00
configure()
}
2024-09-24 11:31:13 -04:00
}).observe(document.body)
}, [engineStreamState.value])
2024-09-13 18:33:34 -04:00
2024-09-13 16:45:36 -04:00
// When the video and canvas element references are set, start the engine.
useEffect(() => {
if (
engineStreamState.context.canvasRef.current &&
engineStreamState.context.videoRef.current
) {
2024-09-13 16:45:36 -04:00
engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine,
modelingMachineActorSend,
settings: settingsEngine,
setAppState,
onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream,
mediaStream,
2024-09-13 16:45:36 -04:00
})
},
2024-09-13 16:45:36 -04:00
})
}
}, [
engineStreamState.context.canvasRef.current,
engineStreamState.context.videoRef.current,
])
2024-09-13 16:45:36 -04:00
2024-09-16 08:14:36 -04:00
// On settings change, reconfigure the engine. When paused this gets really tricky,
// and also requires onMediaStream to be set!
2024-09-13 16:45:36 -04:00
useEffect(() => {
2024-09-16 08:14:36 -04:00
engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine,
modelingMachineActorSend,
settings: settingsEngine,
setAppState,
onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream,
mediaStream,
})
},
2024-09-16 08:14:36 -04:00
})
2024-09-13 16:45:36 -04:00
}, [settings.context])
2024-09-13 18:33:34 -04:00
/**
* Subscribe to execute code when the file changes
* but only if the scene is already ready.
* See onSceneReady for the initial scene setup.
*/
useEffect(() => {
if (engineCommandManager.engineConnection?.isReady() && file?.path) {
console.log('execute on file change')
2024-09-16 10:18:12 -04:00
void kclManager.executeCode(true).catch(trap)
2024-09-13 18:33:34 -04:00
}
}, [file?.path, engineCommandManager.engineConnection])
2024-09-20 09:08:56 -04:00
const IDLE_TIME_MS = Number(streamIdleMode)
2024-09-13 16:45:36 -04:00
// When streamIdleMode is changed, setup or teardown the timeouts
const timeoutStart = useRef<number | null>(null)
2024-09-13 16:45:36 -04:00
useEffect(() => {
timeoutStart.current = streamIdleMode ? Date.now() : null
2024-09-13 16:45:36 -04:00
}, [streamIdleMode])
useEffect(() => {
let frameId: ReturnType<typeof window.requestAnimationFrame> = 0
const frameLoop = () => {
// Do not pause if the user is in the middle of an operation
if (!modelingMachineState.matches('idle')) {
// In fact, stop the timeout, because we don't want to trigger the
// pause when we exit the operation.
timeoutStart.current = null
} else if (timeoutStart.current) {
const elapsed = Date.now() - timeoutStart.current
if (elapsed >= IDLE_TIME_MS) {
timeoutStart.current = null
engineStreamActor.send({ type: EngineStreamTransition.Pause })
}
}
frameId = window.requestAnimationFrame(frameLoop)
}
frameId = window.requestAnimationFrame(frameLoop)
return () => {
window.cancelAnimationFrame(frameId)
}
}, [modelingMachineState])
useEffect(() => {
if (!streamIdleMode) return
2024-09-13 16:45:36 -04:00
const onAnyInput = () => {
// Just in case it happens in the middle of the user turning off
2024-09-13 16:45:36 -04:00
// idle mode.
if (!streamIdleMode) {
timeoutStart.current = null
2024-09-13 16:45:36 -04:00
return
}
if (engineStreamState.value === EngineStreamState.Paused) {
engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine,
modelingMachineActorSend,
settings: settingsEngine,
setAppState,
onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream,
mediaStream,
2024-09-13 16:45:36 -04:00
})
},
2024-09-13 16:45:36 -04:00
})
}
timeoutStart.current = Date.now()
2024-09-13 16:45:36 -04:00
}
// It's possible after a reconnect, the user doesn't move their mouse at
// all, meaning the timer is not reset to run. We need to set it every
// time our effect dependencies change then.
timeoutStart.current = Date.now()
2024-09-13 16:45:36 -04:00
window.document.addEventListener('keydown', onAnyInput)
window.document.addEventListener('keyup', onAnyInput)
2024-09-13 16:45:36 -04:00
window.document.addEventListener('mousemove', onAnyInput)
window.document.addEventListener('mousedown', onAnyInput)
window.document.addEventListener('mouseup', onAnyInput)
2024-09-13 16:45:36 -04:00
window.document.addEventListener('scroll', onAnyInput)
window.document.addEventListener('touchstart', onAnyInput)
window.document.addEventListener('touchstop', onAnyInput)
2024-09-13 16:45:36 -04:00
return () => {
timeoutStart.current = null
2024-09-13 16:45:36 -04:00
window.document.removeEventListener('keydown', onAnyInput)
window.document.removeEventListener('keyup', onAnyInput)
2024-09-13 16:45:36 -04:00
window.document.removeEventListener('mousemove', onAnyInput)
window.document.removeEventListener('mousedown', onAnyInput)
window.document.removeEventListener('mouseup', onAnyInput)
2024-09-13 16:45:36 -04:00
window.document.removeEventListener('scroll', onAnyInput)
window.document.removeEventListener('touchstart', onAnyInput)
window.document.removeEventListener('touchstop', onAnyInput)
2024-09-12 16:06:05 -04:00
}
2024-09-13 16:45:36 -04:00
}, [streamIdleMode, engineStreamState.value])
2024-09-12 16:06:05 -04:00
const isNetworkOkay =
2024-09-12 16:06:05 -04:00
overallState === NetworkHealthState.Ok ||
overallState === NetworkHealthState.Weak
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
if (!isNetworkOkay) return
2024-09-13 16:45:36 -04:00
if (!engineStreamState.context.videoRef.current) return
2024-09-12 16:06:05 -04:00
if (modelingMachineState.matches('Sketch')) return
if (modelingMachineState.matches({ idle: 'showPlanes' })) return
2024-10-02 12:24:12 -04:00
if (btnName(e.nativeEvent).left) {
2024-09-16 10:18:12 -04:00
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sendSelectEventToEngine(e, engineStreamState.context.videoRef.current)
2024-09-12 16:06:05 -04:00
}
}
return (
<div
className="absolute inset-0 z-0"
id="stream"
data-testid="stream"
onMouseUp={handleMouseUp}
onContextMenu={(e) => e.preventDefault()}
onContextMenuCapture={(e) => e.preventDefault()}
>
<video
2024-09-16 16:12:35 -04:00
autoPlay
muted
2024-09-13 16:45:36 -04:00
key={engineStreamActor.id + 'video'}
ref={engineStreamState.context.videoRef}
2024-09-12 16:06:05 -04:00
controls={false}
2024-09-13 16:45:36 -04:00
className="cursor-pointer"
2024-09-12 16:06:05 -04:00
disablePictureInPicture
id="video-stream"
/>
2024-09-13 16:45:36 -04:00
<canvas
key={engineStreamActor.id + 'canvas'}
ref={engineStreamState.context.canvasRef}
className="cursor-pointer"
id="freeze-frame"
>
No canvas support
</canvas>
2024-09-12 16:06:05 -04:00
<ClientSideScene
cameraControls={settings.context.modeling.mouseControls.current}
/>
</div>
)
2024-09-12 16:06:05 -04:00
}