Make the stream idle option a slider for time

This commit is contained in:
49lf
2024-09-19 13:22:07 -04:00
parent 788270d4fc
commit 05a2eada9a
13 changed files with 334 additions and 273 deletions

View File

@ -23,7 +23,10 @@ import {
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { EngineConnectionStateType } from 'lang/std/engineConnection' import { EngineConnectionStateType } from 'lang/std/engineConnection'
import useEngineStreamContext, { EngineStreamState, EngineStreamTransition } from 'hooks/useEngineStreamContext' import useEngineStreamContext, {
EngineStreamState,
EngineStreamTransition,
} from 'hooks/useEngineStreamContext'
export function Toolbar({ export function Toolbar({
className = '', className = '',

View File

@ -97,10 +97,12 @@ export class CameraControls {
wasDragging: boolean wasDragging: boolean
mouseDownPosition: Vector2 mouseDownPosition: Vector2
mouseNewPosition: Vector2 mouseNewPosition: Vector2
old: { old:
camera: PerspectiveCamera, | {
target: Vector3, camera: PerspectiveCamera
} | undefined target: Vector3
}
| undefined
rotationSpeed = 0.3 rotationSpeed = 0.3
enableRotate = true enableRotate = true
enablePan = true enablePan = true
@ -960,33 +962,51 @@ export class CameraControls {
// camera coordinates after a zoom-to-fit... So this is much easier, and // camera coordinates after a zoom-to-fit... So this is much easier, and
// maps better to screen coordinates. // maps better to screen coordinates.
await this.engineCommandManager.sendSceneCommand({ await this.engineCommandManager
type: 'modeling_cmd_batch_req', .sendSceneCommand({
batch_id: uuidv4(), type: 'modeling_cmd_batch_req',
responses: true, batch_id: uuidv4(),
requests: [ responses: true,
{ requests: [
type: 'modeling_cmd_req', {
cmd_id: uuidv4(), type: 'modeling_cmd_req',
cmd: { cmd_id: uuidv4(),
type: 'zoom_to_fit', cmd: {
object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects type: 'zoom_to_fit',
padding: panesWidth > 0 ? (window.innerWidth / panesWidth) : 0.2, // padding around the objects object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects
padding: panesWidth > 0 ? window.innerWidth / panesWidth : 0.2, // padding around the objects
},
}, },
}, {
{ type: 'modeling_cmd_req',
type: 'modeling_cmd_req', cmd: {
cmd: { type: 'camera_drag_start',
type: 'camera_drag_start', interaction: 'pan',
interaction: 'pan', window: { x: 0, y: 0 },
window: { x: 0, y: 0 }, },
cmd_id: uuidv4(),
}, },
cmd_id: uuidv4(), {
}, type: 'modeling_cmd_req',
{ cmd: {
type: 'camera_drag_move',
interaction: 'pan',
window: {
x: goRightPx,
y: 0,
},
},
cmd_id: uuidv4(),
},
],
})
// engineCommandManager can't subscribe to batch responses so we'll send
// this one off by its lonesome after.
.then(() =>
this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd: { cmd: {
type: 'camera_drag_move', type: 'camera_drag_end',
interaction: 'pan', interaction: 'pan',
window: { window: {
x: goRightPx, x: goRightPx,
@ -994,23 +1014,8 @@ export class CameraControls {
}, },
}, },
cmd_id: uuidv4(), cmd_id: uuidv4(),
}, })
] )
})
// engineCommandManager can't subscribe to batch responses so we'll send
// this one off by its lonesome after.
.then(() => this.engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd: {
type: 'camera_drag_end',
interaction: 'pan',
window: {
x: goRightPx,
y: 0,
},
},
cmd_id: uuidv4(),
}))
} }
async tweenCameraToQuaternion( async tweenCameraToQuaternion(

View File

@ -29,7 +29,9 @@ export const CommandBar = () => {
}, [pathname]) }, [pathname])
useEffect(() => { useEffect(() => {
if (immediateState.type !== EngineConnectionStateType.ConnectionEstablished) { if (
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
) {
commandBarSend({ type: 'Close' }) commandBarSend({ type: 'Close' })
} }
}, [immediateState]) }, [immediateState])

View File

@ -11,7 +11,8 @@ export function CommandBarOpenButton() {
const { immediateState } = useNetworkContext() const { immediateState } = useNetworkContext()
const platform = usePlatform() const platform = usePlatform()
const isDisabled = immediateState.type !== EngineConnectionStateType.ConnectionEstablished const isDisabled =
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
return ( return (
<button <button

View File

@ -9,13 +9,14 @@ import { btnName } from 'lib/cameraControls'
import { trap } from 'lib/trap' import { trap } from 'lib/trap'
import { sendSelectEventToEngine } from 'lib/selections' import { sendSelectEventToEngine } from 'lib/selections'
import { kclManager, engineCommandManager } from 'lib/singletons' import { kclManager, engineCommandManager } from 'lib/singletons'
import { import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
EngineCommandManagerEvents,
} from 'lang/std/engineConnection'
import { useRouteLoaderData } from 'react-router-dom' import { useRouteLoaderData } from 'react-router-dom'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { IndexLoaderData } from 'lib/types' import { IndexLoaderData } from 'lib/types'
import useEngineStreamContext, { EngineStreamState, EngineStreamTransition } from 'hooks/useEngineStreamContext' import useEngineStreamContext, {
EngineStreamState,
EngineStreamTransition,
} from 'hooks/useEngineStreamContext'
export const EngineStream = () => { export const EngineStream = () => {
const { setAppState } = useAppState() const { setAppState } = useAppState()
@ -46,7 +47,7 @@ export const EngineStream = () => {
// so those exception people don't see. // so those exception people don't see.
const REASONABLE_TIME_TO_REFRESH_STREAM_SIZE = 100 const REASONABLE_TIME_TO_REFRESH_STREAM_SIZE = 100
const configure = () => { const configure = () => {
engineStreamActor.send({ engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine, type: EngineStreamTransition.StartOrReconfigureEngine,
modelingMachineActorSend, modelingMachineActorSend,
@ -57,9 +58,9 @@ export const EngineStream = () => {
onMediaStream(mediaStream: MediaStream) { onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({ engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream, type: EngineStreamTransition.SetMediaStream,
mediaStream mediaStream,
}) })
} },
}) })
} }
@ -91,11 +92,13 @@ export const EngineStream = () => {
const canvas = engineStreamState.context.canvasRef?.current const canvas = engineStreamState.context.canvasRef?.current
if (!canvas) return if (!canvas) return
if (Math.abs(video.width - window.innerWidth) > 4 || Math.abs(video.height - window.innerHeight) > 4) { if (
Math.abs(video.width - window.innerWidth) > 4 ||
Math.abs(video.height - window.innerHeight) > 4
) {
timeoutStart.current = Date.now() timeoutStart.current = Date.now()
configure() configure()
} }
}, REASONABLE_TIME_TO_REFRESH_STREAM_SIZE) }, REASONABLE_TIME_TO_REFRESH_STREAM_SIZE)
return () => { return () => {
@ -105,7 +108,10 @@ export const EngineStream = () => {
// When the video and canvas element references are set, start the engine. // When the video and canvas element references are set, start the engine.
useEffect(() => { useEffect(() => {
if (engineStreamState.context.canvasRef.current && engineStreamState.context.videoRef.current) { if (
engineStreamState.context.canvasRef.current &&
engineStreamState.context.videoRef.current
) {
engineStreamActor.send({ engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine, type: EngineStreamTransition.StartOrReconfigureEngine,
modelingMachineActorSend, modelingMachineActorSend,
@ -114,24 +120,30 @@ export const EngineStream = () => {
onMediaStream(mediaStream: MediaStream) { onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({ engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream, type: EngineStreamTransition.SetMediaStream,
mediaStream mediaStream,
}) })
} },
}) })
} }
}, [engineStreamState.context.canvasRef.current, engineStreamState.context.videoRef.current]) }, [
engineStreamState.context.canvasRef.current,
engineStreamState.context.videoRef.current,
])
// On settings change, reconfigure the engine. When paused this gets really tricky, // On settings change, reconfigure the engine. When paused this gets really tricky,
// and also requires onMediaStream to be set! // and also requires onMediaStream to be set!
useEffect(() => { useEffect(() => {
engineStreamActor.send({ engineStreamActor.send({
type: EngineStreamTransition.StartOrReconfigureEngine, modelingMachineActorSend, settings: settingsEngine, setAppState, type: EngineStreamTransition.StartOrReconfigureEngine,
onMediaStream(mediaStream: MediaStream) { modelingMachineActorSend,
engineStreamActor.send({ settings: settingsEngine,
type: EngineStreamTransition.SetMediaStream, setAppState,
mediaStream onMediaStream(mediaStream: MediaStream) {
}) engineStreamActor.send({
} type: EngineStreamTransition.SetMediaStream,
mediaStream,
})
},
}) })
}, [settings.context]) }, [settings.context])
@ -147,7 +159,7 @@ export const EngineStream = () => {
} }
}, [file?.path, engineCommandManager.engineConnection]) }, [file?.path, engineCommandManager.engineConnection])
const IDLE_TIME_MS = 1000 * 6 const IDLE_TIME_MS = Number(streamIdleMode) * 1000 * 60
// When streamIdleMode is changed, setup or teardown the timeouts // When streamIdleMode is changed, setup or teardown the timeouts
const timeoutStart = useRef<number | null>(null) const timeoutStart = useRef<number | null>(null)
@ -181,10 +193,10 @@ export const EngineStream = () => {
}, [modelingMachineState]) }, [modelingMachineState])
useEffect(() => { useEffect(() => {
if (!streamIdleMode) return if (!streamIdleMode) return
const onAnyInput = () => { const onAnyInput = () => {
// Just in case it happens in the middle of the user turning off // Just in case it happens in the middle of the user turning off
// idle mode. // idle mode.
if (!streamIdleMode) { if (!streamIdleMode) {
timeoutStart.current = null timeoutStart.current = null
@ -200,9 +212,9 @@ export const EngineStream = () => {
onMediaStream(mediaStream: MediaStream) { onMediaStream(mediaStream: MediaStream) {
engineStreamActor.send({ engineStreamActor.send({
type: EngineStreamTransition.SetMediaStream, type: EngineStreamTransition.SetMediaStream,
mediaStream mediaStream,
}) })
} },
}) })
} }
@ -236,7 +248,7 @@ export const EngineStream = () => {
} }
}, [streamIdleMode, engineStreamState.value]) }, [streamIdleMode, engineStreamState.value])
const isNetworkOkay = const isNetworkOkay =
overallState === NetworkHealthState.Ok || overallState === NetworkHealthState.Ok ||
overallState === NetworkHealthState.Weak overallState === NetworkHealthState.Weak
@ -273,12 +285,15 @@ export const EngineStream = () => {
/> />
<canvas <canvas
key={engineStreamActor.id + 'canvas'} key={engineStreamActor.id + 'canvas'}
ref={engineStreamState.context.canvasRef} className="cursor-pointer" id="freeze-frame">No canvas support</canvas> ref={engineStreamState.context.canvasRef}
className="cursor-pointer"
id="freeze-frame"
>
No canvas support
</canvas>
<ClientSideScene <ClientSideScene
cameraControls={settings.context.modeling.mouseControls.current} cameraControls={settings.context.modeling.mouseControls.current}
/> />
</div> </div>
) )
} }

