Move engineStreamMachine as a global actor; tons of more work
This commit is contained in:
34
src/App.tsx
34
src/App.tsx
@ -41,7 +41,6 @@ import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||
maybeWriteToDisk()
|
||||
.then(() => {})
|
||||
.catch(() => {})
|
||||
import EngineStreamContext from 'hooks/useEngineStreamContext'
|
||||
import { EngineStream } from 'components/EngineStream'
|
||||
|
||||
export function App() {
|
||||
@ -67,8 +66,6 @@ export function App() {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Stream related refs and data
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
let [searchParams] = useSearchParams()
|
||||
const pool = searchParams.get('pool')
|
||||
|
||||
@ -86,7 +83,7 @@ export function App() {
|
||||
useHotKeyListener()
|
||||
|
||||
const settings = useSettings()
|
||||
const token = useToken()
|
||||
const authToken = useToken()
|
||||
|
||||
const coreDumpManager = useMemo(
|
||||
() =>
|
||||
@ -94,7 +91,7 @@ export function App() {
|
||||
engineCommandManager,
|
||||
codeManager,
|
||||
rustContext,
|
||||
token
|
||||
authToken
|
||||
),
|
||||
[]
|
||||
)
|
||||
@ -157,26 +154,13 @@ export function App() {
|
||||
/>
|
||||
<ModalContainer />
|
||||
<ModelingSidebar paneOpacity={paneOpacity} />
|
||||
<EngineStreamContext.Provider
|
||||
options={{
|
||||
input: {
|
||||
videoRef,
|
||||
canvasRef,
|
||||
mediaStream: null,
|
||||
authToken: token,
|
||||
pool,
|
||||
zoomToFit: true,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<EngineStream />
|
||||
{/* <CamToggle /> */}
|
||||
<LowerRightControls coreDumpManager={coreDumpManager}>
|
||||
<UnitsMenu />
|
||||
<Gizmo />
|
||||
<CameraProjectionToggle />
|
||||
</LowerRightControls>
|
||||
</EngineStreamContext.Provider>
|
||||
<EngineStream pool={pool} authToken={authToken} />
|
||||
{/* <CamToggle /> */}
|
||||
<LowerRightControls coreDumpManager={coreDumpManager}>
|
||||
<UnitsMenu />
|
||||
<Gizmo />
|
||||
<CameraProjectionToggle />
|
||||
</LowerRightControls>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -480,12 +480,13 @@ export class CameraControls {
|
||||
if (this.syncDirection === 'engineToClient') {
|
||||
const newCmdId = uuidv4()
|
||||
|
||||
const { videoRef } = engineStreamActor.getSnapshot().context
|
||||
// Nonsense to do anything until the video stream is established.
|
||||
if (!this.engineCommandManager.elVideo) return
|
||||
if (!videoRef.current) return
|
||||
|
||||
const { x, y } = getNormalisedCoordinates(
|
||||
event,
|
||||
this.engineCommandManager.elVideo,
|
||||
videoRef.current,
|
||||
this.engineCommandManager.streamDimensions
|
||||
)
|
||||
this.throttledEngCmd({
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { MouseEventHandler, useEffect, useRef } from 'react'
|
||||
import { useAppState } from 'AppState'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
@ -14,31 +13,34 @@ import { PATHS } from 'lib/paths'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { err, reportRejection, trap } from 'lib/trap'
|
||||
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||
import { clearSceneAndBustCache } from 'lang/wasm'
|
||||
import { ViewControlContextMenu } from './ViewControlMenu'
|
||||
import { useSettings, engineStreamActor } from 'machines/appMachine'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
import { EngineStreamState, EngineStreamTransition } from 'machines/engineStreamMachine'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import useEngineStreamContext, {
|
||||
EngineStreamState,
|
||||
EngineStreamTransition,
|
||||
} from 'hooks/useEngineStreamContext'
|
||||
import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from 'lib/timings'
|
||||
|
||||
export const EngineStream = () => {
|
||||
export const EngineStream = (props: {
|
||||
pool: string | null,
|
||||
authToken: string | undefined,
|
||||
}) => {
|
||||
const { setAppState } = useAppState()
|
||||
|
||||
const { overallState } = useNetworkContext()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const settings = useSettings()
|
||||
|
||||
const engineStreamState = useSelector(engineStreamActor, (state) => state)
|
||||
|
||||
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
const last = useRef<number>(Date.now())
|
||||
const videoWrapperRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
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,
|
||||
cameraProjection: settings.context.modeling.cameraProjection.current,
|
||||
theme: settings.app.theme.current,
|
||||
enableSSAO: settings.modeling.enableSSAO.current,
|
||||
highlightEdges: settings.modeling.highlightEdges.current,
|
||||
showScaleGrid: settings.modeling.showScaleGrid.current,
|
||||
cameraProjection: settings.modeling.cameraProjection.current,
|
||||
}
|
||||
|
||||
const { state: modelingMachineState, send: modelingMachineActorSend } =
|
||||
@ -46,10 +48,7 @@ export const EngineStream = () => {
|
||||
|
||||
const commandBarState = useCommandBarState()
|
||||
|
||||
const engineStreamActor = useEngineStreamContext.useActorRef()
|
||||
const engineStreamState = engineStreamActor.getSnapshot()
|
||||
|
||||
const streamIdleMode = settings.context.app.streamIdleMode.current
|
||||
const streamIdleMode = settings.app.streamIdleMode.current
|
||||
|
||||
const startOrReconfigureEngine = () => {
|
||||
engineStreamActor.send({
|
||||
@ -80,6 +79,15 @@ export const EngineStream = () => {
|
||||
play
|
||||
)
|
||||
|
||||
engineStreamActor.send({
|
||||
type: EngineStreamTransition.SetPool,
|
||||
data: { pool: props.pool },
|
||||
})
|
||||
engineStreamActor.send({
|
||||
type: EngineStreamTransition.SetAuthToken,
|
||||
data: { authToken: props.authToken },
|
||||
})
|
||||
|
||||
return () => {
|
||||
engineCommandManager.tearDown()
|
||||
engineCommandManager.removeEventListener(
|
||||
@ -325,7 +333,7 @@ export const EngineStream = () => {
|
||||
No canvas support
|
||||
</canvas>
|
||||
<ClientSideScene
|
||||
cameraControls={settings.context.modeling.mouseControls.current}
|
||||
cameraControls={settings.modeling.mouseControls.current}
|
||||
/>
|
||||
<ViewControlContextMenu
|
||||
event="mouseup"
|
||||
|
@ -6,8 +6,7 @@ export const ModelStateIndicator = () => {
|
||||
const [commands] = useEngineCommands()
|
||||
const [isDone, setIsDone] = useState<boolean>(false)
|
||||
|
||||
const engineStreamActor = useEngineStreamContext.useActorRef()
|
||||
const engineStreamState = engineStreamActor.getSnapshot()
|
||||
const engineStreamState = useSelector(engineStreamActor, (state) => state)
|
||||
|
||||
const lastCommandType = commands[commands.length - 1]?.type
|
||||
|
||||
|
@ -147,7 +147,6 @@ export const ModelingMachineProvider = ({
|
||||
showScaleGrid,
|
||||
cameraOrbit,
|
||||
enableSSAO,
|
||||
cameraProjection,
|
||||
},
|
||||
} = useSettings()
|
||||
const navigate = useNavigate()
|
||||
|
@ -1,414 +0,0 @@
|
||||
import { useAppStream } from '@src/AppState'
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useRouteLoaderData } from 'react-router-dom'
|
||||
|
||||
import { ClientSideScene } from '@src/clientSideScene/ClientSideSceneComp'
|
||||
import Loading from '@src/components/Loading'
|
||||
import { ViewControlContextMenu } from '@src/components/ViewControlMenu'
|
||||
import { useModelingContext } from '@src/hooks/useModelingContext'
|
||||
import { useNetworkContext } from '@src/hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
|
||||
import { getArtifactOfTypes } from '@src/lang/std/artifactGraph'
|
||||
import {
|
||||
DisconnectingType,
|
||||
EngineCommandManagerEvents,
|
||||
EngineConnectionStateType,
|
||||
} from '@src/lang/std/engineConnection'
|
||||
import { btnName } from '@src/lib/cameraControls'
|
||||
import { PATHS } from '@src/lib/paths'
|
||||
import { sendSelectEventToEngine } from '@src/lib/selections'
|
||||
import {
|
||||
engineCommandManager,
|
||||
kclManager,
|
||||
sceneInfra,
|
||||
} from '@src/lib/singletons'
|
||||
import { err, reportRejection } from '@src/lib/trap'
|
||||
import type { IndexLoaderData } from '@src/lib/types'
|
||||
import { uuidv4 } from '@src/lib/utils'
|
||||
import { useSettings } from '@src/machines/appMachine'
|
||||
import { useCommandBarState } from '@src/machines/commandBarMachine'
|
||||
|
||||
enum StreamState {
|
||||
Playing = 'playing',
|
||||
Paused = 'paused',
|
||||
Resuming = 'resuming',
|
||||
Unset = 'unset',
|
||||
}
|
||||
|
||||
export const Stream = () => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const videoWrapperRef = useRef<HTMLDivElement>(null)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const settings = useSettings()
|
||||
const { state, send } = useModelingContext()
|
||||
const commandBarState = useCommandBarState()
|
||||
const { mediaStream } = useAppStream()
|
||||
const { overallState, immediateState } = useNetworkContext()
|
||||
const [streamState, setStreamState] = useState(StreamState.Unset)
|
||||
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
|
||||
const IDLE = settings.app.streamIdleMode.current
|
||||
|
||||
const isNetworkOkay =
|
||||
overallState === NetworkHealthState.Ok ||
|
||||
overallState === NetworkHealthState.Weak
|
||||
|
||||
engineCommandManager.elVideo = videoRef.current
|
||||
|
||||
/**
|
||||
* Execute code and show a "building scene message"
|
||||
* in Stream.tsx in the meantime.
|
||||
*
|
||||
* I would like for this to live somewhere more central,
|
||||
* but it seems to me that we need the video element ref
|
||||
* to be able to play the video after the code has been
|
||||
* executed. If we can find a way to do this from a more
|
||||
* central place, we can move this code there.
|
||||
*/
|
||||
function executeCodeAndPlayStream() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
kclManager.executeCode().then(async () => {
|
||||
await videoRef.current?.play().catch((e) => {
|
||||
console.warn('Video playing was prevented', e, videoRef.current)
|
||||
})
|
||||
setStreamState(StreamState.Playing)
|
||||
|
||||
// Only call zoom_to_fit once when the stream starts to center the scene.
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'zoom_to_fit',
|
||||
object_ids: [], // leave empty to zoom to all objects
|
||||
padding: 0.1, // padding around the objects
|
||||
animated: false, // don't animate the zoom for now
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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')
|
||||
executeCodeAndPlayStream()
|
||||
}
|
||||
}, [file?.path, engineCommandManager.engineConnection])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
immediateState.type === EngineConnectionStateType.Disconnecting &&
|
||||
immediateState.value.type === DisconnectingType.Pause
|
||||
) {
|
||||
setStreamState(StreamState.Paused)
|
||||
}
|
||||
}, [immediateState])
|
||||
|
||||
// Linux has a default behavior to paste text on middle mouse up
|
||||
// This adds a listener to block that pasting if the click target
|
||||
// is not a text input, so users can move in the 3D scene with
|
||||
// middle mouse drag with a text input focused without pasting.
|
||||
useEffect(() => {
|
||||
const handlePaste = (e: ClipboardEvent) => {
|
||||
const isHtmlElement = e.target && e.target instanceof HTMLElement
|
||||
const isEditable =
|
||||
(isHtmlElement && !('explicitOriginalTarget' in e)) ||
|
||||
('explicitOriginalTarget' in e &&
|
||||
((e.explicitOriginalTarget as HTMLElement).contentEditable ===
|
||||
'true' ||
|
||||
['INPUT', 'TEXTAREA'].some(
|
||||
(tagName) =>
|
||||
tagName === (e.explicitOriginalTarget as HTMLElement).tagName
|
||||
)))
|
||||
if (!isEditable) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.stopImmediatePropagation()
|
||||
}
|
||||
}
|
||||
|
||||
globalThis?.window?.document?.addEventListener('paste', handlePaste, {
|
||||
capture: true,
|
||||
})
|
||||
|
||||
const IDLE_TIME_MS = 1000 * 60 * 2
|
||||
let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined
|
||||
|
||||
const teardown = () => {
|
||||
// Already paused
|
||||
if (streamState === StreamState.Paused) return
|
||||
|
||||
videoRef.current?.pause()
|
||||
setStreamState(StreamState.Paused)
|
||||
sceneInfra.modelingSend({ type: 'Cancel' })
|
||||
// Give video time to pause
|
||||
window.requestAnimationFrame(() => {
|
||||
engineCommandManager.tearDown({ idleMode: true })
|
||||
})
|
||||
}
|
||||
|
||||
const onVisibilityChange = () => {
|
||||
if (globalThis.window.document.visibilityState === 'hidden') {
|
||||
clearTimeout(timeoutIdIdleA)
|
||||
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
|
||||
} else if (!engineCommandManager.engineConnection?.isReady()) {
|
||||
clearTimeout(timeoutIdIdleA)
|
||||
setStreamState(StreamState.Resuming)
|
||||
}
|
||||
}
|
||||
|
||||
// Teardown everything if we go hidden or reconnect
|
||||
if (IDLE) {
|
||||
globalThis?.window?.document?.addEventListener(
|
||||
'visibilitychange',
|
||||
onVisibilityChange
|
||||
)
|
||||
}
|
||||
|
||||
let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined
|
||||
|
||||
const onAnyInput = () => {
|
||||
if (streamState === StreamState.Playing) {
|
||||
// Clear both timers
|
||||
clearTimeout(timeoutIdIdleA)
|
||||
clearTimeout(timeoutIdIdleB)
|
||||
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
|
||||
}
|
||||
if (streamState === StreamState.Paused) {
|
||||
setStreamState(StreamState.Resuming)
|
||||
}
|
||||
}
|
||||
|
||||
if (IDLE) {
|
||||
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
|
||||
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
|
||||
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
|
||||
globalThis?.window?.document?.addEventListener('scroll', onAnyInput)
|
||||
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
|
||||
}
|
||||
|
||||
if (IDLE) {
|
||||
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener to execute code and play the stream
|
||||
* on initial stream setup.
|
||||
*/
|
||||
engineCommandManager.addEventListener(
|
||||
EngineCommandManagerEvents.SceneReady,
|
||||
executeCodeAndPlayStream
|
||||
)
|
||||
|
||||
return () => {
|
||||
engineCommandManager.removeEventListener(
|
||||
EngineCommandManagerEvents.SceneReady,
|
||||
executeCodeAndPlayStream
|
||||
)
|
||||
globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
|
||||
capture: true,
|
||||
})
|
||||
if (IDLE) {
|
||||
clearTimeout(timeoutIdIdleA)
|
||||
clearTimeout(timeoutIdIdleB)
|
||||
|
||||
globalThis?.window?.document?.removeEventListener(
|
||||
'visibilitychange',
|
||||
onVisibilityChange
|
||||
)
|
||||
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
|
||||
globalThis?.window?.document?.removeEventListener(
|
||||
'mousemove',
|
||||
onAnyInput
|
||||
)
|
||||
globalThis?.window?.document?.removeEventListener(
|
||||
'mousedown',
|
||||
onAnyInput
|
||||
)
|
||||
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput)
|
||||
globalThis?.window?.document?.removeEventListener(
|
||||
'touchstart',
|
||||
onAnyInput
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [IDLE, streamState])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
typeof RTCPeerConnection === 'undefined'
|
||||
)
|
||||
return
|
||||
if (!videoRef.current) return
|
||||
if (!mediaStream) return
|
||||
|
||||
// The browser complains if we try to load a new stream without pausing first.
|
||||
// Do not immediately play the stream!
|
||||
// we instead use a setTimeout to play the stream in the next event loop
|
||||
try {
|
||||
videoRef.current.srcObject = mediaStream
|
||||
videoRef.current.pause()
|
||||
setTimeout(() => {
|
||||
videoRef.current?.play().catch((e) => {
|
||||
console.warn('Video playing was prevented', e, videoRef.current)
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('Attempted to pause stream while play was still loading', e)
|
||||
}
|
||||
|
||||
send({
|
||||
type: 'Set context',
|
||||
data: {
|
||||
videoElement: videoRef.current,
|
||||
},
|
||||
})
|
||||
|
||||
setIsLoading(false)
|
||||
}, [mediaStream])
|
||||
|
||||
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
// If we've got no stream or connection, don't do anything
|
||||
if (!isNetworkOkay) return
|
||||
if (!videoRef.current) return
|
||||
// If we're in sketch mode, don't send a engine-side select event
|
||||
if (state.matches('Sketch')) return
|
||||
// Only respect default plane selection if we're on a selection command argument
|
||||
if (
|
||||
state.matches({ idle: 'showPlanes' }) &&
|
||||
!(
|
||||
commandBarState.matches('Gathering arguments') &&
|
||||
commandBarState.context.currentArgument?.inputType === 'selection'
|
||||
)
|
||||
)
|
||||
return
|
||||
// If we're mousing up from a camera drag, don't send a select event
|
||||
if (sceneInfra.camControls.wasDragging === true) return
|
||||
|
||||
if (btnName(e.nativeEvent).left) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
sendSelectEventToEngine(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On double-click of sketch entities we automatically enter sketch mode with the selected sketch,
|
||||
* allowing for quick editing of sketches. TODO: This should be moved to a more central place.
|
||||
*/
|
||||
const enterSketchModeIfSelectingSketch: MouseEventHandler<HTMLDivElement> = (
|
||||
e
|
||||
) => {
|
||||
if (
|
||||
!isNetworkOkay ||
|
||||
!videoRef.current ||
|
||||
state.matches('Sketch') ||
|
||||
state.matches({ idle: 'showPlanes' }) ||
|
||||
sceneInfra.camControls.wasDragging === true ||
|
||||
!btnName(e.nativeEvent).left
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
sendSelectEventToEngine(e)
|
||||
.then(({ entity_id }) => {
|
||||
if (!entity_id) {
|
||||
// No entity selected. This is benign
|
||||
return
|
||||
}
|
||||
const path = getArtifactOfTypes(
|
||||
{ key: entity_id, types: ['path', 'solid2d', 'segment', 'helix'] },
|
||||
kclManager.artifactGraph
|
||||
)
|
||||
if (err(path)) {
|
||||
return path
|
||||
}
|
||||
sceneInfra.modelingSend({ type: 'Enter sketch' })
|
||||
})
|
||||
.catch(reportRejection)
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
ref={videoWrapperRef}
|
||||
className="absolute inset-0 z-0"
|
||||
id="stream"
|
||||
data-testid="stream"
|
||||
onClick={handleClick}
|
||||
onDoubleClick={enterSketchModeIfSelectingSketch}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onContextMenuCapture={(e) => e.preventDefault()}
|
||||
>
|
||||
<video
|
||||
ref={videoRef}
|
||||
muted
|
||||
autoPlay
|
||||
controls={false}
|
||||
onPlay={() => setIsLoading(false)}
|
||||
className="w-full cursor-pointer h-full"
|
||||
disablePictureInPicture
|
||||
id="video-stream"
|
||||
/>
|
||||
<ClientSideScene
|
||||
cameraControls={settings.modeling.mouseControls.current}
|
||||
/>
|
||||
{(streamState === StreamState.Paused ||
|
||||
streamState === StreamState.Resuming) && (
|
||||
<div className="text-center absolute inset-0">
|
||||
<div
|
||||
className="flex flex-col items-center justify-center h-screen"
|
||||
data-testid="paused"
|
||||
>
|
||||
<div className="border-primary border p-2 rounded-sm">
|
||||
<svg
|
||||
width="8"
|
||||
height="12"
|
||||
viewBox="0 0 8 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 12V0H0V12H2ZM8 12V0H6V12H8Z"
|
||||
fill="var(--primary)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-base mt-2 text-primary bold">
|
||||
{streamState === StreamState.Paused && 'Paused'}
|
||||
{streamState === StreamState.Resuming && 'Resuming'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(!isNetworkOkay || isLoading) && (
|
||||
<div className="text-center absolute inset-0">
|
||||
<Loading>
|
||||
{!isNetworkOkay && !isLoading ? (
|
||||
<span data-testid="loading-stream">Stream disconnected...</span>
|
||||
) : (
|
||||
!isLoading && (
|
||||
<span data-testid="loading-stream">Loading stream...</span>
|
||||
)
|
||||
)}
|
||||
</Loading>
|
||||
</div>
|
||||
)}
|
||||
<ViewControlContextMenu
|
||||
event="mouseup"
|
||||
guard={(e) =>
|
||||
sceneInfra.camControls.wasDragging === false &&
|
||||
btnName(e).right === true
|
||||
}
|
||||
menuTargetElement={videoWrapperRef}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -841,7 +841,9 @@ class EngineConnection extends EventTarget {
|
||||
|
||||
// Bust the cache before anything
|
||||
;(async () => {
|
||||
await clearSceneAndBustCache(kclManager.engineCommandManager)
|
||||
await rustContext.clearSceneAndBustCache(
|
||||
kclManager.engineCommandManager.settings
|
||||
)
|
||||
})().catch(reportRejection)
|
||||
|
||||
this.dispatchEvent(
|
||||
@ -1395,8 +1397,6 @@ export class EngineCommandManager extends EventTarget {
|
||||
height: 1337,
|
||||
}
|
||||
|
||||
elVideo: HTMLVideoElement | null = null
|
||||
|
||||
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
||||
|
||||
subscriptions: {
|
||||
|
@ -649,12 +649,13 @@ export async function sendSelectEventToEngine(
|
||||
e: React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
) {
|
||||
// No video stream to normalise against, return immediately
|
||||
if (!engineCommandManager.elVideo)
|
||||
const engineStreamState = engineStreamActor.getSnapshot().context
|
||||
if (!engineStreamState.videoRef.current)
|
||||
return Promise.reject('video element not ready')
|
||||
|
||||
const { x, y } = getNormalisedCoordinates(
|
||||
e,
|
||||
engineCommandManager.elVideo,
|
||||
engineStreamState.videoRef.current,
|
||||
engineCommandManager.streamDimensions
|
||||
)
|
||||
const res = await engineCommandManager.sendSceneCommand({
|
||||
|
@ -6,10 +6,11 @@ import { authMachine } from '@src/machines/authMachine'
|
||||
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
||||
import { settingsMachine } from '@src/machines/settingsMachine'
|
||||
|
||||
const { AUTH, SETTINGS } = ACTOR_IDS
|
||||
const { AUTH, SETTINGS, ENGINE_STREAM } = ACTOR_IDS
|
||||
const appMachineActors = {
|
||||
[AUTH]: authMachine,
|
||||
[SETTINGS]: settingsMachine,
|
||||
[ENGINE_STREAM]: engineStreamMachine,
|
||||
} as const
|
||||
|
||||
const appMachine = setup({
|
||||
@ -29,6 +30,11 @@ const appMachine = setup({
|
||||
systemId: SETTINGS,
|
||||
input: createSettings(),
|
||||
}),
|
||||
spawnChild(ENGINE_STREAM, {
|
||||
id: ENGINE_STREAM,
|
||||
systemId: ENGINE_STREAM,
|
||||
input: engineStreamContextCreate(),
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
@ -61,3 +67,7 @@ export const useSettings = () =>
|
||||
const { currentProject, ...settings } = state.context
|
||||
return settings
|
||||
})
|
||||
|
||||
export const engineStreamActor = appActor.system.get(ENGINE_STREAM) as ActorRefFrom<
|
||||
typeof engineStreamMachine
|
||||
>
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { makeDefaultPlanes, clearSceneAndBustCache } from 'lang/wasm'
|
||||
import { MutableRefObject } from 'react'
|
||||
import { setup, assign, fromPromise } from 'xstate'
|
||||
import { createActorContext } from '@xstate/react'
|
||||
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
|
||||
import { rustContext, kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
|
||||
import { trap } from 'lib/trap'
|
||||
import { Vector3, Vector4 } from 'three'
|
||||
|
||||
@ -19,7 +17,9 @@ export enum EngineStreamState {
|
||||
}
|
||||
|
||||
export enum EngineStreamTransition {
|
||||
SetMediaStream = 'set-context',
|
||||
SetMediaStream = 'set-media-stream',
|
||||
SetPool = 'set-pool',
|
||||
SetAuthToken = 'set-auth-token',
|
||||
Play = 'play',
|
||||
Resume = 'resume',
|
||||
Pause = 'pause',
|
||||
@ -35,6 +35,15 @@ export interface EngineStreamContext {
|
||||
zoomToFit: boolean
|
||||
}
|
||||
|
||||
export const engineStreamContextCreate = (): EngineStreamContext => ({
|
||||
pool: null,
|
||||
authToken: undefined,
|
||||
mediaStream: null,
|
||||
videoRef: { current: null },
|
||||
canvasRef: { current: null },
|
||||
zoomToFit: true,
|
||||
})
|
||||
|
||||
export function getDimensions(streamWidth: number, streamHeight: number) {
|
||||
const factorOf = 4
|
||||
const maxResolution = 2160
|
||||
@ -47,11 +56,11 @@ export function getDimensions(streamWidth: number, streamHeight: number) {
|
||||
return { width: quadWidth, height: quadHeight }
|
||||
}
|
||||
|
||||
export function holdOntoVideoFrameInCanvas(
|
||||
export async function holdOntoVideoFrameInCanvas(
|
||||
video: HTMLVideoElement,
|
||||
canvas: HTMLCanvasElement
|
||||
) {
|
||||
video.pause()
|
||||
await video.pause()
|
||||
canvas.width = video.videoWidth
|
||||
canvas.height = video.videoHeight
|
||||
canvas.style.width = video.videoWidth + 'px'
|
||||
@ -64,7 +73,7 @@ export function holdOntoVideoFrameInCanvas(
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
|
||||
const engineStreamMachine = setup({
|
||||
export const engineStreamMachine = setup({
|
||||
types: {
|
||||
context: {} as EngineStreamContext,
|
||||
input: {} as EngineStreamContext,
|
||||
@ -88,7 +97,7 @@ const engineStreamMachine = setup({
|
||||
video.style.display = 'block'
|
||||
canvas.style.display = 'none'
|
||||
|
||||
await sceneInfra.camControls.restoreCameraPosition()
|
||||
// await sceneInfra.camControls.restoreCameraPosition()
|
||||
|
||||
video.srcObject = mediaStream
|
||||
await video.play()
|
||||
@ -105,12 +114,12 @@ const engineStreamMachine = setup({
|
||||
const video = context.videoRef.current
|
||||
if (!video) return
|
||||
|
||||
video.pause()
|
||||
await video.pause()
|
||||
|
||||
const canvas = context.canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
holdOntoVideoFrameInCanvas(video, canvas)
|
||||
await holdOntoVideoFrameInCanvas(video, canvas)
|
||||
|
||||
// Make sure we're on the next frame for no flickering between canvas
|
||||
// and the video elements.
|
||||
@ -126,57 +135,6 @@ const engineStreamMachine = setup({
|
||||
context.mediaStream?.getVideoTracks()[0].stop()
|
||||
video.srcObject = null
|
||||
|
||||
const reqDefaultCameraGetSettings =
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
// CameraControls subscribes to default_camera_get_settings response events
|
||||
// firing this at connection ensure the camera's are synced initially
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
})
|
||||
|
||||
if (
|
||||
reqDefaultCameraGetSettings === null ||
|
||||
!reqDefaultCameraGetSettings.success
|
||||
)
|
||||
return
|
||||
if (
|
||||
!('modeling_response' in reqDefaultCameraGetSettings.resp.data)
|
||||
)
|
||||
return
|
||||
if (
|
||||
reqDefaultCameraGetSettings.resp.data.modeling_response.type ===
|
||||
'empty'
|
||||
)
|
||||
return
|
||||
if (
|
||||
!(
|
||||
'settings' in
|
||||
reqDefaultCameraGetSettings.resp.data.modeling_response.data
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
const center =
|
||||
reqDefaultCameraGetSettings.resp.data.modeling_response.data
|
||||
.settings.center
|
||||
const pos =
|
||||
reqDefaultCameraGetSettings.resp.data.modeling_response.data
|
||||
.settings.pos
|
||||
|
||||
sceneInfra.camControls.old = {
|
||||
camera: sceneInfra.camControls.camera.clone(),
|
||||
target: new Vector3(center.x, center.y, center.z),
|
||||
}
|
||||
|
||||
sceneInfra.camControls.old.camera.position.set(
|
||||
pos.x,
|
||||
pos.y,
|
||||
pos.z
|
||||
)
|
||||
|
||||
engineCommandManager.tearDown({ idleMode: true })
|
||||
})()
|
||||
)
|
||||
@ -215,7 +173,7 @@ const engineStreamMachine = setup({
|
||||
|
||||
// If we don't pause there could be a really bad flicker
|
||||
// on reconfiguration (resize, for example)
|
||||
holdOntoVideoFrameInCanvas(video, canvas)
|
||||
await holdOntoVideoFrameInCanvas(video, canvas)
|
||||
canvas.style.display = 'block'
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
@ -231,9 +189,6 @@ const engineStreamMachine = setup({
|
||||
height,
|
||||
token: context.authToken,
|
||||
settings: settingsNext,
|
||||
makeDefaultPlanes: () => {
|
||||
return makeDefaultPlanes(kclManager.engineCommandManager)
|
||||
},
|
||||
})
|
||||
|
||||
event.modelingMachineActorSend({
|
||||
@ -250,11 +205,24 @@ const engineStreamMachine = setup({
|
||||
),
|
||||
},
|
||||
}).createMachine({
|
||||
context: (initial) => initial.input,
|
||||
initial: EngineStreamState.Off,
|
||||
context: (initial) => initial.input,
|
||||
states: {
|
||||
[EngineStreamState.Off]: {
|
||||
reenter: true,
|
||||
on: {
|
||||
[EngineStreamTransition.SetPool]: {
|
||||
target: EngineStreamState.Setup,
|
||||
actions: [
|
||||
assign({ pool: ({ context, event }) => event.data.pool }),
|
||||
],
|
||||
},
|
||||
[EngineStreamTransition.SetAuthToken]: {
|
||||
target: EngineStreamState.Setup,
|
||||
actions: [
|
||||
assign({ authToken: ({ context, event }) => event.data.authToken }),
|
||||
],
|
||||
},
|
||||
[EngineStreamTransition.StartOrReconfigureEngine]: {
|
||||
target: EngineStreamState.On,
|
||||
},
|
||||
@ -267,6 +235,7 @@ const engineStreamMachine = setup({
|
||||
input: (args) => args,
|
||||
},
|
||||
on: {
|
||||
// Transition requested by engineConnection
|
||||
[EngineStreamTransition.SetMediaStream]: {
|
||||
target: EngineStreamState.On,
|
||||
actions: [
|
||||
@ -335,5 +304,3 @@ const engineStreamMachine = setup({
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default createActorContext(engineStreamMachine)
|
@ -1,4 +1,5 @@
|
||||
export const ACTOR_IDS = {
|
||||
AUTH: 'auth',
|
||||
SETTINGS: 'settings',
|
||||
ENGINE_STREAM: 'engine_stream',
|
||||
} as const
|
||||
|
Reference in New Issue
Block a user