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

285 lines
9.5 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'
2024-09-12 16:06:05 -04:00
import {
EngineCommandManagerEvents,
} from 'lang/std/engineConnection'
import { useRouteLoaderData } from 'react-router-dom'
import { PATHS } from 'lib/paths'
import { IndexLoaderData } from 'lib/types'
2024-09-16 10:18:12 -04:00
import useEngineStreamContext, { EngineStreamState, EngineStreamTransition } from 'hooks/useEngineStreamContext'
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-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-09-12 16:06:05 -04:00
const {
state: modelingMachineState,
send: modelingMachineActorSend,
context: modelingMachineActorContext,
2024-09-12 16:06:05 -04:00
} = useModelingContext()
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
2024-09-13 18:33:34 -04:00
// 0.25s is the average visual reaction time for humans so we'll go a bit more
// so those exception people don't see.
const REASONABLE_TIME_TO_REFRESH_STREAM_SIZE = 100
const configure = () => {
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(() => {
if (!streamIdleMode) return
2024-09-13 18:33:34 -04:00
const s = setInterval(() => {
const video = engineStreamState.context.videoRef?.current
if (!video) return
const canvas = engineStreamState.context.canvasRef?.current
if (!canvas) return
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()
}
}, REASONABLE_TIME_TO_REFRESH_STREAM_SIZE)
return () => {
clearInterval(s)
}
}, [streamIdleMode, 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) {
engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine,
modelingMachineActorSend,
settings: settingsEngine,
setAppState,
onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream,
mediaStream
})
}
})
}
}, [engineStreamState.context.canvasRef.current, engineStreamState.context.videoRef.current])
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-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-13 16:45:36 -04:00
const IDLE_TIME_MS = 1000 * 6
// 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 = undefined
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
// 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
})
}
})
}
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
2024-09-13 16:45:36 -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-09-16 10:18:12 -04:00
if (btnName(e).left) {
// 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>
)
}