View File

@ -14,7 +14,6 @@ import { NetworkMachineIndicator } from './NetworkMachineIndicator'
import { ModelStateIndicator } from './ModelStateIndicator' import { ModelStateIndicator } from './ModelStateIndicator'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
export function LowerRightControls({ export function LowerRightControls({
children, children,
coreDumpManager, coreDumpManager,

View File

@ -1,7 +1,9 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useEngineCommands } from './EngineCommands' import { useEngineCommands } from './EngineCommands'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import useEngineStreamContext, { EngineStreamState } from 'hooks/useEngineStreamContext' import useEngineStreamContext, {
EngineStreamState,
} from 'hooks/useEngineStreamContext'
export const ModelStateIndicator = () => { export const ModelStateIndicator = () => {
const [commands] = useEngineCommands() const [commands] = useEngineCommands()
@ -26,26 +28,13 @@ export const ModelStateIndicator = () => {
let dataTestId = 'model-state-indicator' let dataTestId = 'model-state-indicator'
if (engineStreamState.value === EngineStreamState.Paused) { if (engineStreamState.value === EngineStreamState.Paused) {
className += className += 'text-secondary'
'text-secondary' icon = <CustomIcon data-testid={dataTestId + '-paused'} name="parallel" />
icon = (
<CustomIcon
data-testid={dataTestId + '-paused'}
name="parallel"
/>
)
} else if (engineStreamState.value === EngineStreamState.Resuming) { } else if (engineStreamState.value === EngineStreamState.Resuming) {
className += className += 'text-secondary'
'text-secondary' icon = <CustomIcon data-testid={dataTestId + '-resuming'} name="parallel" />
icon = (
<CustomIcon
data-testid={dataTestId + '-resuming'}
name="parallel"
/>
)
} else if (isDone) { } else if (isDone) {
className += className += 'text-secondary'
'text-secondary'
icon = ( icon = (
<CustomIcon <CustomIcon
data-testid={dataTestId + '-execution-done'} data-testid={dataTestId + '-execution-done'}

View File

@ -9,7 +9,7 @@ import {
useContext, useContext,
MutableRefObject, MutableRefObject,
forwardRef, forwardRef,
} from 'react'm } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes' import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
@ -25,7 +25,7 @@ import { useKclContext } from 'lang/KclProvider'
import { MachineManagerContext } from 'components/MachineManagerProvider' import { MachineManagerContext } from 'components/MachineManagerProvider'
interface ModelingSidebarProps { interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40', paneOpacity: '' | 'opacity-20' | 'opacity-40'
ref: MutableRefObject<HTMLDivElement> ref: MutableRefObject<HTMLDivElement>
} }
@ -38,7 +38,10 @@ function getPlatformString(): 'web' | 'desktop' {
return isDesktop() ? 'desktop' : 'web' return isDesktop() ? 'desktop' : 'web'
} }
export const ModelingSidebar = forwardRef(function ModelingSidebar({ paneOpacity }: ModelingSidebarProps, ref) { export const ModelingSidebar = forwardRef(function ModelingSidebar(
{ paneOpacity }: ModelingSidebarProps,
ref
) {
const machineManager = useContext(MachineManagerContext) const machineManager = useContext(MachineManagerContext)
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const kclContext = useKclContext() const kclContext = useKclContext()

View File

@ -22,11 +22,11 @@ export enum EngineStreamTransition {
} }
export interface EngineStreamContext { export interface EngineStreamContext {
pool: string | null, pool: string | null
authToken: string | null, authToken: string | null
mediaStream: MediaStream | null, mediaStream: MediaStream | null
videoRef: MutableRefObject<HTMLVideoElement | null>, videoRef: MutableRefObject<HTMLVideoElement | null>
canvasRef: MutableRefObject<HTMLCanvasElement | null>, canvasRef: MutableRefObject<HTMLCanvasElement | null>
} }
export function getDimensions(streamWidth: number, streamHeight: number) { export function getDimensions(streamWidth: number, streamHeight: number) {
@ -42,185 +42,196 @@ export function getDimensions(streamWidth: number, streamHeight: number) {
} }
const engineStreamMachine = setup({ const engineStreamMachine = setup({
types: { types: {
context: {} as EngineStreamContext, context: {} as EngineStreamContext,
input: {} as EngineStreamContext, input: {} as EngineStreamContext,
}, },
actions: { actions: {
[EngineStreamTransition.Play]({ context }, params: { zoomToFit: boolean }) { [EngineStreamTransition.Play]({ context }, params: { zoomToFit: boolean }) {
const canvas = context.canvasRef.current const canvas = context.canvasRef.current
if (!canvas) return false if (!canvas) return false
const video = context.videoRef.current const video = context.videoRef.current
if (!video) return false if (!video) return false
const mediaStream = context.mediaStream const mediaStream = context.mediaStream
if (!mediaStream) return false if (!mediaStream) return false
video.style.display = "block" video.style.display = 'block'
canvas.style.display = "none" canvas.style.display = 'none'
video.srcObject = mediaStream video.srcObject = mediaStream
void sceneInfra.camControls.restoreCameraPosition() void sceneInfra.camControls
.restoreCameraPosition()
.then(() => video.play()) .then(() => video.play())
.catch((e) => { .catch((e) => {
console.warn('Video playing was prevented', e, video) console.warn('Video playing was prevented', e, video)
}) })
.then(() => kclManager.executeCode(params.zoomToFit)) .then(() => kclManager.executeCode(params.zoomToFit))
.catch(trap) .catch(trap)
}, },
[EngineStreamTransition.Pause]({ context }) { [EngineStreamTransition.Pause]({ context }) {
const video = context.videoRef.current const video = context.videoRef.current
if (!video) return if (!video) return
video.pause() video.pause()
const canvas = context.canvasRef.current const canvas = context.canvasRef.current
if (!canvas) return if (!canvas) return
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'
canvas.style.height = video.videoHeight + 'px' canvas.style.height = video.videoHeight + 'px'
canvas.style.display = "block" canvas.style.display = 'block'
const ctx = canvas.getContext("2d") const ctx = canvas.getContext('2d')
if (!ctx) return if (!ctx) return
ctx.drawImage(video, 0, 0, canvas.width, canvas.height) ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
// 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.
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
video.style.display = "none" video.style.display = 'none'
// Destroy the media stream only. We will re-establish it. We could // Destroy the media stream only. We will re-establish it. We could
// leave everything at pausing, preventing video decoders from running // leave everything at pausing, preventing video decoders from running
// but we can do even better by significantly reducing network // but we can do even better by significantly reducing network
// cards also. // cards also.
context.mediaStream?.getVideoTracks()[0].stop() context.mediaStream?.getVideoTracks()[0].stop()
video.srcObject = null video.srcObject = null
sceneInfra.camControls.old = { sceneInfra.camControls.old = {
camera: sceneInfra.camControls.camera.clone(), camera: sceneInfra.camControls.camera.clone(),
target: sceneInfra.camControls.target.clone() target: sceneInfra.camControls.target.clone(),
}
engineCommandManager.tearDown({ idleMode: true })
})
},
async [EngineStreamTransition.StartOrReconfigureEngine]({ context, event }) {
if (!context.authToken) return
const video = context.videoRef.current
if (!video) return
const { width, height } = getDimensions(
window.innerWidth,
window.innerHeight,
)
video.width = width
video.height = height
const settingsNext = {
// override the pool param (?pool=) to request a specific engine instance
// from a particular pool.
pool: context.pool,
...event.settings,
} }
engineCommandManager.settings = settingsNext engineCommandManager.tearDown({ idleMode: true })
})
},
async [EngineStreamTransition.StartOrReconfigureEngine]({
context,
event,
}) {
if (!context.authToken) return
engineCommandManager.start({ const video = context.videoRef.current
setMediaStream: event.onMediaStream, if (!video) return
setIsStreamReady: (isStreamReady) => event.setAppState({ isStreamReady }),
width,
height,
token: context.authToken,
settings: settingsNext,
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
event.modelingMachineActorSend({ const { width, height } = getDimensions(
type: 'Set context', window.innerWidth,
data: { window.innerHeight
streamDimensions: { )
streamWidth: width,
streamHeight: height, video.width = width
}, video.height = height
const settingsNext = {
// override the pool param (?pool=) to request a specific engine instance
// from a particular pool.
pool: context.pool,
...event.settings,
}
engineCommandManager.settings = settingsNext
engineCommandManager.start({
setMediaStream: event.onMediaStream,
setIsStreamReady: (isStreamReady) =>
event.setAppState({ isStreamReady }),
width,
height,
token: context.authToken,
settings: settingsNext,
makeDefaultPlanes: () => {
return makeDefaultPlanes(kclManager.engineCommandManager)
},
modifyGrid: (hidden: boolean) => {
return modifyGrid(kclManager.engineCommandManager, hidden)
},
})
event.modelingMachineActorSend({
type: 'Set context',
data: {
streamDimensions: {
streamWidth: width,
streamHeight: height,
}, },
}) },
})
},
async [EngineStreamTransition.Resume]({ context, event }) {
// engineCommandManager.engineConnection?.reattachMediaStream()
},
},
}).createMachine({
context: (initial) => initial.input,
initial: EngineStreamState.Off,
states: {
[EngineStreamState.Off]: {
on: {
[EngineStreamTransition.StartOrReconfigureEngine]: {
target: EngineStreamState.On,
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
},
}, },
async [EngineStreamTransition.Resume]({ context, event }) { },
// engineCommandManager.engineConnection?.reattachMediaStream() [EngineStreamState.On]: {
on: {
[EngineStreamTransition.SetMediaStream]: {
target: EngineStreamState.On,
actions: [
assign({ mediaStream: ({ context, event }) => event.mediaStream }),
],
},
[EngineStreamTransition.Play]: {
target: EngineStreamState.Playing,
actions: [
{ type: EngineStreamTransition.Play, params: { zoomToFit: true } },
],
},
}, },
} },
}).createMachine({ [EngineStreamState.Playing]: {
context: (initial) => initial.input, on: {
initial: EngineStreamState.Off, [EngineStreamTransition.StartOrReconfigureEngine]: {
states: { target: EngineStreamState.Playing,
[EngineStreamState.Off]: { reenter: true,
on: { actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
[EngineStreamTransition.StartOrReconfigureEngine]: { },
target: EngineStreamState.On, [EngineStreamTransition.Pause]: {
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine } ] target: EngineStreamState.Paused,
} actions: [{ type: EngineStreamTransition.Pause }],
} },
}, },
[EngineStreamState.On]: { },
on: { [EngineStreamState.Paused]: {
[EngineStreamTransition.SetMediaStream]: { on: {
target: EngineStreamState.On, [EngineStreamTransition.StartOrReconfigureEngine]: {
actions: [ assign({ mediaStream: ({ context, event }) => event.mediaStream }) ] target: EngineStreamState.Resuming,
}, actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine }],
[EngineStreamTransition.Play]: { },
target: EngineStreamState.Playing,
actions: [ { type: EngineStreamTransition.Play, params: { zoomToFit: true }} ]
}
}
}, },
[EngineStreamState.Playing]: { },
on: { [EngineStreamState.Resuming]: {
[EngineStreamTransition.StartOrReconfigureEngine]: { on: {
target: EngineStreamState.Playing, [EngineStreamTransition.SetMediaStream]: {
reenter: true, target: EngineStreamState.Resuming,
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine } ] actions: [
}, assign({ mediaStream: ({ context, event }) => event.mediaStream }),
[EngineStreamTransition.Pause]: { ],
target: EngineStreamState.Paused, },
actions: [ { type: EngineStreamTransition.Pause } ] [EngineStreamTransition.Play]: {
} target: EngineStreamState.Playing,
} actions: [
{ type: EngineStreamTransition.Play, params: { zoomToFit: false } },
],
},
}, },
[EngineStreamState.Paused]: { },
on: { },
[EngineStreamTransition.StartOrReconfigureEngine]: { })
target: EngineStreamState.Resuming,
actions: [{ type: EngineStreamTransition.StartOrReconfigureEngine } ]
},
}
},
[EngineStreamState.Resuming]: {
on: {
[EngineStreamTransition.SetMediaStream]: {
target: EngineStreamState.Resuming,
actions: [ assign({ mediaStream: ({ context, event }) => event.mediaStream }) ]
},
[EngineStreamTransition.Play]: {
target: EngineStreamState.Playing,
actions: [ { type: EngineStreamTransition.Play, params: { zoomToFit: false }} ]
}
}
},
}
})
export default createActorContext(engineStreamMachine) export default createActorContext(engineStreamMachine)

View File

@ -404,7 +404,8 @@ class EngineConnection extends EventTarget {
default: default:
if (this.isConnecting()) break if (this.isConnecting()) break
// Means we never could do an initial connection. Reconnect everything. // Means we never could do an initial connection. Reconnect everything.
if (!this.pingPongSpan.ping) this.connect({ reconnect: false }).catch(reportRejection) if (!this.pingPongSpan.ping)
this.connect({ reconnect: false }).catch(reportRejection)
break break
} }
}, pingIntervalMs) }, pingIntervalMs)
@ -542,7 +543,6 @@ class EngineConnection extends EventTarget {
type: DisconnectingType.Quit, type: DisconnectingType.Quit,
}, },
} }
} }
/** /**
@ -1164,7 +1164,7 @@ class EngineConnection extends EventTarget {
} }
if (args.reconnect) { if (args.reconnect) {
createWebSocketConnection() createWebSocketConnection()
} else { } else {
this.onNetworkStatusReady = () => { this.onNetworkStatusReady = () => {
createWebSocketConnection() createWebSocketConnection()

View File

@ -181,13 +181,46 @@ export function createSettings() {
/** /**
* Stream resource saving behavior toggle * Stream resource saving behavior toggle
*/ */
streamIdleMode: new Setting<boolean>({ streamIdleMode: new Setting<number | null>({
defaultValue: false, defaultValue: null,
description: 'Toggle stream idling, saving bandwidth and battery', description: 'Toggle stream idling, saving bandwidth and battery',
validate: (v) => typeof v === 'boolean', validate: (v) =>
commandConfig: { v === null ||
inputType: 'boolean', (typeof v === 'number' &&
}, Number(v) >= 0 &&
Number(v) <= 60),
Component: ({ value, updateValue }) => (
<div className="flex item-center gap-4 px-2 m-0 py-0">
<div className="flex flex-col">
<input
type="checkbox"
checked={value !== null}
onChange={(e) => updateValue(!e.currentTarget.checked ? null : 5)}
className="block w-4 h-4"
/>
<div></div>
</div>
<div className="flex flex-col grow">
<input
type="range"
onChange={(e) =>
updateValue(parseInt(e.currentTarget.value))
}
disabled={value === null}
value={value}
min={1}
max={60}
step={1}
className="block flex-1"
/>
{ value !== null &&
<div>
{value === 60 ? '1 hour' : value === 1 ? '1 minute' : value + ' minutes'}
</div>
}
</div>
</div>
),
}), }),
onboardingStatus: new Setting<string>({ onboardingStatus: new Setting<string>({
defaultValue: '', defaultValue: '',

View File

@ -121,7 +121,7 @@ pub struct AppSettings {
pub dismiss_web_banner: bool, pub dismiss_web_banner: bool,
/// When the user is idle, and this is true, the stream will be torn down. /// When the user is idle, and this is true, the stream will be torn down.
#[serde(default, alias = "streamIdleMode", skip_serializing_if = "is_default")] #[serde(default, alias = "streamIdleMode", skip_serializing_if = "is_default")]
stream_idle_mode: bool, stream_idle_mode: Option<FloatOrInt>,
} }
// TODO: When we remove backwards compatibility with the old settings file, we can remove this. // TODO: When we remove backwards compatibility with the old settings file, we can remove this.
@ -582,7 +582,7 @@ textWrapping = true
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false, stream_idle_mode: None,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::In, base_unit: UnitLength::In,
@ -643,7 +643,7 @@ includeSettings = false
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false, stream_idle_mode: None,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::Yd, base_unit: UnitLength::Yd,
@ -709,7 +709,7 @@ defaultProjectName = "projects-$nnn"
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false, stream_idle_mode: None,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::Yd, base_unit: UnitLength::Yd,
@ -787,7 +787,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false, stream_idle_mode: None,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::Mm, base_unit: UnitLength::Mm,

View File

@ -123,7 +123,7 @@ includeSettings = false
theme_color: None, theme_color: None,
dismiss_web_banner: false, dismiss_web_banner: false,
enable_ssao: None, enable_ssao: None,
stream_idle_mode: false, stream_idle_mode: None,
}, },
modeling: ModelingSettings { modeling: ModelingSettings {
base_unit: UnitLength::Yd, base_unit: UnitLength::Yd,