Compare commits
	
		
			3 Commits
		
	
	
		
			v0.48.0
			...
			franknoiro
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 99c62bf0f0 | |||
| ee20d4781f | |||
| 1e80738b2a | 
| @ -1,5 +1,4 @@ | ||||
| import { useState, useEffect } from 'react' | ||||
| import { EngineCommandManagerEvents } from 'lang/std/engineConnection' | ||||
| import { engineCommandManager, sceneInfra } from 'lib/singletons' | ||||
| import { throttle, isReducedMotion } from 'lib/utils' | ||||
|  | ||||
| @ -14,12 +13,9 @@ export const CamToggle = () => { | ||||
|   const [enableRotate, setEnableRotate] = useState(true) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     engineCommandManager.addEventListener( | ||||
|       EngineCommandManagerEvents.SceneReady, | ||||
|       async () => { | ||||
|     engineCommandManager.waitForReady.then(async () => { | ||||
|       sceneInfra.camControls.dollyZoom(fov) | ||||
|       } | ||||
|     ) | ||||
|     }) | ||||
|   }, []) | ||||
|  | ||||
|   const toggleCamera = () => { | ||||
|  | ||||
| @ -176,7 +176,6 @@ const FileTreeItem = ({ | ||||
|       ) | ||||
|       codeManager.writeToFile() | ||||
|  | ||||
|       // Prevent seeing the model built one piece at a time when changing files | ||||
|       kclManager.isFirstRender = true | ||||
|       kclManager.executeCode(true).then(() => { | ||||
|         kclManager.isFirstRender = false | ||||
|  | ||||
| @ -47,7 +47,7 @@ const Loading = ({ children }: React.PropsWithChildren) => { | ||||
|         onConnectionStateChange as EventListener | ||||
|       ) | ||||
|     } | ||||
|   }, [engineCommandManager, engineCommandManager.engineConnection]) | ||||
|   }, []) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     // Don't set long loading time if there's a more severe error | ||||
|  | ||||
| @ -78,12 +78,7 @@ import { err, trap } from 'lib/trap' | ||||
| import { useCommandsContext } from 'hooks/useCommandsContext' | ||||
| import { modelingMachineEvent } from 'editor/manager' | ||||
| import { hasValidFilletSelection } from 'lang/modifyAst/addFillet' | ||||
| import { | ||||
|   ExportIntent, | ||||
|   EngineConnectionState, | ||||
|   EngineConnectionStateType, | ||||
|   EngineConnectionEvents, | ||||
| } from 'lang/std/engineConnection' | ||||
| import { ExportIntent } from 'lang/std/engineConnection' | ||||
|  | ||||
| type MachineContext<T extends AnyStateMachine> = { | ||||
|   state: StateFrom<T> | ||||
| @ -159,10 +154,7 @@ export const ModelingMachineProvider = ({ | ||||
|             sceneInfra.camControls.syncDirection = 'engineToClient' | ||||
|  | ||||
|             store.videoElement?.pause() | ||||
|  | ||||
|             kclManager.isFirstRender = true | ||||
|             kclManager.executeCode().then(() => { | ||||
|               kclManager.isFirstRender = false | ||||
|               if (engineCommandManager.engineConnection?.idleMode) return | ||||
|  | ||||
|               store.videoElement?.play().catch((e) => { | ||||
| @ -917,19 +909,15 @@ export const ModelingMachineProvider = ({ | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   useSetupEngineManager( | ||||
|     streamRef, | ||||
|     modelingSend, | ||||
|     modelingState.context, | ||||
|     { | ||||
|   useSetupEngineManager(streamRef, token, { | ||||
|     pool: pool, | ||||
|     theme: theme.current, | ||||
|     highlightEdges: highlightEdges.current, | ||||
|     enableSSAO: enableSSAO.current, | ||||
|     modelingSend, | ||||
|     modelingContext: modelingState.context, | ||||
|     showScaleGrid: showScaleGrid.current, | ||||
|     }, | ||||
|     token | ||||
|   ) | ||||
|   }) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     kclManager.registerExecuteCallback(() => { | ||||
| @ -957,25 +945,17 @@ export const ModelingMachineProvider = ({ | ||||
|   }, [modelingState.context.selectionRanges]) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const onConnectionStateChanged = ({ detail }: CustomEvent) => { | ||||
|     const offlineCallback = () => { | ||||
|       // 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 | ||||
|       // this then. | ||||
|       if (detail.type === EngineConnectionStateType.Disconnecting) { | ||||
|       modelingSend({ type: 'Cancel' }) | ||||
|     } | ||||
|     } | ||||
|     engineCommandManager.engineConnection?.addEventListener( | ||||
|       EngineConnectionEvents.ConnectionStateChanged, | ||||
|       onConnectionStateChanged as EventListener | ||||
|     ) | ||||
|     window.addEventListener('offline', offlineCallback) | ||||
|     return () => { | ||||
|       engineCommandManager.engineConnection?.removeEventListener( | ||||
|         EngineConnectionEvents.ConnectionStateChanged, | ||||
|         onConnectionStateChanged as EventListener | ||||
|       ) | ||||
|       window.removeEventListener('offline', offlineCallback) | ||||
|     } | ||||
|   }, [engineCommandManager.engineConnection, modelingSend]) | ||||
|   }, [modelingSend]) | ||||
|  | ||||
|   // Allow using the delete key to delete solids | ||||
|   useHotkeys(['backspace', 'delete', 'del'], () => { | ||||
|  | ||||
| @ -64,6 +64,13 @@ export const KclEditorPane = () => { | ||||
|       : context.app.theme.current | ||||
|   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 | ||||
|   // with the wrapper. | ||||
|   useHotkeys('mod+z', (e) => { | ||||
|  | ||||
| @ -191,7 +191,6 @@ export const SettingsAuthProviderBase = ({ | ||||
|               allSettingsIncludesUnitChange || | ||||
|               resetSettingsIncludesUnitChange | ||||
|             ) { | ||||
|               // Unit changes requires a re-exec of code | ||||
|               kclManager.isFirstRender = true | ||||
|               kclManager.executeCode(true).then(() => { | ||||
|                 kclManager.isFirstRender = false | ||||
|  | ||||
| @ -11,27 +11,21 @@ import { sendSelectEventToEngine } from 'lib/selections' | ||||
| import { kclManager, engineCommandManager, sceneInfra } from 'lib/singletons' | ||||
| import { useAppStream } from 'AppState' | ||||
| import { | ||||
|   EngineCommandManagerEvents, | ||||
|   EngineConnectionStateType, | ||||
|   DisconnectingType, | ||||
| } from 'lang/std/engineConnection' | ||||
|  | ||||
| enum StreamState { | ||||
|   Playing = 'playing', | ||||
|   Paused = 'paused', | ||||
|   Resuming = 'resuming', | ||||
|   Unset = 'unset', | ||||
| } | ||||
|  | ||||
| export const Stream = () => { | ||||
|   const [isLoading, setIsLoading] = useState(true) | ||||
|   const [isFirstRender, setIsFirstRender] = useState(kclManager.isFirstRender) | ||||
|   const [clickCoords, setClickCoords] = useState<{ x: number; y: number }>() | ||||
|   const videoRef = useRef<HTMLVideoElement>(null) | ||||
|   const { settings } = useSettingsAuthContext() | ||||
|   const { state, send, context } = useModelingContext() | ||||
|   const { mediaStream } = useAppStream() | ||||
|   const { overallState, immediateState } = useNetworkContext() | ||||
|   const [streamState, setStreamState] = useState(StreamState.Unset) | ||||
|   const [isFreezeFrame, setIsFreezeFrame] = useState(false) | ||||
|   const [isPaused, setIsPaused] = useState(false) | ||||
|  | ||||
|   const IDLE = settings.context.app.streamIdleMode.current | ||||
|  | ||||
| @ -44,7 +38,10 @@ export const Stream = () => { | ||||
|       immediateState.type === EngineConnectionStateType.Disconnecting && | ||||
|       immediateState.value.type === DisconnectingType.Pause | ||||
|     ) { | ||||
|       setStreamState(StreamState.Paused) | ||||
|       setIsPaused(true) | ||||
|     } | ||||
|     if (immediateState.type === EngineConnectionStateType.Connecting) { | ||||
|       setIsPaused(false) | ||||
|     } | ||||
|   }, [immediateState]) | ||||
|  | ||||
| @ -79,11 +76,8 @@ export const Stream = () => { | ||||
|     let timeoutIdIdleA: ReturnType<typeof setTimeout> | undefined = undefined | ||||
|  | ||||
|     const teardown = () => { | ||||
|       // Already paused | ||||
|       if (streamState === StreamState.Paused) return | ||||
|  | ||||
|       videoRef.current?.pause() | ||||
|       setStreamState(StreamState.Paused) | ||||
|       setIsFreezeFrame(true) | ||||
|       sceneInfra.modelingSend({ type: 'Cancel' }) | ||||
|       // Give video time to pause | ||||
|       window.requestAnimationFrame(() => { | ||||
| @ -97,7 +91,7 @@ export const Stream = () => { | ||||
|         timeoutIdIdleA = setTimeout(teardown, IDLE_TIME_MS) | ||||
|       } else if (!engineCommandManager.engineConnection?.isReady()) { | ||||
|         clearTimeout(timeoutIdIdleA) | ||||
|         setStreamState(StreamState.Resuming) | ||||
|         engineCommandManager.engineConnection?.connect(true) | ||||
|       } | ||||
|     } | ||||
|  | ||||
| @ -112,16 +106,11 @@ export const Stream = () => { | ||||
|     let timeoutIdIdleB: ReturnType<typeof setTimeout> | undefined = undefined | ||||
|  | ||||
|     const onAnyInput = () => { | ||||
|       if (streamState === StreamState.Playing) { | ||||
|       // Clear both timers | ||||
|       clearTimeout(timeoutIdIdleA) | ||||
|       clearTimeout(timeoutIdIdleB) | ||||
|       timeoutIdIdleB = setTimeout(teardown, IDLE_TIME_MS) | ||||
|     } | ||||
|       if (streamState === StreamState.Paused) { | ||||
|         setStreamState(StreamState.Resuming) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (IDLE) { | ||||
|       globalThis?.window?.document?.addEventListener('keydown', onAnyInput) | ||||
| @ -135,27 +124,7 @@ export const Stream = () => { | ||||
|       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 () => { | ||||
|       engineCommandManager.removeEventListener( | ||||
|         EngineCommandManagerEvents.SceneReady, | ||||
|         onSceneReady | ||||
|       ) | ||||
|       globalThis?.window?.document?.removeEventListener('paste', handlePaste, { | ||||
|         capture: true, | ||||
|       }) | ||||
| @ -183,11 +152,10 @@ export const Stream = () => { | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
|   }, [IDLE, streamState]) | ||||
|   }, [IDLE]) | ||||
|  | ||||
|   // HOT FIX: for https://github.com/KittyCAD/modeling-app/pull/3250 | ||||
|   // TODO review if there's a better way to play the stream again. | ||||
|   useEffect(() => { | ||||
|     setIsFirstRender(kclManager.isFirstRender) | ||||
|     if (!kclManager.isFirstRender) | ||||
|       setTimeout(() => | ||||
|         // execute in the next event loop | ||||
| @ -195,6 +163,7 @@ export const Stream = () => { | ||||
|           console.warn('Video playing was prevented', e, videoRef.current) | ||||
|         }) | ||||
|       ) | ||||
|     setIsFreezeFrame(!kclManager.isFirstRender) | ||||
|   }, [kclManager.isFirstRender]) | ||||
|  | ||||
|   useEffect(() => { | ||||
| @ -319,8 +288,7 @@ export const Stream = () => { | ||||
|       <ClientSideScene | ||||
|         cameraControls={settings.context.modeling.mouseControls.current} | ||||
|       /> | ||||
|       {(streamState === StreamState.Paused || | ||||
|         streamState === StreamState.Resuming) && ( | ||||
|       {isPaused && ( | ||||
|         <div className="text-center absolute inset-0"> | ||||
|           <div | ||||
|             className="flex flex-col items-center justify-center h-screen" | ||||
| @ -342,19 +310,16 @@ export const Stream = () => { | ||||
|                 /> | ||||
|               </svg> | ||||
|             </div> | ||||
|             <p className="text-base mt-2 text-primary bold"> | ||||
|               {streamState === StreamState.Paused && 'Paused'} | ||||
|               {streamState === StreamState.Resuming && 'Resuming'} | ||||
|             </p> | ||||
|             <p className="text-base mt-2 text-primary bold">Paused</p> | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
|       {(!isNetworkOkay || isLoading || kclManager.isFirstRender) && ( | ||||
|       {(!isNetworkOkay || isLoading || isFirstRender) && !isFreezeFrame && ( | ||||
|         <div className="text-center absolute inset-0"> | ||||
|           <Loading> | ||||
|             {!isNetworkOkay && !isLoading && !kclManager.isFirstRender ? ( | ||||
|             {!isNetworkOkay && !isLoading ? ( | ||||
|               <span data-testid="loading-stream">Stream disconnected...</span> | ||||
|             ) : !isLoading && kclManager.isFirstRender ? ( | ||||
|             ) : !isLoading && isFirstRender ? ( | ||||
|               <span data-testid="loading-stream">Building scene...</span> | ||||
|             ) : ( | ||||
|               <span data-testid="loading-stream">Loading stream...</span> | ||||
|  | ||||
| @ -4,30 +4,29 @@ import { deferExecution } from 'lib/utils' | ||||
| import { Themes } from 'lib/theme' | ||||
| import { makeDefaultPlanes, modifyGrid } from 'lang/wasm' | ||||
| import { useModelingContext } from './useModelingContext' | ||||
| import { useNetworkContext } from 'hooks/useNetworkContext' | ||||
| import { useAppState, useAppStream } from 'AppState' | ||||
| import { SettingsViaQueryString } from 'lib/settings/settingsTypes' | ||||
| import { | ||||
|   EngineConnectionStateType, | ||||
|   EngineConnectionEvents, | ||||
|   DisconnectingType, | ||||
| } from 'lang/std/engineConnection' | ||||
|  | ||||
| export function useSetupEngineManager( | ||||
|   streamRef: React.RefObject<HTMLDivElement>, | ||||
|   modelingSend: ReturnType<typeof useModelingContext>['send'], | ||||
|   modelingContext: ReturnType<typeof useModelingContext>['context'], | ||||
|   token?: string, | ||||
|   settings = { | ||||
|     pool: null, | ||||
|     theme: Themes.System, | ||||
|     highlightEdges: true, | ||||
|     enableSSAO: true, | ||||
|     modelingSend: (() => {}) as any, | ||||
|     modelingContext: {} as any, | ||||
|     showScaleGrid: false, | ||||
|   } as SettingsViaQueryString, | ||||
|   token?: string | ||||
|   } as { | ||||
|     pool: string | null | ||||
|     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 { setMediaStream } = useAppStream() | ||||
|  | ||||
| @ -36,10 +35,10 @@ export function useSetupEngineManager( | ||||
|   if (settings.pool) { | ||||
|     // override the pool param (?pool=) to request a specific engine instance | ||||
|     // from a particular pool. | ||||
|     engineCommandManager.settings.pool = settings.pool | ||||
|     engineCommandManager.pool = settings.pool | ||||
|   } | ||||
|  | ||||
|   const startEngineInstance = () => { | ||||
|   const startEngineInstance = (restart: boolean = false) => { | ||||
|     // Load the engine command manager once with the initial width and height, | ||||
|     // then we do not want to reload it. | ||||
|     const { width: quadWidth, height: quadHeight } = getDimensions( | ||||
| @ -51,6 +50,14 @@ export function useSetupEngineManager( | ||||
|       setIsStreamReady: (isStreamReady) => setAppState({ isStreamReady }), | ||||
|       width: quadWidth, | ||||
|       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, | ||||
|       settings, | ||||
|       makeDefaultPlanes: () => { | ||||
| @ -60,7 +67,7 @@ export function useSetupEngineManager( | ||||
|         return modifyGrid(kclManager.engineCommandManager, hidden) | ||||
|       }, | ||||
|     }) | ||||
|     modelingSend({ | ||||
|     settings.modelingSend({ | ||||
|       type: 'Set context', | ||||
|       data: { | ||||
|         streamDimensions: { | ||||
| @ -83,27 +90,9 @@ export function useSetupEngineManager( | ||||
|   }, [ | ||||
|     streamRef?.current?.offsetWidth, | ||||
|     streamRef?.current?.offsetHeight, | ||||
|     modelingSend, | ||||
|     settings.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(() => { | ||||
|     const handleResize = deferExecution(() => { | ||||
|       const { width, height } = getDimensions( | ||||
| @ -111,14 +100,14 @@ export function useSetupEngineManager( | ||||
|         streamRef?.current?.offsetHeight ?? 0 | ||||
|       ) | ||||
|       if ( | ||||
|         modelingContext.store.streamDimensions.streamWidth !== width || | ||||
|         modelingContext.store.streamDimensions.streamHeight !== height | ||||
|         settings.modelingContext.store.streamDimensions.streamWidth !== width || | ||||
|         settings.modelingContext.store.streamDimensions.streamHeight !== height | ||||
|       ) { | ||||
|         engineCommandManager.handleResize({ | ||||
|           streamWidth: width, | ||||
|           streamHeight: height, | ||||
|         }) | ||||
|         modelingSend({ | ||||
|         settings.modelingSend({ | ||||
|           type: 'Set context', | ||||
|           data: { | ||||
|             streamDimensions: { | ||||
| @ -131,7 +120,7 @@ export function useSetupEngineManager( | ||||
|     }, 500) | ||||
|  | ||||
|     const onOnline = () => { | ||||
|       startEngineInstance() | ||||
|       startEngineInstance(true) | ||||
|     } | ||||
|  | ||||
|     const onVisibilityChange = () => { | ||||
| @ -147,18 +136,10 @@ export function useSetupEngineManager( | ||||
|     window.document.addEventListener('visibilitychange', onVisibilityChange) | ||||
|  | ||||
|     const onAnyInput = () => { | ||||
|       const isEngineNotReadyOrConnecting = | ||||
|       if ( | ||||
|         !engineCommandManager.engineConnection?.isReady() && | ||||
|         !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() | ||||
|       } | ||||
|     } | ||||
| @ -169,6 +150,7 @@ export function useSetupEngineManager( | ||||
|     window.document.addEventListener('touchstart', onAnyInput) | ||||
|  | ||||
|     const onOffline = () => { | ||||
|       kclManager.isFirstRender = true | ||||
|       engineCommandManager.tearDown() | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -211,6 +211,7 @@ export class KclManager { | ||||
|       type: string | ||||
|     } | ||||
|   ): Promise<void> { | ||||
|     await this?.engineCommandManager?.waitForReady | ||||
|     const currentExecutionId = executionId || Date.now() | ||||
|     this._cancelTokens.set(currentExecutionId, false) | ||||
|  | ||||
| @ -300,6 +301,7 @@ export class KclManager { | ||||
|     codeManager.updateCodeEditor(newCode) | ||||
|     // Write the file to disk. | ||||
|     await codeManager.writeToFile() | ||||
|     await this?.engineCommandManager?.waitForReady | ||||
|     this._ast = { ...newAst } | ||||
|  | ||||
|     const { logs, errors, programMemory } = await executeAst({ | ||||
|  | ||||
| @ -14,10 +14,6 @@ import { | ||||
| } from './artifactGraph' | ||||
| import { err } from 'lib/trap' | ||||
| import { engineCommandManager, kclManager } from 'lib/singletons' | ||||
| import { | ||||
|   EngineCommandManagerEvents, | ||||
|   EngineConnectionEvents, | ||||
| } from 'lang/std/engineConnection' | ||||
| import { CI, VITE_KC_DEV_TOKEN } from 'env' | ||||
| import fsp from 'fs/promises' | ||||
| import fs from 'fs' | ||||
| @ -117,18 +113,20 @@ beforeAll(async () => { | ||||
|   } | ||||
|  | ||||
|   // THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local | ||||
|   await new Promise((resolve) => { | ||||
|   engineCommandManager.start({ | ||||
|       // disableWebRTC: true, | ||||
|     disableWebRTC: true, | ||||
|     token: VITE_KC_DEV_TOKEN, | ||||
|     // there does seem to be a minimum resolution, not sure what it is but 256 works ok. | ||||
|     width: 256, | ||||
|     height: 256, | ||||
|     executeCode: () => {}, | ||||
|     makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager), | ||||
|     setMediaStream: () => {}, | ||||
|     setIsStreamReady: () => {}, | ||||
|     modifyGrid: async () => {}, | ||||
|       callbackOnEngineLiteConnect: async () => { | ||||
|   }) | ||||
|   await engineCommandManager.waitForReady | ||||
|  | ||||
|   const cacheEntries = Object.entries(codeToWriteCacheFor) as [ | ||||
|     CodeKey, | ||||
|     string | ||||
| @ -138,9 +136,9 @@ beforeAll(async () => { | ||||
|     const ast = parse(code) | ||||
|     if (err(ast)) { | ||||
|       console.error(ast) | ||||
|             return Promise.reject(ast) | ||||
|       throw ast | ||||
|     } | ||||
|           const result = await kclManager.executeAst(ast) | ||||
|     await kclManager.executeAst(ast) | ||||
|  | ||||
|     cacheToWriteToFileTemp[codeKey] = { | ||||
|       orderedCommands: engineCommandManager.orderedCommands, | ||||
| @ -151,10 +149,6 @@ beforeAll(async () => { | ||||
|  | ||||
|   await fsp.mkdir(pathStart, { recursive: true }) | ||||
|   await fsp.writeFile(fullPath, cache) | ||||
|         resolve(true) | ||||
|       }, | ||||
|     }) | ||||
|   }) | ||||
| }, 20_000) | ||||
|  | ||||
| afterAll(() => { | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { Program, SourceRange } from 'lang/wasm' | ||||
| import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from 'env' | ||||
| import { VITE_KC_API_WS_MODELING_URL } from 'env' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { exportSave } from 'lib/exportSave' | ||||
| import { deferExecution, isOverlap, uuidv4 } from 'lib/utils' | ||||
| @ -15,10 +15,9 @@ import { | ||||
| import { useModelingContext } from 'hooks/useModelingContext' | ||||
| import { exportMake } from 'lib/exportMake' | ||||
| import toast from 'react-hot-toast' | ||||
| import { SettingsViaQueryString } from 'lib/settings/settingsTypes' | ||||
|  | ||||
| // TODO(paultag): This ought to be tweakable. | ||||
| const pingIntervalMs = 1000 | ||||
| const pingIntervalMs = 10000 | ||||
|  | ||||
| function isHighlightSetEntity_type( | ||||
|   data: any | ||||
| @ -229,7 +228,6 @@ class EngineConnection extends EventTarget { | ||||
|   unreliableDataChannel?: RTCDataChannel | ||||
|   mediaStream?: MediaStream | ||||
|   idleMode: boolean = false | ||||
|   promise?: Promise<void> | ||||
|  | ||||
|   onIceCandidate = function ( | ||||
|     this: RTCPeerConnection, | ||||
| @ -297,33 +295,24 @@ class EngineConnection extends EventTarget { | ||||
|   private engineCommandManager: EngineCommandManager | ||||
|  | ||||
|   private pingPongSpan: { ping?: Date; pong?: Date } | ||||
|   private pingIntervalId: ReturnType<typeof setInterval> = setInterval(() => {}, | ||||
|   60_000) | ||||
|   isUsingConnectionLite: boolean = false | ||||
|   private pingIntervalId: ReturnType<typeof setInterval> | ||||
|  | ||||
|   constructor({ | ||||
|     engineCommandManager, | ||||
|     url, | ||||
|     token, | ||||
|     callbackOnEngineLiteConnect, | ||||
|   }: { | ||||
|     engineCommandManager: EngineCommandManager | ||||
|     url: string | ||||
|     token?: string | ||||
|     callbackOnEngineLiteConnect?: () => void | ||||
|   }) { | ||||
|     super() | ||||
|  | ||||
|     this.engineCommandManager = engineCommandManager | ||||
|     this.url = url | ||||
|     this.token = token | ||||
|     this.pingPongSpan = { ping: undefined, pong: undefined } | ||||
|  | ||||
|     if (callbackOnEngineLiteConnect) { | ||||
|       this.connectLite(callbackOnEngineLiteConnect) | ||||
|       this.isUsingConnectionLite = true | ||||
|       return | ||||
|     } | ||||
|     this.pingPongSpan = { ping: undefined, pong: undefined } | ||||
|  | ||||
|     // Without an interval ping, our connection will timeout. | ||||
|     // If this.idleMode is true we skip this logic so only reconnect | ||||
| @ -333,22 +322,13 @@ class EngineConnection extends EventTarget { | ||||
|  | ||||
|       switch (this.state.type as EngineConnectionStateType) { | ||||
|         case EngineConnectionStateType.ConnectionEstablished: | ||||
|           // If there was no reply to the last ping, report a timeout and | ||||
|           // teardown the connection. | ||||
|           // If there was no reply to the last ping, report a timeout. | ||||
|           if (this.pingPongSpan.ping && !this.pingPongSpan.pong) { | ||||
|             this.dispatchEvent( | ||||
|               new CustomEvent(EngineConnectionEvents.PingPongChanged, { | ||||
|                 detail: 'TIMEOUT', | ||||
|               }) | ||||
|             ) | ||||
|             this.state = { | ||||
|               type: EngineConnectionStateType.Disconnecting, | ||||
|               value: { | ||||
|                 type: DisconnectingType.Timeout, | ||||
|               }, | ||||
|             } | ||||
|             this.disconnectAll() | ||||
|  | ||||
|             // Otherwise check the time between was >= pingIntervalMs, | ||||
|             // and if it was, then it's bad network health. | ||||
|           } else if (this.pingPongSpan.ping && this.pingPongSpan.pong) { | ||||
| @ -378,15 +358,13 @@ class EngineConnection extends EventTarget { | ||||
|           break | ||||
|         case EngineConnectionStateType.Disconnecting: | ||||
|         case EngineConnectionStateType.Disconnected: | ||||
|           // We will do reconnection elsewhere, because we basically need | ||||
|           // 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. | ||||
|           // Reconnect if we have disconnected. | ||||
|           if (!this.isConnecting()) this.connect(true) | ||||
|           break | ||||
|         default: | ||||
|           if (this.isConnecting()) break | ||||
|           // Means we never could do an initial connection. Reconnect everything. | ||||
|           if (!this.pingPongSpan.ping) this.connect() | ||||
|           if (!this.pingPongSpan.ping) this.connect(true) | ||||
|           break | ||||
|       } | ||||
|     }, pingIntervalMs) | ||||
| @ -394,101 +372,6 @@ class EngineConnection extends EventTarget { | ||||
|     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() { | ||||
|     return this.state.type === EngineConnectionStateType.Connecting | ||||
|   } | ||||
| @ -499,29 +382,58 @@ class EngineConnection extends EventTarget { | ||||
|  | ||||
|   tearDown(opts?: { idleMode: boolean }) { | ||||
|     this.idleMode = opts?.idleMode ?? false | ||||
|     this.disconnectAll() | ||||
|     clearInterval(this.pingIntervalId) | ||||
|  | ||||
|     if (opts?.idleMode) { | ||||
|       this.state = { | ||||
|     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.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, | ||||
|           value: { | ||||
|             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, | ||||
|           value: { | ||||
|             type: DisconnectingType.Quit, | ||||
|           }, | ||||
|         } | ||||
|  | ||||
|     this.disconnectAll() | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @ -531,16 +443,17 @@ class EngineConnection extends EventTarget { | ||||
|    * This will attempt the full handshake, and retry if the connection | ||||
|    * did not establish. | ||||
|    */ | ||||
|   connect(reconnecting?: boolean): Promise<void> { | ||||
|     return new Promise((resolve) => { | ||||
|   connect(reconnecting?: boolean) { | ||||
|     if (this.isConnecting() || this.isReady()) { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     const createPeerConnection = () => { | ||||
|       if (!this.engineCommandManager.disableWebRTC) { | ||||
|         this.pc = new RTCPeerConnection({ | ||||
|           bundlePolicy: 'max-bundle', | ||||
|         }) | ||||
|       } | ||||
|  | ||||
|       // Other parts of the application expect pc to be initialized when firing. | ||||
|       this.dispatchEvent( | ||||
| @ -593,10 +506,7 @@ class EngineConnection extends EventTarget { | ||||
|           `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 | ||||
|       // Event type: generic Event type... | ||||
| @ -613,19 +523,8 @@ class EngineConnection extends EventTarget { | ||||
|               }) | ||||
|             ) | ||||
|             break | ||||
|             case 'disconnected': | ||||
|           case 'failed': | ||||
|               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.disconnectAll() | ||||
|             this.state = { | ||||
|               type: EngineConnectionStateType.Disconnecting, | ||||
|               value: { | ||||
| @ -636,7 +535,6 @@ class EngineConnection extends EventTarget { | ||||
|                 }, | ||||
|               }, | ||||
|             } | ||||
|               this.disconnectAll() | ||||
|             break | ||||
|           default: | ||||
|             break | ||||
| @ -701,8 +599,7 @@ class EngineConnection extends EventTarget { | ||||
|                     videoTrackReport.framesPerSecond || 0 | ||||
|                   client_metrics.rtc_freeze_count = | ||||
|                     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 = | ||||
|                     videoTrackReport.keyFramesDecoded || 0 | ||||
|                   client_metrics.rtc_total_freezes_duration_sec = | ||||
| @ -713,8 +610,7 @@ class EngineConnection extends EventTarget { | ||||
|                     videoTrackReport.frameWidth || 0 | ||||
|                   client_metrics.rtc_packets_lost = | ||||
|                     videoTrackReport.packetsLost || 0 | ||||
|                     client_metrics.rtc_pli_count = | ||||
|                       videoTrackReport.pliCount || 0 | ||||
|                   client_metrics.rtc_pli_count = videoTrackReport.pliCount || 0 | ||||
|                 } else if (videoTrackReport.type === 'transport') { | ||||
|                   // videoTrackReport.bytesReceived, | ||||
|                   // videoTrackReport.bytesSent, | ||||
| @ -756,9 +652,7 @@ class EngineConnection extends EventTarget { | ||||
|           } | ||||
|  | ||||
|           // Everything is now connected. | ||||
|             this.state = { | ||||
|               type: EngineConnectionStateType.ConnectionEstablished, | ||||
|             } | ||||
|           this.state = { type: EngineConnectionStateType.ConnectionEstablished } | ||||
|  | ||||
|           this.engineCommandManager.inSequence = 1 | ||||
|  | ||||
| @ -772,32 +666,17 @@ class EngineConnection extends EventTarget { | ||||
|         ) | ||||
|  | ||||
|         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.finalizeIfAllConnectionsClosed() | ||||
|         } | ||||
|  | ||||
|         this.unreliableDataChannel?.addEventListener( | ||||
|           'close', | ||||
|           this.onDataChannelClose | ||||
|         ) | ||||
|  | ||||
|         this.onDataChannelError = (event) => { | ||||
|           this.disconnectAll() | ||||
|  | ||||
|           this.state = { | ||||
|             type: EngineConnectionStateType.Disconnecting, | ||||
|             value: { | ||||
| @ -808,7 +687,6 @@ class EngineConnection extends EventTarget { | ||||
|               }, | ||||
|             }, | ||||
|           } | ||||
|             this.disconnectAll() | ||||
|         } | ||||
|         this.unreliableDataChannel?.addEventListener( | ||||
|           'error', | ||||
| @ -818,8 +696,7 @@ class EngineConnection extends EventTarget { | ||||
|         this.onDataChannelMessage = (event) => { | ||||
|           const result: UnreliableResponses = JSON.parse(event.data) | ||||
|           Object.values( | ||||
|               this.engineCommandManager.unreliableSubscriptions[result.type] || | ||||
|                 {} | ||||
|             this.engineCommandManager.unreliableSubscriptions[result.type] || {} | ||||
|           ).forEach( | ||||
|             // 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 | ||||
| @ -879,28 +756,23 @@ class EngineConnection extends EventTarget { | ||||
|         // Send an initial ping | ||||
|         this.send({ type: 'ping' }) | ||||
|         this.pingPongSpan.ping = new Date() | ||||
|         if (this.engineCommandManager.disableWebRTC) { | ||||
|           this.engineCommandManager | ||||
|             .initPlanes() | ||||
|             .then(() => this.engineCommandManager.resolveReady()) | ||||
|         } | ||||
|       } | ||||
|       this.websocket.addEventListener('open', this.onWebSocketOpen) | ||||
|  | ||||
|       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.finalizeIfAllConnectionsClosed() | ||||
|       } | ||||
|       this.websocket.addEventListener('close', this.onWebSocketClose) | ||||
|  | ||||
|       this.onWebSocketError = (event) => { | ||||
|         this.disconnectAll() | ||||
|  | ||||
|         this.state = { | ||||
|           type: EngineConnectionStateType.Disconnecting, | ||||
|           value: { | ||||
| @ -911,8 +783,6 @@ class EngineConnection extends EventTarget { | ||||
|             }, | ||||
|           }, | ||||
|         } | ||||
|  | ||||
|           this.disconnectAll() | ||||
|       } | ||||
|       this.websocket.addEventListener('error', this.onWebSocketError) | ||||
|  | ||||
| @ -928,9 +798,7 @@ class EngineConnection extends EventTarget { | ||||
|           return | ||||
|         } | ||||
|  | ||||
|           const message: Models['WebSocketResponse_type'] = JSON.parse( | ||||
|             event.data | ||||
|           ) | ||||
|         const message: Models['WebSocketResponse_type'] = JSON.parse(event.data) | ||||
|  | ||||
|         if (!message.success) { | ||||
|           const errorsString = message?.errors | ||||
| @ -986,8 +854,6 @@ class EngineConnection extends EventTarget { | ||||
|           case 'pong': | ||||
|             this.pingPongSpan.pong = new Date() | ||||
|             break | ||||
|  | ||||
|             // Only fires on successful authentication. | ||||
|           case 'ice_server_info': | ||||
|             let ice_servers = resp.data?.ice_servers | ||||
|  | ||||
| @ -1067,6 +933,7 @@ class EngineConnection extends EventTarget { | ||||
|               }) | ||||
|               .catch((err: Error) => { | ||||
|                 // The local description is invalid, so there's no point continuing. | ||||
|                 this.disconnectAll() | ||||
|                 this.state = { | ||||
|                   type: EngineConnectionStateType.Disconnecting, | ||||
|                   value: { | ||||
| @ -1077,7 +944,6 @@ class EngineConnection extends EventTarget { | ||||
|                     }, | ||||
|                   }, | ||||
|                 } | ||||
|                   this.disconnectAll() | ||||
|               }) | ||||
|             break | ||||
|  | ||||
| @ -1148,7 +1014,6 @@ class EngineConnection extends EventTarget { | ||||
|         this.onNetworkStatusReady | ||||
|       ) | ||||
|     } | ||||
|     }) | ||||
|   } | ||||
|   // Do not change this back to an object or any, we should only be sending the | ||||
|   // WebSocketRequest type! | ||||
| @ -1162,9 +1027,6 @@ class EngineConnection extends EventTarget { | ||||
|   // Do not change this back to an object or any, we should only be sending the | ||||
|   // 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 | ||||
|     // take actions if needed? | ||||
|     this.websocket?.send( | ||||
| @ -1172,35 +1034,18 @@ class EngineConnection extends EventTarget { | ||||
|     ) | ||||
|   } | ||||
|   disconnectAll() { | ||||
|     if (this.websocket?.readyState === 1) { | ||||
|     this.websocket?.close() | ||||
|     } | ||||
|     if (this.unreliableDataChannel?.readyState === 'open') { | ||||
|     this.unreliableDataChannel?.close() | ||||
|     } | ||||
|     if (this.pc?.connectionState === 'connected') { | ||||
|     this.pc?.close() | ||||
|     } | ||||
|  | ||||
|     this.webrtcStatsCollector = undefined | ||||
|  | ||||
|     // Already triggered | ||||
|     if (this.state.type === EngineConnectionStateType.Disconnected) return | ||||
|  | ||||
|     const closedPc = !this.pc || this.pc?.connectionState === 'closed' | ||||
|     const closedUDC = | ||||
|       !this.unreliableDataChannel || | ||||
|   } | ||||
|   finalizeIfAllConnectionsClosed() { | ||||
|     const allClosed = | ||||
|       this.websocket?.readyState === 3 && | ||||
|       this.pc?.connectionState === 'closed' && | ||||
|       this.unreliableDataChannel?.readyState === 'closed' | ||||
|  | ||||
|     // 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) { | ||||
|     if (allClosed) { | ||||
|       // Do not notify the rest of the program that we have cut off anything. | ||||
|       this.state = { type: EngineConnectionStateType.Disconnected } | ||||
|     } | ||||
| @ -1248,11 +1093,7 @@ export type CommandLog = | ||||
|     } | ||||
|  | ||||
| export enum EngineCommandManagerEvents { | ||||
|   // engineConnection is available but scene setup may not have run | ||||
|   EngineAvailable = 'engine-available', | ||||
|  | ||||
|   // the whole scene is ready (settings loaded) | ||||
|   SceneReady = 'scene-ready', | ||||
| } | ||||
|  | ||||
| /** | ||||
| @ -1310,6 +1151,7 @@ export class EngineCommandManager extends EventTarget { | ||||
|    * any out-of-order late responses in the unreliable channel. | ||||
|    */ | ||||
|   inSequence = 1 | ||||
|   pool?: string | ||||
|   engineConnection?: EngineConnection | ||||
|   defaultPlanes: DefaultPlanes | null = null | ||||
|   commandLogs: CommandLog[] = [] | ||||
| @ -1318,8 +1160,6 @@ export class EngineCommandManager extends EventTarget { | ||||
|     reject: (reason: any) => void | ||||
|     commandId: string | ||||
|   } | ||||
|   settings: SettingsViaQueryString | ||||
|  | ||||
|   /** | ||||
|    * 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. | ||||
| @ -1327,6 +1167,15 @@ export class EngineCommandManager extends EventTarget { | ||||
|    */ | ||||
|   private _exportIntent: ExportIntent | null = null | ||||
|   _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: { | ||||
|     [event: string]: { | ||||
| @ -1339,19 +1188,11 @@ export class EngineCommandManager extends EventTarget { | ||||
|     } | ||||
|   } = {} as any | ||||
|  | ||||
|   constructor(settings?: SettingsViaQueryString) { | ||||
|   constructor(pool?: string) { | ||||
|     super() | ||||
|  | ||||
|     this.engineConnection = undefined | ||||
|     this.settings = settings | ||||
|       ? settings | ||||
|       : { | ||||
|           pool: null, | ||||
|           theme: Themes.Dark, | ||||
|           highlightEdges: true, | ||||
|           enableSSAO: true, | ||||
|           showScaleGrid: false, | ||||
|         } | ||||
|     this.pool = pool | ||||
|   } | ||||
|  | ||||
|   private _camControlsCameraChange = () => {} | ||||
| @ -1373,6 +1214,7 @@ export class EngineCommandManager extends EventTarget { | ||||
|   private onEngineConnectionNewTrack = ({ | ||||
|     detail, | ||||
|   }: CustomEvent<NewTrackArgs>) => {} | ||||
|   disableWebRTC = false | ||||
|   modelingSend: ReturnType<typeof useModelingContext>['send'] = | ||||
|     (() => {}) as any | ||||
|  | ||||
| @ -1385,38 +1227,40 @@ export class EngineCommandManager extends EventTarget { | ||||
|   } | ||||
|  | ||||
|   start({ | ||||
|     disableWebRTC = false, | ||||
|     setMediaStream, | ||||
|     setIsStreamReady, | ||||
|     width, | ||||
|     height, | ||||
|     executeCode, | ||||
|     token, | ||||
|     makeDefaultPlanes, | ||||
|     modifyGrid, | ||||
|     settings = { | ||||
|       pool: null, | ||||
|       theme: Themes.Dark, | ||||
|       highlightEdges: true, | ||||
|       enableSSAO: true, | ||||
|       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, | ||||
|   }: { | ||||
|     callbackOnEngineLiteConnect?: () => void | ||||
|     disableWebRTC?: boolean | ||||
|     setMediaStream: (stream: MediaStream) => void | ||||
|     setIsStreamReady: (isStreamReady: boolean) => void | ||||
|     width: number | ||||
|     height: number | ||||
|     executeCode: () => void | ||||
|     token?: string | ||||
|     makeDefaultPlanes: () => Promise<DefaultPlanes> | ||||
|     modifyGrid: (hidden: boolean) => Promise<void> | ||||
|     settings?: SettingsViaQueryString | ||||
|   }) { | ||||
|     if (settings) { | ||||
|       this.settings = settings | ||||
|     settings?: { | ||||
|       theme: Themes | ||||
|       highlightEdges: boolean | ||||
|       enableSSAO: boolean | ||||
|       showScaleGrid: boolean | ||||
|     } | ||||
|   }) { | ||||
|     this.makeDefaultPlanes = makeDefaultPlanes | ||||
|     this.disableWebRTC = disableWebRTC | ||||
|     this.modifyGrid = modifyGrid | ||||
|     if (width === 0 || height === 0) { | ||||
|       return | ||||
| @ -1431,28 +1275,22 @@ export class EngineCommandManager extends EventTarget { | ||||
|       return | ||||
|     } | ||||
|  | ||||
|     const additionalSettings = this.settings.enableSSAO | ||||
|       ? '&post_effect=ssao' | ||||
|       : '' | ||||
|     const pool = !this.settings.pool ? '' : `&pool=${this.settings.pool}` | ||||
|     const additionalSettings = settings.enableSSAO ? '&post_effect=ssao' : '' | ||||
|     const pool = this.pool === undefined ? '' : `&pool=${this.pool}` | ||||
|     const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}${pool}` | ||||
|     this.engineConnection = new EngineConnection({ | ||||
|       engineCommandManager: this, | ||||
|       url, | ||||
|       token, | ||||
|       callbackOnEngineLiteConnect, | ||||
|     }) | ||||
|  | ||||
|     // Nothing more to do when using a lite engine initialization | ||||
|     if (callbackOnEngineLiteConnect) return | ||||
|  | ||||
|     this.dispatchEvent( | ||||
|       new CustomEvent(EngineCommandManagerEvents.EngineAvailable, { | ||||
|         detail: this.engineConnection, | ||||
|       }) | ||||
|     ) | ||||
|  | ||||
|     this.onEngineConnectionOpened = async () => { | ||||
|     this.onEngineConnectionOpened = () => { | ||||
|       // Set the stream background color | ||||
|       // This takes RGBA values from 0-1 | ||||
|       // So we convert from the conventional 0-255 found in Figma | ||||
| @ -1462,12 +1300,12 @@ export class EngineCommandManager extends EventTarget { | ||||
|         cmd_id: uuidv4(), | ||||
|         cmd: { | ||||
|           type: 'set_background_color', | ||||
|           color: getThemeColorForEngine(this.settings.theme), | ||||
|           color: getThemeColorForEngine(settings.theme), | ||||
|         }, | ||||
|       }) | ||||
|  | ||||
|       // Sets the default line colors | ||||
|       const opposingTheme = getOppositeTheme(this.settings.theme) | ||||
|       const opposingTheme = getOppositeTheme(settings.theme) | ||||
|       this.sendSceneCommand({ | ||||
|         cmd_id: uuidv4(), | ||||
|         type: 'modeling_cmd_req', | ||||
| @ -1483,7 +1321,7 @@ export class EngineCommandManager extends EventTarget { | ||||
|         cmd_id: uuidv4(), | ||||
|         cmd: { | ||||
|           type: 'edge_lines_visible' as any, // TODO: update kittycad.ts to use the correct type | ||||
|           hidden: !this.settings.highlightEdges, | ||||
|           hidden: !settings.highlightEdges, | ||||
|         }, | ||||
|       }) | ||||
|  | ||||
| @ -1500,19 +1338,13 @@ export class EngineCommandManager extends EventTarget { | ||||
|       // 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 | ||||
|       // that) https://github.com/KittyCAD/engine/issues/2282 | ||||
|       this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => { | ||||
|       this.modifyGrid(!settings.showScaleGrid)?.then(async () => { | ||||
|         await this.initPlanes() | ||||
|         this.resolveReady() | ||||
|         setIsStreamReady(true) | ||||
|  | ||||
|         // Other parts of the application should use this to react on scene ready. | ||||
|         this.dispatchEvent( | ||||
|           new CustomEvent(EngineCommandManagerEvents.SceneReady, { | ||||
|             detail: this.engineConnection, | ||||
|           }) | ||||
|         ) | ||||
|         await executeCode() | ||||
|       }) | ||||
|     } | ||||
|  | ||||
|     this.engineConnection.addEventListener( | ||||
|       EngineConnectionEvents.Opened, | ||||
|       this.onEngineConnectionOpened | ||||
| @ -1709,8 +1541,6 @@ export class EngineCommandManager extends EventTarget { | ||||
|       EngineConnectionEvents.ConnectionStarted, | ||||
|       this.onEngineConnectionStarted | ||||
|     ) | ||||
|  | ||||
|     return | ||||
|   } | ||||
|  | ||||
|   handleResize({ | ||||
| @ -1739,28 +1569,25 @@ export class EngineCommandManager extends EventTarget { | ||||
|  | ||||
|   tearDown(opts?: { idleMode: boolean }) { | ||||
|     if (this.engineConnection) { | ||||
|       for (const pending of Object.values(this.pendingCommands)) { | ||||
|         pending.reject('no connection to send on') | ||||
|       } | ||||
|  | ||||
|       this.engineConnection?.removeEventListener?.( | ||||
|       this.engineConnection.removeEventListener( | ||||
|         EngineConnectionEvents.Opened, | ||||
|         this.onEngineConnectionOpened | ||||
|       ) | ||||
|       this.engineConnection.removeEventListener?.( | ||||
|       this.engineConnection.removeEventListener( | ||||
|         EngineConnectionEvents.Closed, | ||||
|         this.onEngineConnectionClosed | ||||
|       ) | ||||
|       this.engineConnection.removeEventListener?.( | ||||
|       this.engineConnection.removeEventListener( | ||||
|         EngineConnectionEvents.ConnectionStarted, | ||||
|         this.onEngineConnectionStarted | ||||
|       ) | ||||
|       this.engineConnection.removeEventListener?.( | ||||
|       this.engineConnection.removeEventListener( | ||||
|         EngineConnectionEvents.NewTrack, | ||||
|         this.onEngineConnectionNewTrack as EventListener | ||||
|       ) | ||||
|  | ||||
|       this.engineConnection?.tearDown(opts) | ||||
|       this.engineConnection = undefined | ||||
|  | ||||
|       // Our window.tearDown assignment causes this case to happen which is | ||||
|       // only really for tests. | ||||
| @ -1942,17 +1769,17 @@ export class EngineCommandManager extends EventTarget { | ||||
|     commandStr: string, | ||||
|     idToRangeStr: string | ||||
|   ): Promise<string | void> { | ||||
|     if (this.engineConnection === undefined) return Promise.resolve() | ||||
|     if ( | ||||
|       !this.engineConnection?.isReady() && | ||||
|       !this.engineConnection.isUsingConnectionLite | ||||
|     ) | ||||
|     if (this.engineConnection === undefined) { | ||||
|       return Promise.resolve() | ||||
|     } | ||||
|     if (!this.engineConnection?.isReady() && !this.disableWebRTC) | ||||
|       return Promise.resolve() | ||||
|     if (id === undefined) return Promise.reject(new Error('id is undefined')) | ||||
|     if (rangeStr === undefined) | ||||
|       return Promise.reject(new Error('rangeStr is undefined')) | ||||
|     if (commandStr === undefined) | ||||
|     if (commandStr === undefined) { | ||||
|       return Promise.reject(new Error('commandStr is undefined')) | ||||
|     } | ||||
|     const range: SourceRange = JSON.parse(rangeStr) | ||||
|     const command: EngineCommand = JSON.parse(commandStr) | ||||
|     const idToRangeMap: { [key: string]: SourceRange } = | ||||
| @ -2047,17 +1874,6 @@ export class EngineCommandManager extends EventTarget { | ||||
|   } | ||||
|  | ||||
|   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({ | ||||
|       type: 'modeling_cmd_req', | ||||
|       cmd_id: uuidv4(), | ||||
|  | ||||
| @ -63,7 +63,7 @@ export class CoreDumpManager { | ||||
|  | ||||
|   // Get the backend pool we've requested. | ||||
|   pool(): string { | ||||
|     return this.engineCommandManager.settings.pool || '' | ||||
|     return this.engineCommandManager.pool || '' | ||||
|   } | ||||
|  | ||||
|   // Get the os information. | ||||
|  | ||||
| @ -104,9 +104,11 @@ export const fileLoader: LoaderFunction = async ({ | ||||
|     // the file system and not the editor. | ||||
|     codeManager.updateCurrentFilePath(current_file_path) | ||||
|     codeManager.updateCodeStateEditor(code) | ||||
|  | ||||
|     // 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 | ||||
|     // So that WASM gets an updated path for operations | ||||
|  | ||||
| @ -2,15 +2,6 @@ import { type Models } from '@kittycad/lib' | ||||
| import { Setting, settings } from './initialSettings' | ||||
| import { AtLeast, PathValue, Paths } from 'lib/types' | ||||
| 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 { | ||||
|   Imperial = 'imperial', | ||||
|  | ||||
| @ -1,8 +1,5 @@ | ||||
| import { Program, ProgramMemory, _executor, SourceRange } from '../lang/wasm' | ||||
| import { | ||||
|   EngineCommandManager, | ||||
|   EngineCommandManagerEvents, | ||||
| } from 'lang/std/engineConnection' | ||||
| import { EngineCommandManager } from 'lang/std/engineConnection' | ||||
| import { EngineCommand } from 'lang/std/artifactGraph' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import { v4 as uuidv4 } from 'uuid' | ||||
| @ -85,6 +82,7 @@ export async function enginelessExecutor( | ||||
|     setIsStreamReady: () => {}, | ||||
|     setMediaStream: () => {}, | ||||
|   }) as any as EngineCommandManager | ||||
|   await mockEngineCommandManager.waitForReady | ||||
|   mockEngineCommandManager.startNewSession() | ||||
|   const programMemory = await _executor(ast, pm, mockEngineCommandManager, true) | ||||
|   await mockEngineCommandManager.waitForAllCommands() | ||||
| @ -101,6 +99,7 @@ export async function executor( | ||||
|     setMediaStream: () => {}, | ||||
|     width: 0, | ||||
|     height: 0, | ||||
|     executeCode: () => {}, | ||||
|     makeDefaultPlanes: () => { | ||||
|       return new Promise((resolve) => resolve(defaultPlanes)) | ||||
|     }, | ||||
| @ -108,21 +107,9 @@ export async function executor( | ||||
|       return new Promise((resolve) => resolve()) | ||||
|     }, | ||||
|   }) | ||||
|  | ||||
|   return new Promise((resolve) => { | ||||
|     engineCommandManager.addEventListener( | ||||
|       EngineCommandManagerEvents.SceneReady, | ||||
|       async () => { | ||||
|   await engineCommandManager.waitForReady | ||||
|   engineCommandManager.startNewSession() | ||||
|         const programMemory = await _executor( | ||||
|           ast, | ||||
|           pm, | ||||
|           engineCommandManager, | ||||
|           false | ||||
|         ) | ||||
|   const programMemory = await _executor(ast, pm, engineCommandManager, false) | ||||
|   await engineCommandManager.waitForAllCommands() | ||||
|         Promise.resolve(programMemory) | ||||
|       } | ||||
|     ) | ||||
|   }) | ||||
|   return programMemory | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	