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()
|
maybeWriteToDisk()
|
||||||
.then(() => {})
|
.then(() => {})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
import EngineStreamContext from 'hooks/useEngineStreamContext'
|
|
||||||
import { EngineStream } from 'components/EngineStream'
|
import { EngineStream } from 'components/EngineStream'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
@ -67,8 +66,6 @@ export function App() {
|
|||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// Stream related refs and data
|
// Stream related refs and data
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
||||||
let [searchParams] = useSearchParams()
|
let [searchParams] = useSearchParams()
|
||||||
const pool = searchParams.get('pool')
|
const pool = searchParams.get('pool')
|
||||||
|
|
||||||
@ -86,7 +83,7 @@ export function App() {
|
|||||||
useHotKeyListener()
|
useHotKeyListener()
|
||||||
|
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const token = useToken()
|
const authToken = useToken()
|
||||||
|
|
||||||
const coreDumpManager = useMemo(
|
const coreDumpManager = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -94,7 +91,7 @@ export function App() {
|
|||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
codeManager,
|
codeManager,
|
||||||
rustContext,
|
rustContext,
|
||||||
token
|
authToken
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@ -157,26 +154,13 @@ export function App() {
|
|||||||
/>
|
/>
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<ModelingSidebar paneOpacity={paneOpacity} />
|
<ModelingSidebar paneOpacity={paneOpacity} />
|
||||||
<EngineStreamContext.Provider
|
<EngineStream pool={pool} authToken={authToken} />
|
||||||
options={{
|
{/* <CamToggle /> */}
|
||||||
input: {
|
<LowerRightControls coreDumpManager={coreDumpManager}>
|
||||||
videoRef,
|
<UnitsMenu />
|
||||||
canvasRef,
|
<Gizmo />
|
||||||
mediaStream: null,
|
<CameraProjectionToggle />
|
||||||
authToken: token,
|
</LowerRightControls>
|
||||||
pool,
|
|
||||||
zoomToFit: true,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EngineStream />
|
|
||||||
{/* <CamToggle /> */}
|
|
||||||
<LowerRightControls coreDumpManager={coreDumpManager}>
|
|
||||||
<UnitsMenu />
|
|
||||||
<Gizmo />
|
|
||||||
<CameraProjectionToggle />
|
|
||||||
</LowerRightControls>
|
|
||||||
</EngineStreamContext.Provider>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -480,12 +480,13 @@ export class CameraControls {
|
|||||||
if (this.syncDirection === 'engineToClient') {
|
if (this.syncDirection === 'engineToClient') {
|
||||||
const newCmdId = uuidv4()
|
const newCmdId = uuidv4()
|
||||||
|
|
||||||
|
const { videoRef } = engineStreamActor.getSnapshot().context
|
||||||
// Nonsense to do anything until the video stream is established.
|
// Nonsense to do anything until the video stream is established.
|
||||||
if (!this.engineCommandManager.elVideo) return
|
if (!videoRef.current) return
|
||||||
|
|
||||||
const { x, y } = getNormalisedCoordinates(
|
const { x, y } = getNormalisedCoordinates(
|
||||||
event,
|
event,
|
||||||
this.engineCommandManager.elVideo,
|
videoRef.current,
|
||||||
this.engineCommandManager.streamDimensions
|
this.engineCommandManager.streamDimensions
|
||||||
)
|
)
|
||||||
this.throttledEngCmd({
|
this.throttledEngCmd({
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { MouseEventHandler, useEffect, useRef } from 'react'
|
import { MouseEventHandler, useEffect, useRef } from 'react'
|
||||||
import { useAppState } from 'AppState'
|
import { useAppState } from 'AppState'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
@ -14,31 +13,34 @@ import { PATHS } from 'lib/paths'
|
|||||||
import { IndexLoaderData } from 'lib/types'
|
import { IndexLoaderData } from 'lib/types'
|
||||||
import { err, reportRejection, trap } from 'lib/trap'
|
import { err, reportRejection, trap } from 'lib/trap'
|
||||||
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
import { getArtifactOfTypes } from 'lang/std/artifactGraph'
|
||||||
import { clearSceneAndBustCache } from 'lang/wasm'
|
|
||||||
import { ViewControlContextMenu } from './ViewControlMenu'
|
import { ViewControlContextMenu } from './ViewControlMenu'
|
||||||
|
import { useSettings, engineStreamActor } from 'machines/appMachine'
|
||||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||||
|
import { EngineStreamState, EngineStreamTransition } from 'machines/engineStreamMachine'
|
||||||
import { useSelector } from '@xstate/react'
|
import { useSelector } from '@xstate/react'
|
||||||
import useEngineStreamContext, {
|
|
||||||
EngineStreamState,
|
|
||||||
EngineStreamTransition,
|
|
||||||
} from 'hooks/useEngineStreamContext'
|
|
||||||
import { REASONABLE_TIME_TO_REFRESH_STREAM_SIZE } from 'lib/timings'
|
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 { setAppState } = useAppState()
|
||||||
|
|
||||||
const { overallState } = useNetworkContext()
|
const { overallState } = useNetworkContext()
|
||||||
const { settings } = useSettingsAuthContext()
|
const settings = useSettings()
|
||||||
|
|
||||||
|
const engineStreamState = useSelector(engineStreamActor, (state) => state)
|
||||||
|
|
||||||
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
const { file } = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||||
const last = useRef<number>(Date.now())
|
const last = useRef<number>(Date.now())
|
||||||
const videoWrapperRef = useRef<HTMLDivElement>(null)
|
const videoWrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const settingsEngine = {
|
const settingsEngine = {
|
||||||
theme: settings.context.app.theme.current,
|
theme: settings.app.theme.current,
|
||||||
enableSSAO: settings.context.app.enableSSAO.current,
|
enableSSAO: settings.modeling.enableSSAO.current,
|
||||||
highlightEdges: settings.context.modeling.highlightEdges.current,
|
highlightEdges: settings.modeling.highlightEdges.current,
|
||||||
showScaleGrid: settings.context.modeling.showScaleGrid.current,
|
showScaleGrid: settings.modeling.showScaleGrid.current,
|
||||||
cameraProjection: settings.context.modeling.cameraProjection.current,
|
cameraProjection: settings.modeling.cameraProjection.current,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { state: modelingMachineState, send: modelingMachineActorSend } =
|
const { state: modelingMachineState, send: modelingMachineActorSend } =
|
||||||
@ -46,10 +48,7 @@ export const EngineStream = () => {
|
|||||||
|
|
||||||
const commandBarState = useCommandBarState()
|
const commandBarState = useCommandBarState()
|
||||||
|
|
||||||
const engineStreamActor = useEngineStreamContext.useActorRef()
|
const streamIdleMode = settings.app.streamIdleMode.current
|
||||||
const engineStreamState = engineStreamActor.getSnapshot()
|
|
||||||
|
|
||||||
const streamIdleMode = settings.context.app.streamIdleMode.current
|
|
||||||
|
|
||||||
const startOrReconfigureEngine = () => {
|
const startOrReconfigureEngine = () => {
|
||||||
engineStreamActor.send({
|
engineStreamActor.send({
|
||||||
@ -80,6 +79,15 @@ export const EngineStream = () => {
|
|||||||
play
|
play
|
||||||
)
|
)
|
||||||
|
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetPool,
|
||||||
|
data: { pool: props.pool },
|
||||||
|
})
|
||||||
|
engineStreamActor.send({
|
||||||
|
type: EngineStreamTransition.SetAuthToken,
|
||||||
|
data: { authToken: props.authToken },
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
engineCommandManager.tearDown()
|
engineCommandManager.tearDown()
|
||||||
engineCommandManager.removeEventListener(
|
engineCommandManager.removeEventListener(
|
||||||
@ -325,7 +333,7 @@ export const EngineStream = () => {
|
|||||||
No canvas support
|
No canvas support
|
||||||
</canvas>
|
</canvas>
|
||||||
<ClientSideScene
|
<ClientSideScene
|
||||||
cameraControls={settings.context.modeling.mouseControls.current}
|
cameraControls={settings.modeling.mouseControls.current}
|
||||||
/>
|
/>
|
||||||
<ViewControlContextMenu
|
<ViewControlContextMenu
|
||||||
event="mouseup"
|
event="mouseup"
|
||||||
|
@ -6,8 +6,7 @@ export const ModelStateIndicator = () => {
|
|||||||
const [commands] = useEngineCommands()
|
const [commands] = useEngineCommands()
|
||||||
const [isDone, setIsDone] = useState<boolean>(false)
|
const [isDone, setIsDone] = useState<boolean>(false)
|
||||||
|
|
||||||
const engineStreamActor = useEngineStreamContext.useActorRef()
|
const engineStreamState = useSelector(engineStreamActor, (state) => state)
|
||||||
const engineStreamState = engineStreamActor.getSnapshot()
|
|
||||||
|
|
||||||
const lastCommandType = commands[commands.length - 1]?.type
|
const lastCommandType = commands[commands.length - 1]?.type
|
||||||
|
|
||||||
|
@ -147,7 +147,6 @@ export const ModelingMachineProvider = ({
|
|||||||
showScaleGrid,
|
showScaleGrid,
|
||||||
cameraOrbit,
|
cameraOrbit,
|
||||||
enableSSAO,
|
enableSSAO,
|
||||||
cameraProjection,
|
|
||||||
},
|
},
|
||||||
} = useSettings()
|
} = useSettings()
|
||||||
const navigate = useNavigate()
|
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
|
// Bust the cache before anything
|
||||||
;(async () => {
|
;(async () => {
|
||||||
await clearSceneAndBustCache(kclManager.engineCommandManager)
|
await rustContext.clearSceneAndBustCache(
|
||||||
|
kclManager.engineCommandManager.settings
|
||||||
|
)
|
||||||
})().catch(reportRejection)
|
})().catch(reportRejection)
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
@ -1395,8 +1397,6 @@ export class EngineCommandManager extends EventTarget {
|
|||||||
height: 1337,
|
height: 1337,
|
||||||
}
|
}
|
||||||
|
|
||||||
elVideo: HTMLVideoElement | null = null
|
|
||||||
|
|
||||||
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
||||||
|
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
|
@ -649,12 +649,13 @@ export async function sendSelectEventToEngine(
|
|||||||
e: React.MouseEvent<HTMLDivElement, MouseEvent>
|
e: React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||||
) {
|
) {
|
||||||
// No video stream to normalise against, return immediately
|
// 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')
|
return Promise.reject('video element not ready')
|
||||||
|
|
||||||
const { x, y } = getNormalisedCoordinates(
|
const { x, y } = getNormalisedCoordinates(
|
||||||
e,
|
e,
|
||||||
engineCommandManager.elVideo,
|
engineStreamState.videoRef.current,
|
||||||
engineCommandManager.streamDimensions
|
engineCommandManager.streamDimensions
|
||||||
)
|
)
|
||||||
const res = await engineCommandManager.sendSceneCommand({
|
const res = await engineCommandManager.sendSceneCommand({
|
||||||
|
@ -6,10 +6,11 @@ import { authMachine } from '@src/machines/authMachine'
|
|||||||
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
import { ACTOR_IDS } from '@src/machines/machineConstants'
|
||||||
import { settingsMachine } from '@src/machines/settingsMachine'
|
import { settingsMachine } from '@src/machines/settingsMachine'
|
||||||
|
|
||||||
const { AUTH, SETTINGS } = ACTOR_IDS
|
const { AUTH, SETTINGS, ENGINE_STREAM } = ACTOR_IDS
|
||||||
const appMachineActors = {
|
const appMachineActors = {
|
||||||
[AUTH]: authMachine,
|
[AUTH]: authMachine,
|
||||||
[SETTINGS]: settingsMachine,
|
[SETTINGS]: settingsMachine,
|
||||||
|
[ENGINE_STREAM]: engineStreamMachine,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const appMachine = setup({
|
const appMachine = setup({
|
||||||
@ -29,6 +30,11 @@ const appMachine = setup({
|
|||||||
systemId: SETTINGS,
|
systemId: SETTINGS,
|
||||||
input: createSettings(),
|
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
|
const { currentProject, ...settings } = state.context
|
||||||
return settings
|
return settings
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const engineStreamActor = appActor.system.get(ENGINE_STREAM) as ActorRefFrom<
|
||||||
|
typeof engineStreamMachine
|
||||||
|
>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { makeDefaultPlanes, clearSceneAndBustCache } from 'lang/wasm'
|
|
||||||
import { MutableRefObject } from 'react'
|
import { MutableRefObject } from 'react'
|
||||||
import { setup, assign, fromPromise } from 'xstate'
|
import { setup, assign, fromPromise } from 'xstate'
|
||||||
import { createActorContext } from '@xstate/react'
|
import { rustContext, kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
|
||||||
import { kclManager, sceneInfra, engineCommandManager } from 'lib/singletons'
|
|
||||||
import { trap } from 'lib/trap'
|
import { trap } from 'lib/trap'
|
||||||
import { Vector3, Vector4 } from 'three'
|
import { Vector3, Vector4 } from 'three'
|
||||||
|
|
||||||
@ -19,7 +17,9 @@ export enum EngineStreamState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum EngineStreamTransition {
|
export enum EngineStreamTransition {
|
||||||
SetMediaStream = 'set-context',
|
SetMediaStream = 'set-media-stream',
|
||||||
|
SetPool = 'set-pool',
|
||||||
|
SetAuthToken = 'set-auth-token',
|
||||||
Play = 'play',
|
Play = 'play',
|
||||||
Resume = 'resume',
|
Resume = 'resume',
|
||||||
Pause = 'pause',
|
Pause = 'pause',
|
||||||
@ -35,6 +35,15 @@ export interface EngineStreamContext {
|
|||||||
zoomToFit: boolean
|
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) {
|
export function getDimensions(streamWidth: number, streamHeight: number) {
|
||||||
const factorOf = 4
|
const factorOf = 4
|
||||||
const maxResolution = 2160
|
const maxResolution = 2160
|
||||||
@ -47,11 +56,11 @@ export function getDimensions(streamWidth: number, streamHeight: number) {
|
|||||||
return { width: quadWidth, height: quadHeight }
|
return { width: quadWidth, height: quadHeight }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function holdOntoVideoFrameInCanvas(
|
export async function holdOntoVideoFrameInCanvas(
|
||||||
video: HTMLVideoElement,
|
video: HTMLVideoElement,
|
||||||
canvas: HTMLCanvasElement
|
canvas: HTMLCanvasElement
|
||||||
) {
|
) {
|
||||||
video.pause()
|
await video.pause()
|
||||||
canvas.width = video.videoWidth
|
canvas.width = video.videoWidth
|
||||||
canvas.height = video.videoHeight
|
canvas.height = video.videoHeight
|
||||||
canvas.style.width = video.videoWidth + 'px'
|
canvas.style.width = video.videoWidth + 'px'
|
||||||
@ -64,7 +73,7 @@ export function holdOntoVideoFrameInCanvas(
|
|||||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
const engineStreamMachine = setup({
|
export const engineStreamMachine = setup({
|
||||||
types: {
|
types: {
|
||||||
context: {} as EngineStreamContext,
|
context: {} as EngineStreamContext,
|
||||||
input: {} as EngineStreamContext,
|
input: {} as EngineStreamContext,
|
||||||
@ -88,7 +97,7 @@ const engineStreamMachine = setup({
|
|||||||
video.style.display = 'block'
|
video.style.display = 'block'
|
||||||
canvas.style.display = 'none'
|
canvas.style.display = 'none'
|
||||||
|
|
||||||
await sceneInfra.camControls.restoreCameraPosition()
|
// await sceneInfra.camControls.restoreCameraPosition()
|
||||||
|
|
||||||
video.srcObject = mediaStream
|
video.srcObject = mediaStream
|
||||||
await video.play()
|
await video.play()
|
||||||
@ -105,12 +114,12 @@ const engineStreamMachine = setup({
|
|||||||
const video = context.videoRef.current
|
const video = context.videoRef.current
|
||||||
if (!video) return
|
if (!video) return
|
||||||
|
|
||||||
video.pause()
|
await video.pause()
|
||||||
|
|
||||||
const canvas = context.canvasRef.current
|
const canvas = context.canvasRef.current
|
||||||
if (!canvas) return
|
if (!canvas) return
|
||||||
|
|
||||||
holdOntoVideoFrameInCanvas(video, canvas)
|
await holdOntoVideoFrameInCanvas(video, canvas)
|
||||||
|
|
||||||
// Make sure we're on the next frame for no flickering between canvas
|
// Make sure we're on the next frame for no flickering between canvas
|
||||||
// and the video elements.
|
// and the video elements.
|
||||||
@ -126,57 +135,6 @@ const engineStreamMachine = setup({
|
|||||||
context.mediaStream?.getVideoTracks()[0].stop()
|
context.mediaStream?.getVideoTracks()[0].stop()
|
||||||
video.srcObject = null
|
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 })
|
engineCommandManager.tearDown({ idleMode: true })
|
||||||
})()
|
})()
|
||||||
)
|
)
|
||||||
@ -215,7 +173,7 @@ const engineStreamMachine = setup({
|
|||||||
|
|
||||||
// If we don't pause there could be a really bad flicker
|
// If we don't pause there could be a really bad flicker
|
||||||
// on reconfiguration (resize, for example)
|
// on reconfiguration (resize, for example)
|
||||||
holdOntoVideoFrameInCanvas(video, canvas)
|
await holdOntoVideoFrameInCanvas(video, canvas)
|
||||||
canvas.style.display = 'block'
|
canvas.style.display = 'block'
|
||||||
|
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
@ -231,9 +189,6 @@ const engineStreamMachine = setup({
|
|||||||
height,
|
height,
|
||||||
token: context.authToken,
|
token: context.authToken,
|
||||||
settings: settingsNext,
|
settings: settingsNext,
|
||||||
makeDefaultPlanes: () => {
|
|
||||||
return makeDefaultPlanes(kclManager.engineCommandManager)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
event.modelingMachineActorSend({
|
event.modelingMachineActorSend({
|
||||||
@ -250,11 +205,24 @@ const engineStreamMachine = setup({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
context: (initial) => initial.input,
|
|
||||||
initial: EngineStreamState.Off,
|
initial: EngineStreamState.Off,
|
||||||
|
context: (initial) => initial.input,
|
||||||
states: {
|
states: {
|
||||||
[EngineStreamState.Off]: {
|
[EngineStreamState.Off]: {
|
||||||
|
reenter: true,
|
||||||
on: {
|
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]: {
|
[EngineStreamTransition.StartOrReconfigureEngine]: {
|
||||||
target: EngineStreamState.On,
|
target: EngineStreamState.On,
|
||||||
},
|
},
|
||||||
@ -267,6 +235,7 @@ const engineStreamMachine = setup({
|
|||||||
input: (args) => args,
|
input: (args) => args,
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
|
// Transition requested by engineConnection
|
||||||
[EngineStreamTransition.SetMediaStream]: {
|
[EngineStreamTransition.SetMediaStream]: {
|
||||||
target: EngineStreamState.On,
|
target: EngineStreamState.On,
|
||||||
actions: [
|
actions: [
|
||||||
@ -335,5 +304,3 @@ const engineStreamMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export default createActorContext(engineStreamMachine)
|
|
@ -1,4 +1,5 @@
|
|||||||
export const ACTOR_IDS = {
|
export const ACTOR_IDS = {
|
||||||
AUTH: 'auth',
|
AUTH: 'auth',
|
||||||
SETTINGS: 'settings',
|
SETTINGS: 'settings',
|
||||||
|
ENGINE_STREAM: 'engine_stream',
|
||||||
} as const
|
} as const
|
||||||
|
Reference in New Issue
Block a user