Persist theme - Reload everything on a disconnect (#3250)

* Reload everything on a disconnect

* fix unit-integration tests

* Further improvements to connection manager; persist theme across reconnects

* Fix up artifactGraph.test

* Actually pass the callback

* Kurt hmmm (#3308)

* kurts attempts

* we're almost sane

* get tests working, praise be

---------

Co-authored-by: 49lf <ircsurfer33@gmail.com>

* typo

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
This commit is contained in:
49fl
2024-08-07 03:11:57 -04:00
committed by GitHub
parent e1c45bdb33
commit 3f082c8222
15 changed files with 1006 additions and 732 deletions

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
import { engineCommandManager, sceneInfra } from 'lib/singletons' import { engineCommandManager, sceneInfra } from 'lib/singletons'
import { throttle, isReducedMotion } from 'lib/utils' import { throttle, isReducedMotion } from 'lib/utils'
@ -13,9 +14,12 @@ export const CamToggle = () => {
const [enableRotate, setEnableRotate] = useState(true) const [enableRotate, setEnableRotate] = useState(true)
useEffect(() => { useEffect(() => {
engineCommandManager.waitForReady.then(async () => { engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady,
async () => {
sceneInfra.camControls.dollyZoom(fov) sceneInfra.camControls.dollyZoom(fov)
}) }
)
}, []) }, [])
const toggleCamera = () => { const toggleCamera = () => {

View File

@ -176,6 +176,7 @@ const FileTreeItem = ({
) )
codeManager.writeToFile() codeManager.writeToFile()
// Prevent seeing the model built one piece at a time when changing files
kclManager.isFirstRender = true kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => { kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false kclManager.isFirstRender = false

View File

@ -47,7 +47,7 @@ const Loading = ({ children }: React.PropsWithChildren) => {
onConnectionStateChange as EventListener onConnectionStateChange as EventListener
) )
} }
}, []) }, [engineCommandManager, engineCommandManager.engineConnection])
useEffect(() => { useEffect(() => {
// Don't set long loading time if there's a more severe error // Don't set long loading time if there's a more severe error

View File

@ -78,7 +78,12 @@ import { err, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager' import { modelingMachineEvent } from 'editor/manager'
import { hasValidFilletSelection } from 'lang/modifyAst/addFillet' import { hasValidFilletSelection } from 'lang/modifyAst/addFillet'
import { ExportIntent } from 'lang/std/engineConnection' import {
ExportIntent,
EngineConnectionState,
EngineConnectionStateType,
EngineConnectionEvents,
} from 'lang/std/engineConnection'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -154,7 +159,10 @@ export const ModelingMachineProvider = ({
sceneInfra.camControls.syncDirection = 'engineToClient' sceneInfra.camControls.syncDirection = 'engineToClient'
store.videoElement?.pause() store.videoElement?.pause()
kclManager.isFirstRender = true
kclManager.executeCode().then(() => { kclManager.executeCode().then(() => {
kclManager.isFirstRender = false
if (engineCommandManager.engineConnection?.idleMode) return if (engineCommandManager.engineConnection?.idleMode) return
store.videoElement?.play().catch((e) => { store.videoElement?.play().catch((e) => {
@ -909,15 +917,19 @@ export const ModelingMachineProvider = ({
} }
) )
useSetupEngineManager(streamRef, token, { useSetupEngineManager(
streamRef,
modelingSend,
modelingState.context,
{
pool: pool, pool: pool,
theme: theme.current, theme: theme.current,
highlightEdges: highlightEdges.current, highlightEdges: highlightEdges.current,
enableSSAO: enableSSAO.current, enableSSAO: enableSSAO.current,
modelingSend,
modelingContext: modelingState.context,
showScaleGrid: showScaleGrid.current, showScaleGrid: showScaleGrid.current,
}) },
token
)
useEffect(() => { useEffect(() => {
kclManager.registerExecuteCallback(() => { kclManager.registerExecuteCallback(() => {
@ -945,17 +957,25 @@ export const ModelingMachineProvider = ({
}, [modelingState.context.selectionRanges]) }, [modelingState.context.selectionRanges])
useEffect(() => { useEffect(() => {
const offlineCallback = () => { const onConnectionStateChanged = ({ detail }: CustomEvent) => {
// If we are in sketch mode we need to exit it. // If we are in sketch mode we need to exit it.
// TODO: how do i check if we are in a sketch mode, I only want to call // TODO: how do i check if we are in a sketch mode, I only want to call
// this then. // this then.
if (detail.type === EngineConnectionStateType.Disconnecting) {
modelingSend({ type: 'Cancel' }) modelingSend({ type: 'Cancel' })
} }
window.addEventListener('offline', offlineCallback)
return () => {
window.removeEventListener('offline', offlineCallback)
} }
}, [modelingSend]) engineCommandManager.engineConnection?.addEventListener(
EngineConnectionEvents.ConnectionStateChanged,
onConnectionStateChanged as EventListener
)
return () => {
engineCommandManager.engineConnection?.removeEventListener(
EngineConnectionEvents.ConnectionStateChanged,
onConnectionStateChanged as EventListener
)
}
}, [engineCommandManager.engineConnection, modelingSend])
// Allow using the delete key to delete solids // Allow using the delete key to delete solids
useHotkeys(['backspace', 'delete', 'del'], () => { useHotkeys(['backspace', 'delete', 'del'], () => {

View File

@ -64,13 +64,6 @@ export const KclEditorPane = () => {
: context.app.theme.current : context.app.theme.current
const { copilotLSP, kclLSP } = useLspContext() const { copilotLSP, kclLSP } = useLspContext()
useEffect(() => {
if (typeof window === 'undefined') return
const onlineCallback = () => kclManager.executeCode(true)
window.addEventListener('online', onlineCallback)
return () => window.removeEventListener('online', onlineCallback)
}, [])
// Since these already exist in the editor, we don't need to define them // Since these already exist in the editor, we don't need to define them
// with the wrapper. // with the wrapper.
useHotkeys('mod+z', (e) => { useHotkeys('mod+z', (e) => {

View File

@ -191,6 +191,7 @@ export const SettingsAuthProviderBase = ({
allSettingsIncludesUnitChange || allSettingsIncludesUnitChange ||
resetSettingsIncludesUnitChange resetSettingsIncludesUnitChange
) { ) {
// Unit changes requires a re-exec of code
kclManager.isFirstRender = true kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => { kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false kclManager.isFirstRender = false

View File

@ -11,21 +11,27 @@ import { sendSelectEventToEngine } from 'lib/selections'
import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons' import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons'
import { useAppStream } from 'AppState' import { useAppStream } from 'AppState'
import { import {
EngineCommandManagerEvents,
EngineConnectionStateType, EngineConnectionStateType,
DisconnectingType, DisconnectingType,
} from 'lang/std/engineConnection' } from 'lang/std/engineConnection'
enum StreamState {
Playing = 'playing',
Paused = 'paused',
Resuming = 'resuming',
Unset = 'unset',
}
export const Stream = () => { export const Stream = () => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [isFirstRender, setIsFirstRender] = useState(kclManager.isFirstRender)
const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>() const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>()
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const { state, send, context } = useModelingContext() const { state, send, context } = useModelingContext()
const { mediaStream } = useAppStream() const { mediaStream } = useAppStream()
const { overallState, immediateState } = useNetworkContext() const { overallState, immediateState } = useNetworkContext()
const [isFreezeFrame, setIsFreezeFrame] = useState(false) const [streamState, setStreamState] = useState(StreamState.Unset)
const [isPaused, setIsPaused] = useState(false)
const IDLE = settings.context.app.streamIdleMode.current const IDLE = settings.context.app.streamIdleMode.current
@ -38,10 +44,7 @@ export const Stream = () => {
immediateState.type === EngineConnectionStateType.Disconnecting && immediateState.type === EngineConnectionStateType.Disconnecting &&
immediateState.value.type === DisconnectingType.Pause immediateState.value.type === DisconnectingType.Pause
) { ) {
setIsPaused(true) setStreamState(StreamState.Paused)
}
if (immediateState.type === EngineConnectionStateType.Connecting) {
setIsPaused(false)
} }
}, [immediateState]) }, [immediateState])
@ -76,8 +79,11 @@ export const Stream = () => {
let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined
const teardown = () => { const teardown = () => {
// Already paused
if (streamState === StreamState.Paused) return
videoRef.current?.pause() videoRef.current?.pause()
setIsFreezeFrame(true) setStreamState(StreamState.Paused)
sceneInfra.modelingSend({ type: 'Cancel' }) sceneInfra.modelingSend({ type: 'Cancel' })
// Give video time to pause // Give video time to pause
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
@ -91,7 +97,7 @@ export const Stream = () => {
timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS) timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS)
} else if (!engineCommandManager.engineConnection?.isReady()) { } else if (!engineCommandManager.engineConnection?.isReady()) {
clearTimeout(timeoutIdIdleA) clearTimeout(timeoutIdIdleA)
engineCommandManager.engineConnection?.connect(true) setStreamState(StreamState.Resuming)
} }
} }
@ -106,11 +112,16 @@ export const Stream = () => {
let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined
const onAnyInput = () => { const onAnyInput = () => {
if (streamState === StreamState.Playing) {
// Clear both timers // Clear both timers
clearTimeout(timeoutIdIdleA) clearTimeout(timeoutIdIdleA)
clearTimeout(timeoutIdIdleB) clearTimeout(timeoutIdIdleB)
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS) timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
} }
if (streamState === StreamState.Paused) {
setStreamState(StreamState.Resuming)
}
}
if (IDLE) { if (IDLE) {
globalThis?.window?.document?.addEventListener('keydown', onAnyInput) globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
@ -124,7 +135,27 @@ export const Stream = () => {
timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS) timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS)
} }
const onSceneReady = () => {
kclManager.isFirstRender = true
setStreamState(StreamState.Playing)
kclManager.executeCode(true).then(() => {
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
kclManager.isFirstRender = false
})
}
engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady,
onSceneReady
)
return () => { return () => {
engineCommandManager.removeEventListener(
EngineCommandManagerEvents.SceneReady,
onSceneReady
)
globalThis?.window?.document?.removeEventListener('paste', handlePaste, { globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
capture: true, capture: true,
}) })
@ -152,19 +183,7 @@ export const Stream = () => {
) )
} }
} }
}, [IDLE]) }, [IDLE, streamState])
useEffect(() => {
setIsFirstRender(kclManager.isFirstRender)
if (!kclManager.isFirstRender)
setTimeout(() =>
// execute in the next event loop
videoRef.current?.play().catch((e) => {
console.warn('Video playing was prevented', e, videoRef.current)
})
)
setIsFreezeFrame(!kclManager.isFirstRender)
}, [kclManager.isFirstRender])
useEffect(() => { useEffect(() => {
if ( if (
@ -288,7 +307,8 @@ export const Stream = () => {
<ClientSideScene <ClientSideScene
cameraControls={settings.context.modeling.mouseControls.current} cameraControls={settings.context.modeling.mouseControls.current}
/> />
{isPaused && ( {(streamState === StreamState.Paused ||
streamState === StreamState.Resuming) && (
<div className="text-center absolute inset-0"> <div className="text-center absolute inset-0">
<div <div
className="flex flex-col items-center justify-center h-screen" className="flex flex-col items-center justify-center h-screen"
@ -310,16 +330,19 @@ export const Stream = () => {
/> />
</svg> </svg>
</div> </div>
<p className="text-base mt-2 text-primary bold">Paused</p> <p className="text-base mt-2 text-primary bold">
{streamState === StreamState.Paused && 'Paused'}
{streamState === StreamState.Resuming && 'Resuming'}
</p>
</div> </div>
</div> </div>
)} )}
{(!isNetworkOkay || isLoading || isFirstRender) && !isFreezeFrame && ( {(!isNetworkOkay || isLoading || kclManager.isFirstRender) && (
<div className="text-center absolute inset-0"> <div className="text-center absolute inset-0">
<Loading> <Loading>
{!isNetworkOkay && !isLoading ? ( {!isNetworkOkay && !isLoading && !kclManager.isFirstRender ? (
<span data-testid="loading-stream">Stream disconnected...</span> <span data-testid="loading-stream">Stream disconnected...</span>
) : !isLoading && isFirstRender ? ( ) : !isLoading && kclManager.isFirstRender ? (
<span data-testid="loading-stream">Building scene...</span> <span data-testid="loading-stream">Building scene...</span>
) : ( ) : (
<span data-testid="loading-stream">Loading stream...</span> <span data-testid="loading-stream">Loading stream...</span>

View File

@ -4,29 +4,30 @@ import { deferExecution } from 'lib/utils'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { makeDefaultPlanes, modifyGrid } from 'lang/wasm' import { makeDefaultPlanes, modifyGrid } from 'lang/wasm'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { useNetworkContext } from 'hooks/useNetworkContext'
import { useAppState, useAppStream } from 'AppState' import { useAppState, useAppStream } from 'AppState'
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
import {
EngineConnectionStateType,
EngineConnectionEvents,
DisconnectingType,
} from 'lang/std/engineConnection'
export function useSetupEngineManager( export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>, streamRef: React.RefObject<HTMLDivElement>,
token?: string, modelingSend: ReturnType<typeof useModelingContext>['send'],
modelingContext: ReturnType<typeof useModelingContext>['context'],
settings = { settings = {
pool: null, pool: null,
theme: Themes.System, theme: Themes.System,
highlightEdges: true, highlightEdges: true,
enableSSAO: true, enableSSAO: true,
modelingSend: (() => {}) as any,
modelingContext: {} as any,
showScaleGrid: false, showScaleGrid: false,
} as { } as SettingsViaQueryString,
pool: string | null token?: string
theme: Themes
highlightEdges: boolean
enableSSAO: boolean
modelingSend: ReturnType<typeof useModelingContext>['send']
modelingContext: ReturnType<typeof useModelingContext>['context']
showScaleGrid: boolean
}
) { ) {
const networkContext = useNetworkContext()
const { pingPongHealth, immediateState } = networkContext
const { setAppState } = useAppState() const { setAppState } = useAppState()
const { setMediaStream } = useAppStream() const { setMediaStream } = useAppStream()
@ -35,10 +36,10 @@ export function useSetupEngineManager(
if (settings.pool) { if (settings.pool) {
// override the pool param (?pool=) to request a specific engine instance // override the pool param (?pool=) to request a specific engine instance
// from a particular pool. // from a particular pool.
engineCommandManager.pool = settings.pool engineCommandManager.settings.pool = settings.pool
} }
const startEngineInstance = (restart: boolean = false) => { const startEngineInstance = () => {
// Load the engine command manager once with the initial width and height, // Load the engine command manager once with the initial width and height,
// then we do not want to reload it. // then we do not want to reload it.
const { width: quadWidth, height: quadHeight } = getDimensions( const { width: quadWidth, height: quadHeight } = getDimensions(
@ -50,14 +51,6 @@ export function useSetupEngineManager(
setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }), setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }),
width: quadWidth, width: quadWidth,
height: quadHeight, height: quadHeight,
executeCode: () => {
// We only want to execute the code here that we already have set.
// Nothing else.
kclManager.isFirstRender = true
return kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
},
token, token,
settings, settings,
makeDefaultPlanes: () => { makeDefaultPlanes: () => {
@ -67,7 +60,7 @@ export function useSetupEngineManager(
return modifyGrid(kclManager.engineCommandManager, hidden) return modifyGrid(kclManager.engineCommandManager, hidden)
}, },
}) })
settings.modelingSend({ modelingSend({
type: 'Set context', type: 'Set context',
data: { data: {
streamDimensions: { streamDimensions: {
@ -90,9 +83,27 @@ export function useSetupEngineManager(
}, [ }, [
streamRef?.current?.offsetWidth, streamRef?.current?.offsetWidth,
streamRef?.current?.offsetHeight, streamRef?.current?.offsetHeight,
settings.modelingSend, modelingSend,
]) ])
useEffect(() => {
if (pingPongHealth === 'TIMEOUT') {
engineCommandManager.tearDown()
}
}, [pingPongHealth])
useEffect(() => {
const intervalId = setInterval(() => {
if (immediateState.type === EngineConnectionStateType.Disconnected) {
engineCommandManager.engineConnection = undefined
startEngineInstance()
}
}, 3000)
return () => {
clearInterval(intervalId)
}
}, [immediateState])
useEffect(() => { useEffect(() => {
const handleResize = deferExecution(() => { const handleResize = deferExecution(() => {
const { width, height } = getDimensions( const { width, height } = getDimensions(
@ -100,14 +111,14 @@ export function useSetupEngineManager(
streamRef?.current?.offsetHeight ?? 0 streamRef?.current?.offsetHeight ?? 0
) )
if ( if (
settings.modelingContext.store.streamDimensions.streamWidth !== width || modelingContext.store.streamDimensions.streamWidth !== width ||
settings.modelingContext.store.streamDimensions.streamHeight !== height modelingContext.store.streamDimensions.streamHeight !== height
) { ) {
engineCommandManager.handleResize({ engineCommandManager.handleResize({
streamWidth: width, streamWidth: width,
streamHeight: height, streamHeight: height,
}) })
settings.modelingSend({ modelingSend({
type: 'Set context', type: 'Set context',
data: { data: {
streamDimensions: { streamDimensions: {
@ -120,7 +131,7 @@ export function useSetupEngineManager(
}, 500) }, 500)
const onOnline = () => { const onOnline = () => {
startEngineInstance(true) startEngineInstance()
} }
const onVisibilityChange = () => { const onVisibilityChange = () => {
@ -136,10 +147,18 @@ export function useSetupEngineManager(
window.document.addEventListener('visibilitychange', onVisibilityChange) window.document.addEventListener('visibilitychange', onVisibilityChange)
const onAnyInput = () => { const onAnyInput = () => {
if ( const isEngineNotReadyOrConnecting =
!engineCommandManager.engineConnection?.isReady() && !engineCommandManager.engineConnection?.isReady() &&
!engineCommandManager.engineConnection?.isConnecting() !engineCommandManager.engineConnection?.isConnecting()
) {
const conn = engineCommandManager.engineConnection
const isStreamPaused =
conn?.state.type === EngineConnectionStateType.Disconnecting &&
conn?.state.value.type === DisconnectingType.Pause
if (isEngineNotReadyOrConnecting || isStreamPaused) {
engineCommandManager.engineConnection = undefined
startEngineInstance() startEngineInstance()
} }
} }
@ -150,7 +169,6 @@ export function useSetupEngineManager(
window.document.addEventListener('touchstart', onAnyInput) window.document.addEventListener('touchstart', onAnyInput)
const onOffline = () => { const onOffline = () => {
kclManager.isFirstRender = true
engineCommandManager.tearDown() engineCommandManager.tearDown()
} }

View File

@ -211,7 +211,6 @@ export class KclManager {
type: string type: string
} }
): Promise<void> { ): Promise<void> {
await this?.engineCommandManager?.waitForReady
const currentExecutionId = executionId || Date.now() const currentExecutionId = executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false) this._cancelTokens.set(currentExecutionId, false)
@ -301,7 +300,6 @@ export class KclManager {
codeManager.updateCodeEditor(newCode) codeManager.updateCodeEditor(newCode)
// Write the file to disk. // Write the file to disk.
await codeManager.writeToFile() await codeManager.writeToFile()
await this?.engineCommandManager?.waitForReady
this._ast = { ...newAst } this._ast = { ...newAst }
const { logs, errors, programMemory } = await executeAst({ const { logs, errors, programMemory } = await executeAst({

View File

@ -14,6 +14,10 @@ import {
} from './artifactGraph' } from './artifactGraph'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { engineCommandManager, kclManager } from 'lib/singletons' import { engineCommandManager, kclManager } from 'lib/singletons'
import {
EngineCommandManagerEvents,
EngineConnectionEvents,
} from 'lang/std/engineConnection'
import { CI, VITE_KC_DEV_TOKEN } from 'env' import { CI, VITE_KC_DEV_TOKEN } from 'env'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import fs from 'fs' import fs from 'fs'
@ -113,20 +117,18 @@ beforeAll(async () => {
} }
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local // THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
await new Promise((resolve) => {
engineCommandManager.start({ engineCommandManager.start({
disableWebRTC: true, // disableWebRTC: true,
token: VITE_KC_DEV_TOKEN, token: VITE_KC_DEV_TOKEN,
// there does seem to be a minimum resolution, not sure what it is but 256 works ok. // there does seem to be a minimum resolution, not sure what it is but 256 works ok.
width: 256, width: 256,
height: 256, height: 256,
executeCode: () => {},
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager), makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
setMediaStream: () => {}, setMediaStream: () => {},
setIsStreamReady: () => {}, setIsStreamReady: () => {},
modifyGrid: async () => {}, modifyGrid: async () => {},
}) callbackOnEngineLiteConnect: async () => {
await engineCommandManager.waitForReady
const cacheEntries = Object.entries(codeToWriteCacheFor) as [ const cacheEntries = Object.entries(codeToWriteCacheFor) as [
CodeKey, CodeKey,
string string
@ -136,9 +138,9 @@ beforeAll(async () => {
const ast = parse(code) const ast = parse(code)
if (err(ast)) { if (err(ast)) {
console.error(ast) console.error(ast)
throw ast return Promise.reject(ast)
} }
await kclManager.executeAst(ast) const result = await kclManager.executeAst(ast)
cacheToWriteToFileTemp[codeKey] = { cacheToWriteToFileTemp[codeKey] = {
orderedCommands: engineCommandManager.orderedCommands, orderedCommands: engineCommandManager.orderedCommands,
@ -149,6 +151,10 @@ beforeAll(async () => {
await fsp.mkdir(pathStart, { recursive: true }) await fsp.mkdir(pathStart, { recursive: true })
await fsp.writeFile(fullPath, cache) await fsp.writeFile(fullPath, cache)
resolve(true)
},
})
})
}, 20_000) }, 20_000)
afterAll(() => { afterAll(() => {

View File

@ -1,5 +1,5 @@
import { Program, SourceRange } from 'lang/wasm' import { Program, SourceRange } from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL } from 'env' import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave' import { exportSave } from 'lib/exportSave'
import { deferExecution, isOverlap, uuidv4 } from 'lib/utils' import { deferExecution, isOverlap, uuidv4 } from 'lib/utils'
@ -15,9 +15,10 @@ import {
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { exportMake } from 'lib/exportMake' import { exportMake } from 'lib/exportMake'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
// TODO(paultag): This ought to be tweakable. // TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 10000 const pingIntervalMs = 1000
function isHighlightSetEntity_type( function isHighlightSetEntity_type(
data: any data: any
@ -228,6 +229,7 @@ class EngineConnection extends EventTarget {
unreliableDataChannel?: RTCDataChannel unreliableDataChannel?: RTCDataChannel
mediaStream?: MediaStream mediaStream?: MediaStream
idleMode: boolean = false idleMode: boolean = false
promise?: Promise<void>
onIceCandidate = function ( onIceCandidate = function (
this: RTCPeerConnection, this: RTCPeerConnection,
@ -295,25 +297,34 @@ class EngineConnection extends EventTarget {
private engineCommandManager: EngineCommandManager private engineCommandManager: EngineCommandManager
private pingPongSpan: { ping?: Date; pong?: Date } private pingPongSpan: { ping?: Date; pong?: Date }
private pingIntervalId: ReturnType<typeof setInterval> private pingIntervalId: ReturnType<typeof setInterval> = setInterval(() => {},
60_000)
isUsingConnectionLite: boolean = false
constructor({ constructor({
engineCommandManager, engineCommandManager,
url, url,
token, token,
callbackOnEngineLiteConnect,
}: { }: {
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
url: string url: string
token?: string token?: string
callbackOnEngineLiteConnect?: () => void
}) { }) {
super() super()
this.engineCommandManager = engineCommandManager this.engineCommandManager = engineCommandManager
this.url = url this.url = url
this.token = token this.token = token
this.pingPongSpan = { ping: undefined, pong: undefined } this.pingPongSpan = { ping: undefined, pong: undefined }
if (callbackOnEngineLiteConnect) {
this.connectLite(callbackOnEngineLiteConnect)
this.isUsingConnectionLite = true
return
}
// Without an interval ping, our connection will timeout. // Without an interval ping, our connection will timeout.
// If this.idleMode 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 // happens on mouse move
@ -322,13 +333,22 @@ class EngineConnection extends EventTarget {
switch (this.state.type as EngineConnectionStateType) { switch (this.state.type as EngineConnectionStateType) {
case EngineConnectionStateType.ConnectionEstablished: case EngineConnectionStateType.ConnectionEstablished:
// If there was no reply to the last ping, report a timeout. // If there was no reply to the last ping, report a timeout and
// teardown the connection.
if (this.pingPongSpan.ping && !this.pingPongSpan.pong) { if (this.pingPongSpan.ping && !this.pingPongSpan.pong) {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.PingPongChanged, { new CustomEvent(EngineConnectionEvents.PingPongChanged, {
detail: 'TIMEOUT', detail: 'TIMEOUT',
}) })
) )
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: {
type: DisconnectingType.Timeout,
},
}
this.disconnectAll()
// Otherwise check the time between was >= pingIntervalMs, // Otherwise check the time between was >= pingIntervalMs,
// and if it was, then it's bad network health. // and if it was, then it's bad network health.
} else if (this.pingPongSpan.ping && this.pingPongSpan.pong) { } else if (this.pingPongSpan.ping && this.pingPongSpan.pong) {
@ -358,13 +378,15 @@ class EngineConnection extends EventTarget {
break break
case EngineConnectionStateType.Disconnecting: case EngineConnectionStateType.Disconnecting:
case EngineConnectionStateType.Disconnected: case EngineConnectionStateType.Disconnected:
// Reconnect if we have disconnected. // We will do reconnection elsewhere, because we basically need
if (!this.isConnecting()) this.connect(true) // to destroy this EngineConnection, and this setInterval loop
// lives inside it. (lee) I might change this in the future so it's
// outside this class.
break break
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(true) if (!this.pingPongSpan.ping) this.connect()
break break
} }
}, pingIntervalMs) }, pingIntervalMs)
@ -372,6 +394,101 @@ class EngineConnection extends EventTarget {
this.connect() this.connect()
} }
// SHOULD ONLY BE USED FOR VITESTS
connectLite(callback: () => void) {
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${256}&video_res_height=${256}`
this.websocket = new WebSocket(url, [])
this.websocket.binaryType = 'arraybuffer'
this.send = (a) => {
if (!this.websocket) return
this.websocket.send(JSON.stringify(a))
}
this.onWebSocketOpen = (event) => {
this.send({
type: 'headers',
headers: { Authorization: `Bearer ${VITE_KC_DEV_TOKEN}` },
})
// }
}
this.tearDown = () => {}
this.websocket.addEventListener('open', this.onWebSocketOpen)
this.websocket?.addEventListener('message', ((event: MessageEvent) => {
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data)
const pending =
this.engineCommandManager.pendingCommands[message.request_id || '']
if (!('resp' in message)) return
let resp = message.resp
// If there's no body to the response, we can bail here.
if (!resp || !resp.type) {
return
}
switch (resp.type) {
case 'pong':
break
// Only fires on successful authentication.
case 'ice_server_info':
callback()
return
}
if (
!(
pending &&
message.success &&
(message.resp.type === 'modeling' ||
message.resp.type === 'modeling_batch')
)
)
return
if (
message.resp.type === 'modeling' &&
pending.command.type === 'modeling_cmd_req' &&
message.request_id
) {
this.engineCommandManager.responseMap[message.request_id] = message.resp
} else if (
message.resp.type === 'modeling_batch' &&
pending.command.type === 'modeling_cmd_batch_req'
) {
let individualPendingResponses: {
[key: string]: Models['WebSocketRequest_type']
} = {}
pending.command.requests.forEach(({ cmd, cmd_id }) => {
individualPendingResponses[cmd_id] = {
type: 'modeling_cmd_req',
cmd,
cmd_id,
}
})
Object.entries(message.resp.data.responses).forEach(
([commandId, response]) => {
if (!('response' in response)) return
const command = individualPendingResponses[commandId]
if (!command) return
if (command.type === 'modeling_cmd_req')
this.engineCommandManager.responseMap[commandId] = {
type: 'modeling',
data: {
modeling_response: response.response,
},
}
}
)
}
pending.resolve([message])
delete this.engineCommandManager.pendingCommands[message.request_id || '']
}) as EventListener)
}
isConnecting() { isConnecting() {
return this.state.type === EngineConnectionStateType.Connecting return this.state.type === EngineConnectionStateType.Connecting
} }
@ -382,58 +499,29 @@ class EngineConnection extends EventTarget {
tearDown(opts?: { idleMode: boolean }) { tearDown(opts?: { idleMode: boolean }) {
this.idleMode = opts?.idleMode ?? false this.idleMode = opts?.idleMode ?? false
this.disconnectAll()
clearInterval(this.pingIntervalId) clearInterval(this.pingIntervalId)
this.pc?.removeEventListener('icecandidate', this.onIceCandidate) if (opts?.idleMode) {
this.pc?.removeEventListener('icecandidateerror', this.onIceCandidateError) this.state = {
this.pc?.removeEventListener(
'connectionstatechange',
this.onConnectionStateChange
)
this.pc?.removeEventListener('track', this.onTrack)
this.unreliableDataChannel?.removeEventListener(
'open',
this.onDataChannelOpen
)
this.unreliableDataChannel?.removeEventListener(
'close',
this.onDataChannelClose
)
this.unreliableDataChannel?.removeEventListener(
'error',
this.onDataChannelError
)
this.unreliableDataChannel?.removeEventListener(
'message',
this.onDataChannelMessage
)
this.pc?.removeEventListener('datachannel', this.onDataChannel)
this.websocket?.removeEventListener('open', this.onWebSocketOpen)
this.websocket?.removeEventListener('close', this.onWebSocketClose)
this.websocket?.removeEventListener('error', this.onWebSocketError)
this.websocket?.removeEventListener('message', this.onWebSocketMessage)
window.removeEventListener(
'use-network-status-ready',
this.onNetworkStatusReady
)
this.state = opts?.idleMode
? {
type: EngineConnectionStateType.Disconnecting, type: EngineConnectionStateType.Disconnecting,
value: { value: {
type: DisconnectingType.Pause, type: DisconnectingType.Pause,
}, },
} }
: { }
// Pass the state along
if (this.state.type === EngineConnectionStateType.Disconnecting) return
if (this.state.type === EngineConnectionStateType.Disconnected) return
// Otherwise it's by default a "quit"
this.state = {
type: EngineConnectionStateType.Disconnecting, type: EngineConnectionStateType.Disconnecting,
value: { value: {
type: DisconnectingType.Quit, type: DisconnectingType.Quit,
}, },
} }
this.disconnectAll()
} }
/** /**
@ -443,17 +531,16 @@ class EngineConnection extends EventTarget {
* This will attempt the full handshake, and retry if the connection * This will attempt the full handshake, and retry if the connection
* did not establish. * did not establish.
*/ */
connect(reconnecting?: boolean) { connect(reconnecting?: boolean): Promise<void> {
return new Promise((resolve) => {
if (this.isConnecting() || this.isReady()) { if (this.isConnecting() || this.isReady()) {
return return
} }
const createPeerConnection = () => { const createPeerConnection = () => {
if (!this.engineCommandManager.disableWebRTC) {
this.pc = new RTCPeerConnection({ this.pc = new RTCPeerConnection({
bundlePolicy: 'max-bundle', bundlePolicy: 'max-bundle',
}) })
}
// Other parts of the application expect pc to be initialized when firing. // Other parts of the application expect pc to be initialized when firing.
this.dispatchEvent( this.dispatchEvent(
@ -506,7 +593,10 @@ class EngineConnection extends EventTarget {
`ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}` `ICE candidate returned an error: ${event.errorCode}: ${event.errorText} for ${event.url}`
) )
} }
this.pc?.addEventListener?.('icecandidateerror', this.onIceCandidateError) this.pc?.addEventListener?.(
'icecandidateerror',
this.onIceCandidateError
)
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionstatechange_event
// Event type: generic Event type... // Event type: generic Event type...
@ -523,8 +613,19 @@ class EngineConnection extends EventTarget {
}) })
) )
break break
case 'disconnected':
case 'failed': case 'failed':
this.disconnectAll() this.pc?.removeEventListener('icecandidate', this.onIceCandidate)
this.pc?.removeEventListener(
'icecandidateerror',
this.onIceCandidateError
)
this.pc?.removeEventListener(
'connectionstatechange',
this.onConnectionStateChange
)
this.pc?.removeEventListener('track', this.onTrack)
this.state = { this.state = {
type: EngineConnectionStateType.Disconnecting, type: EngineConnectionStateType.Disconnecting,
value: { value: {
@ -535,6 +636,7 @@ class EngineConnection extends EventTarget {
}, },
}, },
} }
this.disconnectAll()
break break
default: default:
break break
@ -599,7 +701,8 @@ class EngineConnection extends EventTarget {
videoTrackReport.framesPerSecond || 0 videoTrackReport.framesPerSecond || 0
client_metrics.rtc_freeze_count = client_metrics.rtc_freeze_count =
videoTrackReport.freezeCount || 0 videoTrackReport.freezeCount || 0
client_metrics.rtc_jitter_sec = videoTrackReport.jitter || 0.0 client_metrics.rtc_jitter_sec =
videoTrackReport.jitter || 0.0
client_metrics.rtc_keyframes_decoded = client_metrics.rtc_keyframes_decoded =
videoTrackReport.keyFramesDecoded || 0 videoTrackReport.keyFramesDecoded || 0
client_metrics.rtc_total_freezes_duration_sec = client_metrics.rtc_total_freezes_duration_sec =
@ -610,7 +713,8 @@ class EngineConnection extends EventTarget {
videoTrackReport.frameWidth || 0 videoTrackReport.frameWidth || 0
client_metrics.rtc_packets_lost = client_metrics.rtc_packets_lost =
videoTrackReport.packetsLost || 0 videoTrackReport.packetsLost || 0
client_metrics.rtc_pli_count = videoTrackReport.pliCount || 0 client_metrics.rtc_pli_count =
videoTrackReport.pliCount || 0
} else if (videoTrackReport.type === 'transport') { } else if (videoTrackReport.type === 'transport') {
// videoTrackReport.bytesReceived, // videoTrackReport.bytesReceived,
// videoTrackReport.bytesSent, // videoTrackReport.bytesSent,
@ -652,7 +756,9 @@ class EngineConnection extends EventTarget {
} }
// Everything is now connected. // Everything is now connected.
this.state = { type: EngineConnectionStateType.ConnectionEstablished } this.state = {
type: EngineConnectionStateType.ConnectionEstablished,
}
this.engineCommandManager.inSequence = 1 this.engineCommandManager.inSequence = 1
@ -666,17 +772,32 @@ class EngineConnection extends EventTarget {
) )
this.onDataChannelClose = (event) => { this.onDataChannelClose = (event) => {
this.unreliableDataChannel?.removeEventListener(
'open',
this.onDataChannelOpen
)
this.unreliableDataChannel?.removeEventListener(
'close',
this.onDataChannelClose
)
this.unreliableDataChannel?.removeEventListener(
'error',
this.onDataChannelError
)
this.unreliableDataChannel?.removeEventListener(
'message',
this.onDataChannelMessage
)
this.pc?.removeEventListener('datachannel', this.onDataChannel)
this.disconnectAll() this.disconnectAll()
this.finalizeIfAllConnectionsClosed()
} }
this.unreliableDataChannel?.addEventListener( this.unreliableDataChannel?.addEventListener(
'close', 'close',
this.onDataChannelClose this.onDataChannelClose
) )
this.onDataChannelError = (event) => { this.onDataChannelError = (event) => {
this.disconnectAll()
this.state = { this.state = {
type: EngineConnectionStateType.Disconnecting, type: EngineConnectionStateType.Disconnecting,
value: { value: {
@ -687,6 +808,7 @@ class EngineConnection extends EventTarget {
}, },
}, },
} }
this.disconnectAll()
} }
this.unreliableDataChannel?.addEventListener( this.unreliableDataChannel?.addEventListener(
'error', 'error',
@ -696,7 +818,8 @@ class EngineConnection extends EventTarget {
this.onDataChannelMessage = (event) => { this.onDataChannelMessage = (event) => {
const result: UnreliableResponses = JSON.parse(event.data) const result: UnreliableResponses = JSON.parse(event.data)
Object.values( Object.values(
this.engineCommandManager.unreliableSubscriptions[result.type] || {} this.engineCommandManager.unreliableSubscriptions[result.type] ||
{}
).forEach( ).forEach(
// TODO: There is only one response that uses the unreliable channel atm, // TODO: There is only one response that uses the unreliable channel atm,
// highlight_set_entity, if there are more it's likely they will all have the same // highlight_set_entity, if there are more it's likely they will all have the same
@ -756,23 +879,28 @@ class EngineConnection extends EventTarget {
// Send an initial ping // Send an initial ping
this.send({ type: 'ping' }) this.send({ type: 'ping' })
this.pingPongSpan.ping = new Date() this.pingPongSpan.ping = new Date()
if (this.engineCommandManager.disableWebRTC) {
this.engineCommandManager
.initPlanes()
.then(() => this.engineCommandManager.resolveReady())
}
} }
this.websocket.addEventListener('open', this.onWebSocketOpen) this.websocket.addEventListener('open', this.onWebSocketOpen)
this.onWebSocketClose = (event) => { this.onWebSocketClose = (event) => {
this.websocket?.removeEventListener('open', this.onWebSocketOpen)
this.websocket?.removeEventListener('close', this.onWebSocketClose)
this.websocket?.removeEventListener('error', this.onWebSocketError)
this.websocket?.removeEventListener(
'message',
this.onWebSocketMessage
)
window.removeEventListener(
'use-network-status-ready',
this.onNetworkStatusReady
)
this.disconnectAll() this.disconnectAll()
this.finalizeIfAllConnectionsClosed()
} }
this.websocket.addEventListener('close', this.onWebSocketClose) this.websocket.addEventListener('close', this.onWebSocketClose)
this.onWebSocketError = (event) => { this.onWebSocketError = (event) => {
this.disconnectAll()
this.state = { this.state = {
type: EngineConnectionStateType.Disconnecting, type: EngineConnectionStateType.Disconnecting,
value: { value: {
@ -783,6 +911,8 @@ class EngineConnection extends EventTarget {
}, },
}, },
} }
this.disconnectAll()
} }
this.websocket.addEventListener('error', this.onWebSocketError) this.websocket.addEventListener('error', this.onWebSocketError)
@ -798,7 +928,9 @@ class EngineConnection extends EventTarget {
return return
} }
const message: Models['WebSocketResponse_type'] = JSON.parse(event.data) const message: Models['WebSocketResponse_type'] = JSON.parse(
event.data
)
if (!message.success) { if (!message.success) {
const errorsString = message?.errors const errorsString = message?.errors
@ -854,6 +986,8 @@ class EngineConnection extends EventTarget {
case 'pong': case 'pong':
this.pingPongSpan.pong = new Date() this.pingPongSpan.pong = new Date()
break break
// Only fires on successful authentication.
case 'ice_server_info': case 'ice_server_info':
let ice_servers = resp.data?.ice_servers let ice_servers = resp.data?.ice_servers
@ -933,7 +1067,6 @@ class EngineConnection extends EventTarget {
}) })
.catch((err: Error) => { .catch((err: Error) => {
// The local description is invalid, so there's no point continuing. // The local description is invalid, so there's no point continuing.
this.disconnectAll()
this.state = { this.state = {
type: EngineConnectionStateType.Disconnecting, type: EngineConnectionStateType.Disconnecting,
value: { value: {
@ -944,6 +1077,7 @@ class EngineConnection extends EventTarget {
}, },
}, },
} }
this.disconnectAll()
}) })
break break
@ -1014,6 +1148,7 @@ class EngineConnection extends EventTarget {
this.onNetworkStatusReady this.onNetworkStatusReady
) )
} }
})
} }
// Do not change this back to an object or any, we should only be sending the // Do not change this back to an object or any, we should only be sending the
// WebSocketRequest type! // WebSocketRequest type!
@ -1027,6 +1162,9 @@ class EngineConnection extends EventTarget {
// Do not change this back to an object or any, we should only be sending the // Do not change this back to an object or any, we should only be sending the
// WebSocketRequest type! // WebSocketRequest type!
send(message: Models['WebSocketRequest_type']) { send(message: Models['WebSocketRequest_type']) {
// Not connected, don't send anything
if (this.websocket?.readyState === 3) return
// TODO(paultag): Add in logic to determine the connection state and // TODO(paultag): Add in logic to determine the connection state and
// take actions if needed? // take actions if needed?
this.websocket?.send( this.websocket?.send(
@ -1034,18 +1172,35 @@ class EngineConnection extends EventTarget {
) )
} }
disconnectAll() { disconnectAll() {
if (this.websocket?.readyState === 1) {
this.websocket?.close() this.websocket?.close()
}
if (this.unreliableDataChannel?.readyState === 'open') {
this.unreliableDataChannel?.close() this.unreliableDataChannel?.close()
}
if (this.pc?.connectionState === 'connected') {
this.pc?.close() this.pc?.close()
}
this.webrtcStatsCollector = undefined this.webrtcStatsCollector = undefined
}
finalizeIfAllConnectionsClosed() { // Already triggered
const allClosed = if (this.state.type === EngineConnectionStateType.Disconnected) return
this.websocket?.readyState === 3 &&
this.pc?.connectionState === 'closed' && const closedPc = !this.pc || this.pc?.connectionState === 'closed'
const closedUDC =
!this.unreliableDataChannel ||
this.unreliableDataChannel?.readyState === 'closed' this.unreliableDataChannel?.readyState === 'closed'
if (allClosed) {
// Do not check when timing out because websockets take forever to
// report their disconnected state.
const closedWS =
(this.state.type === EngineConnectionStateType.Disconnecting &&
this.state.value.type === DisconnectingType.Timeout) ||
!this.websocket ||
this.websocket?.readyState === 3
if (closedPc && closedUDC && closedWS) {
// Do not notify the rest of the program that we have cut off anything. // Do not notify the rest of the program that we have cut off anything.
this.state = { type: EngineConnectionStateType.Disconnected } this.state = { type: EngineConnectionStateType.Disconnected }
} }
@ -1093,7 +1248,11 @@ export type CommandLog =
} }
export enum EngineCommandManagerEvents { export enum EngineCommandManagerEvents {
// engineConnection is available but scene setup may not have run
EngineAvailable = 'engine-available', EngineAvailable = 'engine-available',
// the whole scene is ready (settings loaded)
SceneReady = 'scene-ready',
} }
/** /**
@ -1151,7 +1310,6 @@ export class EngineCommandManager extends EventTarget {
* any out-of-order late responses in the unreliable channel. * any out-of-order late responses in the unreliable channel.
*/ */
inSequence = 1 inSequence = 1
pool?: string
engineConnection?: EngineConnection engineConnection?: EngineConnection
defaultPlanes: DefaultPlanes | null = null defaultPlanes: DefaultPlanes | null = null
commandLogs: CommandLog[] = [] commandLogs: CommandLog[] = []
@ -1160,6 +1318,8 @@ export class EngineCommandManager extends EventTarget {
reject: (reason: any) => void reject: (reason: any) => void
commandId: string commandId: string
} }
settings: SettingsViaQueryString
/** /**
* Export intent traxcks the intent of the export. If it is null there is no * Export intent traxcks the intent of the export. If it is null there is no
* export in progress. Otherwise it is an enum value of the intent. * export in progress. Otherwise it is an enum value of the intent.
@ -1167,15 +1327,6 @@ export class EngineCommandManager extends EventTarget {
*/ */
private _exportIntent: ExportIntent | null = null private _exportIntent: ExportIntent | null = null
_commandLogCallBack: (command: CommandLog[]) => void = () => {} _commandLogCallBack: (command: CommandLog[]) => void = () => {}
resolveReady = () => {}
/** Folks should realize that wait for ready does not get called _everytime_
* the connection resets and restarts, it only gets called the first time.
*
* Be careful what you put here.
*/
waitForReady: Promise<void> = new Promise((resolve) => {
this.resolveReady = resolve
})
subscriptions: { subscriptions: {
[event: string]: { [event: string]: {
@ -1188,11 +1339,19 @@ export class EngineCommandManager extends EventTarget {
} }
} = {} as any } = {} as any
constructor(pool?: string) { constructor(settings?: SettingsViaQueryString) {
super() super()
this.engineConnection = undefined this.engineConnection = undefined
this.pool = pool this.settings = settings
? settings
: {
pool: null,
theme: Themes.Dark,
highlightEdges: true,
enableSSAO: true,
showScaleGrid: false,
}
} }
private _camControlsCameraChange = () => {} private _camControlsCameraChange = () => {}
@ -1214,7 +1373,6 @@ export class EngineCommandManager extends EventTarget {
private onEngineConnectionNewTrack = ({ private onEngineConnectionNewTrack = ({
detail, detail,
}: CustomEvent<NewTrackArgs>) => {} }: CustomEvent<NewTrackArgs>) => {}
disableWebRTC = false
modelingSend: ReturnType<typeof useModelingContext>['send'] = modelingSend: ReturnType<typeof useModelingContext>['send'] =
(() => {}) as any (() => {}) as any
@ -1227,40 +1385,38 @@ export class EngineCommandManager extends EventTarget {
} }
start({ start({
disableWebRTC = false,
setMediaStream, setMediaStream,
setIsStreamReady, setIsStreamReady,
width, width,
height, height,
executeCode,
token, token,
makeDefaultPlanes, makeDefaultPlanes,
modifyGrid, modifyGrid,
settings = { settings = {
pool: null,
theme: Themes.Dark, theme: Themes.Dark,
highlightEdges: true, highlightEdges: true,
enableSSAO: true, enableSSAO: true,
showScaleGrid: false, showScaleGrid: false,
}, },
// When passed, use a completely separate connecting code path that simply
// opens a websocket and this is a function that is called when connected.
callbackOnEngineLiteConnect,
}: { }: {
disableWebRTC?: boolean callbackOnEngineLiteConnect?: () => void
setMediaStream: (stream: MediaStream) => void setMediaStream: (stream: MediaStream) => void
setIsStreamReady: (isStreamReady: boolean) => void setIsStreamReady: (isStreamReady: boolean) => void
width: number width: number
height: number height: number
executeCode: () => void
token?: string token?: string
makeDefaultPlanes: () => Promise<DefaultPlanes> makeDefaultPlanes: () => Promise<DefaultPlanes>
modifyGrid: (hidden: boolean) => Promise<void> modifyGrid: (hidden: boolean) => Promise<void>
settings?: { settings?: SettingsViaQueryString
theme: Themes
highlightEdges: boolean
enableSSAO: boolean
showScaleGrid: boolean
}
}) { }) {
if (settings) {
this.settings = settings
}
this.makeDefaultPlanes = makeDefaultPlanes this.makeDefaultPlanes = makeDefaultPlanes
this.disableWebRTC = disableWebRTC
this.modifyGrid = modifyGrid this.modifyGrid = modifyGrid
if (width === 0 || height === 0) { if (width === 0 || height === 0) {
return return
@ -1275,22 +1431,28 @@ export class EngineCommandManager extends EventTarget {
return return
} }
const additionalSettings = settings.enableSSAO ? '&post_effect=ssao' : '' const additionalSettings = this.settings.enableSSAO
const pool = this.pool === undefined ? '' : `&pool=${this.pool}` ? '&post_effect=ssao'
: ''
const pool = !this.settings.pool ? '' : `&pool=${this.settings.pool}`
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}${pool}` const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}${pool}`
this.engineConnection = new EngineConnection({ this.engineConnection = new EngineConnection({
engineCommandManager: this, engineCommandManager: this,
url, url,
token, token,
callbackOnEngineLiteConnect,
}) })
// Nothing more to do when using a lite engine initialization
if (callbackOnEngineLiteConnect) return
this.dispatchEvent( this.dispatchEvent(
new CustomEvent(EngineCommandManagerEvents.EngineAvailable, { new CustomEvent(EngineCommandManagerEvents.EngineAvailable, {
detail: this.engineConnection, detail: this.engineConnection,
}) })
) )
this.onEngineConnectionOpened = () => { this.onEngineConnectionOpened = async () => {
// Set the stream background color // Set the stream background color
// This takes RGBA values from 0-1 // This takes RGBA values from 0-1
// So we convert from the conventional 0-255 found in Figma // So we convert from the conventional 0-255 found in Figma
@ -1300,12 +1462,12 @@ export class EngineCommandManager extends EventTarget {
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { cmd: {
type: 'set_background_color', type: 'set_background_color',
color: getThemeColorForEngine(settings.theme), color: getThemeColorForEngine(this.settings.theme),
}, },
}) })
// Sets the default line colors // Sets the default line colors
const opposingTheme = getOppositeTheme(settings.theme) const opposingTheme = getOppositeTheme(this.settings.theme)
this.sendSceneCommand({ this.sendSceneCommand({
cmd_id: uuidv4(), cmd_id: uuidv4(),
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -1321,7 +1483,7 @@ export class EngineCommandManager extends EventTarget {
cmd_id: uuidv4(), cmd_id: uuidv4(),
cmd: { cmd: {
type: 'edge_lines_visible' as any, // TODO: update kittycad.ts to use the correct type type: 'edge_lines_visible' as any, // TODO: update kittycad.ts to use the correct type
hidden: !settings.highlightEdges, hidden: !this.settings.highlightEdges,
}, },
}) })
@ -1338,13 +1500,19 @@ export class EngineCommandManager extends EventTarget {
// We want modify the grid first because we don't want it to flash. // We want modify the grid first because we don't want it to flash.
// Ideally these would already be default hidden in engine (TODO do // Ideally these would already be default hidden in engine (TODO do
// that) https://github.com/KittyCAD/engine/issues/2282 // that) https://github.com/KittyCAD/engine/issues/2282
this.modifyGrid(!settings.showScaleGrid)?.then(async () => { this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
await this.initPlanes() await this.initPlanes()
this.resolveReady()
setIsStreamReady(true) setIsStreamReady(true)
await executeCode()
// Other parts of the application should use this to react on scene ready.
this.dispatchEvent(
new CustomEvent(EngineCommandManagerEvents.SceneReady, {
detail: this.engineConnection,
})
)
}) })
} }
this.engineConnection.addEventListener( this.engineConnection.addEventListener(
EngineConnectionEvents.Opened, EngineConnectionEvents.Opened,
this.onEngineConnectionOpened this.onEngineConnectionOpened
@ -1541,6 +1709,8 @@ export class EngineCommandManager extends EventTarget {
EngineConnectionEvents.ConnectionStarted, EngineConnectionEvents.ConnectionStarted,
this.onEngineConnectionStarted this.onEngineConnectionStarted
) )
return
} }
handleResize({ handleResize({
@ -1569,25 +1739,28 @@ export class EngineCommandManager extends EventTarget {
tearDown(opts?: { idleMode: boolean }) { tearDown(opts?: { idleMode: boolean }) {
if (this.engineConnection) { if (this.engineConnection) {
this.engineConnection.removeEventListener( for (const pending of Object.values(this.pendingCommands)) {
pending.reject('no connection to send on')
}
this.engineConnection?.removeEventListener?.(
EngineConnectionEvents.Opened, EngineConnectionEvents.Opened,
this.onEngineConnectionOpened this.onEngineConnectionOpened
) )
this.engineConnection.removeEventListener( this.engineConnection.removeEventListener?.(
EngineConnectionEvents.Closed, EngineConnectionEvents.Closed,
this.onEngineConnectionClosed this.onEngineConnectionClosed
) )
this.engineConnection.removeEventListener( this.engineConnection.removeEventListener?.(
EngineConnectionEvents.ConnectionStarted, EngineConnectionEvents.ConnectionStarted,
this.onEngineConnectionStarted this.onEngineConnectionStarted
) )
this.engineConnection.removeEventListener( this.engineConnection.removeEventListener?.(
EngineConnectionEvents.NewTrack, EngineConnectionEvents.NewTrack,
this.onEngineConnectionNewTrack as EventListener this.onEngineConnectionNewTrack as EventListener
) )
this.engineConnection?.tearDown(opts) this.engineConnection?.tearDown(opts)
this.engineConnection = undefined
// Our window.tearDown assignment causes this case to happen which is // Our window.tearDown assignment causes this case to happen which is
// only really for tests. // only really for tests.
@ -1769,17 +1942,17 @@ export class EngineCommandManager extends EventTarget {
commandStr: string, commandStr: string,
idToRangeStr: string idToRangeStr: string
): Promise<string | void> { ): Promise<string | void> {
if (this.engineConnection === undefined) { if (this.engineConnection === undefined) return Promise.resolve()
return Promise.resolve() if (
} !this.engineConnection?.isReady() &&
if (!this.engineConnection?.isReady() && !this.disableWebRTC) !this.engineConnection.isUsingConnectionLite
)
return Promise.resolve() return Promise.resolve()
if (id === undefined) return Promise.reject(new Error('id is undefined')) if (id === undefined) return Promise.reject(new Error('id is undefined'))
if (rangeStr === undefined) if (rangeStr === undefined)
return Promise.reject(new Error('rangeStr is undefined')) return Promise.reject(new Error('rangeStr is undefined'))
if (commandStr === undefined) { if (commandStr === undefined)
return Promise.reject(new Error('commandStr is undefined')) return Promise.reject(new Error('commandStr is undefined'))
}
const range: SourceRange = JSON.parse(rangeStr) const range: SourceRange = JSON.parse(rangeStr)
const command: EngineCommand = JSON.parse(commandStr) const command: EngineCommand = JSON.parse(commandStr)
const idToRangeMap: { [key: string]: SourceRange } = const idToRangeMap: { [key: string]: SourceRange } =
@ -1874,6 +2047,17 @@ export class EngineCommandManager extends EventTarget {
} }
async setPlaneHidden(id: string, hidden: boolean) { async setPlaneHidden(id: string, hidden: boolean) {
if (this.engineConnection === undefined) return
// Can't send commands if there's no connection
if (
this.engineConnection.state.type ===
EngineConnectionStateType.Disconnecting ||
this.engineConnection.state.type ===
EngineConnectionStateType.Disconnected
)
return
return await this.sendSceneCommand({ return await this.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),

View File

@ -63,7 +63,7 @@ export class CoreDumpManager {
// Get the backend pool we've requested. // Get the backend pool we've requested.
pool(): string { pool(): string {
return this.engineCommandManager.pool || '' return this.engineCommandManager.settings.pool || ''
} }
// Get the os information. // Get the os information.

View File

@ -104,8 +104,12 @@ export const fileLoader: LoaderFunction = async ({
// the file system and not the editor. // the file system and not the editor.
codeManager.updateCurrentFilePath(current_file_path) codeManager.updateCurrentFilePath(current_file_path)
codeManager.updateCodeStateEditor(code) codeManager.updateCodeStateEditor(code)
// We don't want to call await on execute code since we don't want to block the UI // We don't want to call await on execute code since we don't want to block the UI
kclManager.executeCode(true) kclManager.isFirstRender = true
kclManager.executeCode(true).then(() => {
kclManager.isFirstRender = false
})
// Set the file system manager to the project path // Set the file system manager to the project path
// So that WASM gets an updated path for operations // So that WASM gets an updated path for operations

View File

@ -2,6 +2,15 @@ import { type Models } from '@kittycad/lib'
import { Setting, settings } from './initialSettings' import { Setting, settings } from './initialSettings'
import { AtLeast, PathValue, Paths } from 'lib/types' import { AtLeast, PathValue, Paths } from 'lib/types'
import { CommandArgumentConfig } from 'lib/commandTypes' import { CommandArgumentConfig } from 'lib/commandTypes'
import { Themes } from 'lib/theme'
export interface SettingsViaQueryString {
pool: string | null
theme: Themes
highlightEdges: boolean
enableSSAO: boolean
showScaleGrid: boolean
}
export enum UnitSystem { export enum UnitSystem {
Imperial = 'imperial', Imperial = 'imperial',

View File

@ -1,5 +1,8 @@
import { Program, ProgramMemory, _executor, SourceRange } from '../lang/wasm' import { Program, ProgramMemory, _executor, SourceRange } from '../lang/wasm'
import { EngineCommandManager } from 'lang/std/engineConnection' import {
EngineCommandManager,
EngineCommandManagerEvents,
} from 'lang/std/engineConnection'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
@ -82,7 +85,6 @@ export async function enginelessExecutor(
setIsStreamReady: () => {}, setIsStreamReady: () => {},
setMediaStream: () => {}, setMediaStream: () => {},
}) as any as EngineCommandManager }) as any as EngineCommandManager
await mockEngineCommandManager.waitForReady
mockEngineCommandManager.startNewSession() mockEngineCommandManager.startNewSession()
const programMemory = await _executor(ast, pm, mockEngineCommandManager, true) const programMemory = await _executor(ast, pm, mockEngineCommandManager, true)
await mockEngineCommandManager.waitForAllCommands() await mockEngineCommandManager.waitForAllCommands()
@ -99,7 +101,6 @@ export async function executor(
setMediaStream: () => {}, setMediaStream: () => {},
width: 0, width: 0,
height: 0, height: 0,
executeCode: () => {},
makeDefaultPlanes: () => { makeDefaultPlanes: () => {
return new Promise((resolve) => resolve(defaultPlanes)) return new Promise((resolve) => resolve(defaultPlanes))
}, },
@ -107,9 +108,21 @@ export async function executor(
return new Promise((resolve) => resolve()) return new Promise((resolve) => resolve())
}, },
}) })
await engineCommandManager.waitForReady
return new Promise((resolve) => {
engineCommandManager.addEventListener(
EngineCommandManagerEvents.SceneReady,
async () => {
engineCommandManager.startNewSession() engineCommandManager.startNewSession()
const programMemory = await _executor(ast, pm, engineCommandManager, false) const programMemory = await _executor(
ast,
pm,
engineCommandManager,
false
)
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()
return programMemory Promise.resolve(programMemory)
}
)
})
} }