Lf94/pause improvements (#3032)

* Add stream idle mode as a setting (default is off)

* Add pause icon
This commit is contained in:
49fl
2024-07-16 22:45:11 -04:00
committed by GitHub
parent d9d0a72306
commit 482833c88f
11 changed files with 136 additions and 36 deletions

View File

@ -166,7 +166,7 @@ export const ModelingMachineProvider = ({
store.videoElement?.pause()
kclManager.executeCode(true).then(() => {
if (engineCommandManager.engineConnection?.freezeFrame) return
if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play()
})

View File

@ -1,4 +1,3 @@
import { DEV } from 'env'
import { MouseEventHandler, useEffect, useRef, useState } from 'react'
import { getNormalisedCoordinates } from '../lib/utils'
import Loading from './Loading'
@ -11,6 +10,10 @@ import { btnName } from 'lib/cameraControls'
import { sendSelectEventToEngine } from 'lib/selections'
import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons'
import { useAppStream } from 'AppState'
import {
EngineConnectionStateType,
DisconnectingType,
} from 'lang/std/engineConnection'
export const Stream = () => {
const [isLoading, setIsLoading] = useState(true)
@ -20,15 +23,28 @@ export const Stream = () => {
const { settings } = useSettingsAuthContext()
const { state, send, context } = useModelingContext()
const { mediaStream } = useAppStream()
const { overallState } = useNetworkContext()
const { overallState, immediateState } = useNetworkContext()
const [isFreezeFrame, setIsFreezeFrame] = useState(false)
const [isPaused, setIsPaused] = useState(false)
const IDLE = true
const IDLE = settings.context.app.streamIdleMode.current
const isNetworkOkay =
overallState === NetworkHealthState.Ok ||
overallState === NetworkHealthState.Weak
useEffect(() => {
if (
immediateState.type === EngineConnectionStateType.Disconnecting &&
immediateState.value.type === DisconnectingType.Pause
) {
setIsPaused(true)
}
if (immediateState.type === EngineConnectionStateType.Connecting) {
setIsPaused(false)
}
}, [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
@ -65,14 +81,11 @@ export const Stream = () => {
sceneInfra.modelingSend({ type: 'Cancel' })
// Give video time to pause
window.requestAnimationFrame(() => {
engineCommandManager.tearDown()
engineCommandManager.tearDown({ idleMode: true })
})
}
// Teardown everything if we go hidden or reconnect
if (IDLE && DEV) {
if (globalThis?.window?.document) {
globalThis.window.document.onvisibilitychange = () => {
const onVisibilityChange = () => {
if (globalThis.window.document.visibilityState === 'hidden') {
clearTimeout(timeoutIdIdleA)
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
@ -81,7 +94,13 @@ export const Stream = () => {
engineCommandManager.engineConnection?.connect(true)
}
}
}
// Teardown everything if we go hidden or reconnect
if (IDLE) {
globalThis?.window?.document?.addEventListener(
'visibilitychange',
onVisibilityChange
)
}
let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined
@ -93,7 +112,7 @@ export const Stream = () => {
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
}
if (IDLE && DEV) {
if (IDLE) {
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
@ -101,7 +120,7 @@ export const Stream = () => {
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
}
if (IDLE && DEV) {
if (IDLE) {
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
}
@ -109,7 +128,14 @@ export const Stream = () => {
globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
capture: true,
})
if (IDLE && DEV) {
if (IDLE) {
clearTimeout(timeoutIdIdleA)
clearTimeout(timeoutIdIdleB)
globalThis?.window?.document?.removeEventListener(
'visibilitychange',
onVisibilityChange
)
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'mousemove',
@ -126,7 +152,7 @@ export const Stream = () => {
)
}
}
}, [])
}, [IDLE])
useEffect(() => {
setIsFirstRender(kclManager.isFirstRender)
@ -249,6 +275,32 @@ export const Stream = () => {
<ClientSideScene
cameraControls={settings.context.modeling.mouseControls.current}
/>
{isPaused && (
<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">Paused</p>
</div>
</div>
)}
{(!isNetworkOkay || isLoading || isFirstRender) && !isFreezeFrame && (
<div className="text-center absolute inset-0">
<Loading>

View File

@ -1,11 +1,16 @@
import { createContext, useContext } from 'react'
import {
ConnectingTypeGroup,
EngineConnectionStateType,
EngineConnectionState,
initialConnectingTypeGroupState,
} from '../lang/std/engineConnection'
import { NetworkStatus, NetworkHealthState } from './useNetworkStatus'
export const NetworkContext = createContext<NetworkStatus>({
immediateState: {
type: EngineConnectionStateType.Disconnected,
} as EngineConnectionState,
hasIssues: undefined,
overallState: NetworkHealthState.Disconnected,
internetConnected: true,

View File

@ -6,6 +6,7 @@ import {
EngineCommandManagerEvents,
EngineConnectionEvents,
EngineConnectionStateType,
EngineConnectionState,
ErrorType,
initialConnectingTypeGroupState,
} from '../lang/std/engineConnection'
@ -19,6 +20,7 @@ export enum NetworkHealthState {
}
export interface NetworkStatus {
immediateState: EngineConnectionState
hasIssues: boolean | undefined
overallState: NetworkHealthState
internetConnected: boolean
@ -33,6 +35,9 @@ export interface NetworkStatus {
// Must be called from one place in the application.
// We've chosen the <Router /> component for this.
export function useNetworkStatus() {
const [immediateState, setImmediateState] = useState<EngineConnectionState>({
type: EngineConnectionStateType.Disconnected,
})
const [steps, setSteps] = useState(
structuredClone(initialConnectingTypeGroupState)
)
@ -126,6 +131,7 @@ export function useNetworkStatus() {
const onConnectionStateChange = ({
detail: engineConnectionState,
}: CustomEvent) => {
setImmediateState(engineConnectionState)
setSteps((steps) => {
let nextSteps = structuredClone(steps)
@ -215,6 +221,7 @@ export function useNetworkStatus() {
}, [])
return {
immediateState,
hasIssues,
overallState,
internetConnected,

View File

@ -143,6 +143,7 @@ export enum DisconnectingType {
Error = 'error',
Timeout = 'timeout',
Quit = 'quit',
Pause = 'pause',
}
// Sorted by severity
@ -200,6 +201,7 @@ export type DisconnectingValue =
| State<DisconnectingType.Error, ErrorType>
| State<DisconnectingType.Timeout, void>
| State<DisconnectingType.Quit, void>
| State<DisconnectingType.Pause, void>
// These are ordered by the expected sequence.
export enum ConnectingType {
@ -300,7 +302,7 @@ class EngineConnection extends EventTarget {
pc?: RTCPeerConnection
unreliableDataChannel?: RTCDataChannel
mediaStream?: MediaStream
freezeFrame: boolean = false
idleMode: boolean = false
onIceCandidate = function (
this: RTCPeerConnection,
@ -391,10 +393,10 @@ class EngineConnection extends EventTarget {
this.pingPongSpan = { ping: undefined, pong: undefined }
// Without an interval ping, our connection will timeout.
// If this.freezeFrame is true we skip this logic so only reconnect
// If this.idleMode is true we skip this logic so only reconnect
// happens on mouse move
this.pingIntervalId = setInterval(() => {
if (this.freezeFrame) return
if (this.idleMode) return
switch (this.state.type as EngineConnectionStateType) {
case EngineConnectionStateType.ConnectionEstablished:
@ -456,8 +458,8 @@ class EngineConnection extends EventTarget {
return this.state.type === EngineConnectionStateType.ConnectionEstablished
}
tearDown(opts?: { freeze: boolean }) {
this.freezeFrame = opts?.freeze ?? false
tearDown(opts?: { idleMode: boolean }) {
this.idleMode = opts?.idleMode ?? false
this.disconnectAll()
clearInterval(this.pingIntervalId)
@ -497,9 +499,18 @@ class EngineConnection extends EventTarget {
this.onNetworkStatusReady
)
this.state = {
this.state = opts?.idleMode
? {
type: EngineConnectionStateType.Disconnecting,
value: { type: DisconnectingType.Quit },
value: {
type: DisconnectingType.Pause,
},
}
: {
type: EngineConnectionStateType.Disconnecting,
value: {
type: DisconnectingType.Quit,
},
}
}
@ -1099,8 +1110,6 @@ class EngineConnection extends EventTarget {
this.unreliableDataChannel?.readyState === 'closed'
if (allClosed) {
// Do not notify the rest of the program that we have cut off anything.
if (this.freezeFrame) return
this.state = { type: EngineConnectionStateType.Disconnected }
}
}
@ -1738,7 +1747,7 @@ export class EngineCommandManager extends EventTarget {
}
}
}
tearDown() {
tearDown(opts?: { idleMode: boolean }) {
if (this.engineConnection) {
this.engineConnection.removeEventListener(
EngineConnectionEvents.Opened,
@ -1757,7 +1766,7 @@ export class EngineCommandManager extends EventTarget {
this.onEngineConnectionNewTrack as EventListener
)
this.engineConnection?.tearDown()
this.engineConnection?.tearDown(opts)
this.engineConnection = undefined
// Our window.tearDown assignment causes this case to happen which is
@ -1765,7 +1774,7 @@ export class EngineCommandManager extends EventTarget {
// @ts-ignore
} else if (this.engineCommandManager?.engineConnection) {
// @ts-ignore
this.engineCommandManager?.engineConnection?.tearDown()
this.engineCommandManager?.engineConnection?.tearDown(opts)
}
}
async startNewSession() {

View File

@ -163,6 +163,17 @@ export function createSettings() {
validate: (v) => typeof v === 'boolean',
hideOnPlatform: 'both', //for now
}),
/**
* Stream resource saving behavior toggle
*/
streamIdleMode: new Setting<boolean>({
defaultValue: false,
description: 'Toggle stream idling, saving bandwidth and battery',
validate: (v) => typeof v === 'boolean',
commandConfig: {
inputType: 'boolean',
},
}),
onboardingStatus: new Setting<string>({
defaultValue: '',
validate: (v) => typeof v === 'string',

View File

@ -38,6 +38,7 @@ function configurationToSettingsPayload(
: undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status,
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
projectDirectory: configuration?.settings?.project?.directory,
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
},
@ -75,6 +76,7 @@ function projectConfigurationToSettingsPayload(
: undefined,
onboardingStatus: configuration?.settings?.app?.onboarding_status,
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
streamIdleMode: configuration?.settings?.app?.stream_idle_mode,
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
},
modeling: {

View File

@ -1081,7 +1081,7 @@ export const modelingMachine = createMachine(
type: 'start_path',
},
})
if (!engineCommandManager.engineConnection?.freezeFrame) {
if (!engineCommandManager.engineConnection?.idleMode) {
store.videoElement?.play()
}
if (updatedAst?.selections) {

View File

@ -63,6 +63,12 @@ export const settingsMachine = createMachine(
],
},
'set.app.streamIdleMode': {
target: 'persisting settings',
actions: ['setSettingAtLevel', 'toastSuccess'],
},
'set.modeling.highlightEdges': {
target: 'persisting settings',

View File

@ -234,6 +234,9 @@ pub struct AppSettings {
/// This setting only applies to the web app. And is temporary until we have Linux support.
#[serde(default, alias = "dismissWebBanner", skip_serializing_if = "is_default")]
pub dismiss_web_banner: bool,
/// When the user is idle, and this is true, the stream will be torn down.
#[serde(default, alias = "streamIdleMode", skip_serializing_if = "is_default")]
stream_idle_mode: bool,
}
// TODO: When we remove backwards compatibility with the old settings file, we can remove this.
@ -651,6 +654,7 @@ textWrapping = true
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
stream_idle_mode: false,
},
modeling: ModelingSettings {
base_unit: UnitLength::In,
@ -710,6 +714,7 @@ includeSettings = false
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
stream_idle_mode: false,
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
@ -774,6 +779,7 @@ defaultProjectName = "projects-$nnn"
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
stream_idle_mode: false,
},
modeling: ModelingSettings {
base_unit: UnitLength::Yd,
@ -850,6 +856,7 @@ projectDirectory = "/Users/macinatormax/Documents/kittycad-modeling-projects""#;
theme_color: None,
dismiss_web_banner: false,
enable_ssao: None,
stream_idle_mode: false,
},
modeling: ModelingSettings {
base_unit: UnitLength::Mm,

View File

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