internal: Add lints for promises (#3733)
* Add lints for floating and misued promises * Add logging async errors in main * Add async error catch in test-utils * Change any to unknown * Trap promise errors and ignore more await warnings * Add more ignores and toSync helper * Fix more lint warnings * Add more ignores and fixes * Add more reject reporting * Add accepting arbitrary parameters to toSync() * Fix more lints * Revert unintentional change to non-arrow function * Revert unintentional change to use arrow function * Fix new warnings in main with auto updater * Fix formatting * Change lints to error This is what the recommended type checked rules do. * Fix to properly report promise rejections * Fix formatting * Fix formatting * Remove unused import * Remove unused convenience function * Move type helpers * Fix to not return promise when caller doesn't expect it * Add ignores to lsp code
This commit is contained in:
		@ -13,6 +13,8 @@
 | 
			
		||||
      "plugin:css-modules/recommended"
 | 
			
		||||
    ],
 | 
			
		||||
    "rules": {
 | 
			
		||||
      "@typescript-eslint/no-floating-promises": "error",
 | 
			
		||||
      "@typescript-eslint/no-misused-promises": "error",
 | 
			
		||||
      "semi": [
 | 
			
		||||
        "error",
 | 
			
		||||
        "never"
 | 
			
		||||
@ -24,7 +26,6 @@
 | 
			
		||||
      {
 | 
			
		||||
        "files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
 | 
			
		||||
        "rules": {
 | 
			
		||||
          "@typescript-eslint/no-floating-promises": "warn",
 | 
			
		||||
          "suggest-no-throw/suggest-no-throw": "off",
 | 
			
		||||
          "testing-library/prefer-screen-queries": "off",
 | 
			
		||||
          "jest/valid-expect": "off"
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ import * as TOML from '@iarna/toml'
 | 
			
		||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
 | 
			
		||||
import { SETTINGS_FILE_NAME } from 'lib/constants'
 | 
			
		||||
import { isArray } from 'lib/utils'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
type TestColor = [number, number, number]
 | 
			
		||||
export const TEST_COLORS = {
 | 
			
		||||
@ -439,46 +440,50 @@ export async function getUtils(page: Page, test_?: typeof test) {
 | 
			
		||||
      }
 | 
			
		||||
      return maxDiff
 | 
			
		||||
    },
 | 
			
		||||
    doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
 | 
			
		||||
      new Promise(async (resolve) => {
 | 
			
		||||
        await page.screenshot({
 | 
			
		||||
          path: './e2e/playwright/temp1.png',
 | 
			
		||||
          fullPage: true,
 | 
			
		||||
        })
 | 
			
		||||
        await fn()
 | 
			
		||||
        const isImageDiff = async () => {
 | 
			
		||||
    doAndWaitForImageDiff: (fn: () => Promise<unknown>, diffCount = 200) =>
 | 
			
		||||
      new Promise<boolean>((resolve) => {
 | 
			
		||||
        ;(async () => {
 | 
			
		||||
          await page.screenshot({
 | 
			
		||||
            path: './e2e/playwright/temp2.png',
 | 
			
		||||
            path: './e2e/playwright/temp1.png',
 | 
			
		||||
            fullPage: true,
 | 
			
		||||
          })
 | 
			
		||||
          const screenshot1 = PNG.sync.read(
 | 
			
		||||
            await fsp.readFile('./e2e/playwright/temp1.png')
 | 
			
		||||
          )
 | 
			
		||||
          const screenshot2 = PNG.sync.read(
 | 
			
		||||
            await fsp.readFile('./e2e/playwright/temp2.png')
 | 
			
		||||
          )
 | 
			
		||||
          const actualDiffCount = pixelMatch(
 | 
			
		||||
            screenshot1.data,
 | 
			
		||||
            screenshot2.data,
 | 
			
		||||
            null,
 | 
			
		||||
            screenshot1.width,
 | 
			
		||||
            screenshot2.height
 | 
			
		||||
          )
 | 
			
		||||
          return actualDiffCount > diffCount
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
 | 
			
		||||
        let count = 0
 | 
			
		||||
        const interval = setInterval(async () => {
 | 
			
		||||
          count++
 | 
			
		||||
          if (await isImageDiff()) {
 | 
			
		||||
            clearInterval(interval)
 | 
			
		||||
            resolve(true)
 | 
			
		||||
          } else if (count > 100) {
 | 
			
		||||
            clearInterval(interval)
 | 
			
		||||
            resolve(false)
 | 
			
		||||
          await fn()
 | 
			
		||||
          const isImageDiff = async () => {
 | 
			
		||||
            await page.screenshot({
 | 
			
		||||
              path: './e2e/playwright/temp2.png',
 | 
			
		||||
              fullPage: true,
 | 
			
		||||
            })
 | 
			
		||||
            const screenshot1 = PNG.sync.read(
 | 
			
		||||
              await fsp.readFile('./e2e/playwright/temp1.png')
 | 
			
		||||
            )
 | 
			
		||||
            const screenshot2 = PNG.sync.read(
 | 
			
		||||
              await fsp.readFile('./e2e/playwright/temp2.png')
 | 
			
		||||
            )
 | 
			
		||||
            const actualDiffCount = pixelMatch(
 | 
			
		||||
              screenshot1.data,
 | 
			
		||||
              screenshot2.data,
 | 
			
		||||
              null,
 | 
			
		||||
              screenshot1.width,
 | 
			
		||||
              screenshot2.height
 | 
			
		||||
            )
 | 
			
		||||
            return actualDiffCount > diffCount
 | 
			
		||||
          }
 | 
			
		||||
        }, 50)
 | 
			
		||||
 | 
			
		||||
          // run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
 | 
			
		||||
          let count = 0
 | 
			
		||||
          const interval = setInterval(() => {
 | 
			
		||||
            ;(async () => {
 | 
			
		||||
              count++
 | 
			
		||||
              if (await isImageDiff()) {
 | 
			
		||||
                clearInterval(interval)
 | 
			
		||||
                resolve(true)
 | 
			
		||||
              } else if (count > 100) {
 | 
			
		||||
                clearInterval(interval)
 | 
			
		||||
                resolve(false)
 | 
			
		||||
              }
 | 
			
		||||
            })().catch(reportRejection)
 | 
			
		||||
          }, 50)
 | 
			
		||||
        })().catch(reportRejection)
 | 
			
		||||
      }),
 | 
			
		||||
    emulateNetworkConditions: async (
 | 
			
		||||
      networkOptions: Protocol.Network.emulateNetworkConditionsParameters
 | 
			
		||||
 | 
			
		||||
@ -72,6 +72,7 @@ export class LanguageServerClient {
 | 
			
		||||
  async initialize() {
 | 
			
		||||
    // Start the client in the background.
 | 
			
		||||
    this.client.setNotifyFn(this.processNotifications.bind(this))
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    this.client.start()
 | 
			
		||||
 | 
			
		||||
    this.ready = true
 | 
			
		||||
@ -195,6 +196,9 @@ export class LanguageServerClient {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private processNotifications(notification: LSP.NotificationMessage) {
 | 
			
		||||
    for (const plugin of this.plugins) plugin.processNotification(notification)
 | 
			
		||||
    for (const plugin of this.plugins) {
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      plugin.processNotification(notification)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ export default function lspFormatExt(
 | 
			
		||||
      run: (view: EditorView) => {
 | 
			
		||||
        let value = view.plugin(plugin)
 | 
			
		||||
        if (!value) return false
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
        value.requestFormatting()
 | 
			
		||||
        return true
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@ -117,6 +117,7 @@ export class LanguageServerPlugin implements PluginValue {
 | 
			
		||||
 | 
			
		||||
    this.processLspNotification = options.processLspNotification
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    this.initialize({
 | 
			
		||||
      documentText: this.getDocText(),
 | 
			
		||||
    })
 | 
			
		||||
@ -149,6 +150,7 @@ export class LanguageServerPlugin implements PluginValue {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async initialize({ documentText }: { documentText: string }) {
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
    if (this.client.initializePromise) {
 | 
			
		||||
      await this.client.initializePromise
 | 
			
		||||
    }
 | 
			
		||||
@ -162,7 +164,9 @@ export class LanguageServerPlugin implements PluginValue {
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    this.requestSemanticTokens()
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    this.updateFoldingRanges()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -225,7 +229,9 @@ export class LanguageServerPlugin implements PluginValue {
 | 
			
		||||
        contentChanges: [{ text: this.view.state.doc.toString() }],
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      this.requestSemanticTokens()
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      this.updateFoldingRanges()
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error(e)
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,7 @@ import useHotkeyWrapper from 'lib/hotkeyWrapper'
 | 
			
		||||
import Gizmo from 'components/Gizmo'
 | 
			
		||||
import { CoreDumpManager } from 'lib/coredump'
 | 
			
		||||
import { UnitsMenu } from 'components/UnitsMenu'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
export function App() {
 | 
			
		||||
  const { project, file } = useLoaderData() as IndexLoaderData
 | 
			
		||||
@ -80,7 +81,7 @@ export function App() {
 | 
			
		||||
  useEngineConnectionSubscriptions()
 | 
			
		||||
 | 
			
		||||
  const debounceSocketSend = throttle<EngineCommand>((message) => {
 | 
			
		||||
    engineCommandManager.sendSceneCommand(message)
 | 
			
		||||
    engineCommandManager.sendSceneCommand(message).catch(reportRejection)
 | 
			
		||||
  }, 1000 / 15)
 | 
			
		||||
  const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => {
 | 
			
		||||
    if (state.matches('Sketch')) {
 | 
			
		||||
 | 
			
		||||
@ -41,6 +41,7 @@ import toast from 'react-hot-toast'
 | 
			
		||||
import { coreDump } from 'lang/wasm'
 | 
			
		||||
import { useMemo } from 'react'
 | 
			
		||||
import { AppStateProvider } from 'AppState'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
 | 
			
		||||
 | 
			
		||||
@ -173,21 +174,23 @@ function CoreDump() {
 | 
			
		||||
    []
 | 
			
		||||
  )
 | 
			
		||||
  useHotkeyWrapper(['mod + shift + .'], () => {
 | 
			
		||||
    toast.promise(
 | 
			
		||||
      coreDump(coreDumpManager, true),
 | 
			
		||||
      {
 | 
			
		||||
        loading: 'Starting core dump...',
 | 
			
		||||
        success: 'Core dump completed successfully',
 | 
			
		||||
        error: 'Error while exporting core dump',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        success: {
 | 
			
		||||
          // Note: this extended duration is especially important for Playwright e2e testing
 | 
			
		||||
          // default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
 | 
			
		||||
          duration: 6000,
 | 
			
		||||
    toast
 | 
			
		||||
      .promise(
 | 
			
		||||
        coreDump(coreDumpManager, true),
 | 
			
		||||
        {
 | 
			
		||||
          loading: 'Starting core dump...',
 | 
			
		||||
          success: 'Core dump completed successfully',
 | 
			
		||||
          error: 'Error while exporting core dump',
 | 
			
		||||
        },
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
        {
 | 
			
		||||
          success: {
 | 
			
		||||
            // Note: this extended duration is especially important for Playwright e2e testing
 | 
			
		||||
            // default duration is 2000 - https://react-hot-toast.com/docs/toast#default-durations
 | 
			
		||||
            duration: 6000,
 | 
			
		||||
          },
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
      .catch(reportRejection)
 | 
			
		||||
  })
 | 
			
		||||
  return null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -22,11 +22,12 @@ import {
 | 
			
		||||
  UnreliableSubscription,
 | 
			
		||||
} from 'lang/std/engineConnection'
 | 
			
		||||
import { EngineCommand } from 'lang/std/artifactGraph'
 | 
			
		||||
import { uuidv4 } from 'lib/utils'
 | 
			
		||||
import { toSync, uuidv4 } from 'lib/utils'
 | 
			
		||||
import { deg2Rad } from 'lib/utils2d'
 | 
			
		||||
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
 | 
			
		||||
import * as TWEEN from '@tweenjs/tween.js'
 | 
			
		||||
import { isQuaternionVertical } from './helpers'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
const ORTHOGRAPHIC_CAMERA_SIZE = 20
 | 
			
		||||
const FRAMES_TO_ANIMATE_IN = 30
 | 
			
		||||
@ -100,6 +101,7 @@ export class CameraControls {
 | 
			
		||||
      camProps.type === 'perspective' &&
 | 
			
		||||
      this.camera instanceof OrthographicCamera
 | 
			
		||||
    ) {
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      this.usePerspectiveCamera()
 | 
			
		||||
    } else if (
 | 
			
		||||
      camProps.type === 'orthographic' &&
 | 
			
		||||
@ -127,6 +129,7 @@ export class CameraControls {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  throttledEngCmd = throttle((cmd: EngineCommand) => {
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    this.engineCommandManager.sendSceneCommand(cmd)
 | 
			
		||||
  }, 1000 / 30)
 | 
			
		||||
 | 
			
		||||
@ -139,6 +142,7 @@ export class CameraControls {
 | 
			
		||||
        ...convertThreeCamValuesToEngineCam(threeValues),
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    this.engineCommandManager.sendSceneCommand(cmd)
 | 
			
		||||
  }, 1000 / 15)
 | 
			
		||||
 | 
			
		||||
@ -151,6 +155,7 @@ export class CameraControls {
 | 
			
		||||
      this.lastPerspectiveCmd &&
 | 
			
		||||
      Date.now() - this.lastPerspectiveCmdTime >= lastCmdDelay
 | 
			
		||||
    ) {
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      this.engineCommandManager.sendSceneCommand(this.lastPerspectiveCmd, true)
 | 
			
		||||
      this.lastPerspectiveCmdTime = Date.now()
 | 
			
		||||
    }
 | 
			
		||||
@ -218,6 +223,7 @@ export class CameraControls {
 | 
			
		||||
        this.useOrthographicCamera()
 | 
			
		||||
      }
 | 
			
		||||
      if (this.camera instanceof OrthographicCamera && !camSettings.ortho) {
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
        this.usePerspectiveCamera()
 | 
			
		||||
      }
 | 
			
		||||
      if (this.camera instanceof PerspectiveCamera && camSettings.fov_y) {
 | 
			
		||||
@ -249,6 +255,7 @@ export class CameraControls {
 | 
			
		||||
    const doZoom = () => {
 | 
			
		||||
      if (this.zoomDataFromLastFrame !== undefined) {
 | 
			
		||||
        this.handleStart()
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
        this.engineCommandManager.sendSceneCommand({
 | 
			
		||||
          type: 'modeling_cmd_req',
 | 
			
		||||
          cmd: {
 | 
			
		||||
@ -266,6 +273,7 @@ export class CameraControls {
 | 
			
		||||
 | 
			
		||||
    const doMove = () => {
 | 
			
		||||
      if (this.moveDataFromLastFrame !== undefined) {
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
        this.engineCommandManager.sendSceneCommand({
 | 
			
		||||
          type: 'modeling_cmd_req',
 | 
			
		||||
          cmd: {
 | 
			
		||||
@ -459,6 +467,7 @@ export class CameraControls {
 | 
			
		||||
 | 
			
		||||
    this.camera.quaternion.set(qx, qy, qz, qw)
 | 
			
		||||
    this.camera.updateProjectionMatrix()
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    this.engineCommandManager.sendSceneCommand({
 | 
			
		||||
      type: 'modeling_cmd_req',
 | 
			
		||||
      cmd_id: uuidv4(),
 | 
			
		||||
@ -929,6 +938,7 @@ export class CameraControls {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (isReducedMotion()) {
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
        onComplete()
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
@ -937,7 +947,7 @@ export class CameraControls {
 | 
			
		||||
        .to({ t: tweenEnd }, duration)
 | 
			
		||||
        .easing(TWEEN.Easing.Quadratic.InOut)
 | 
			
		||||
        .onUpdate(({ t }) => cameraAtTime(t))
 | 
			
		||||
        .onComplete(onComplete)
 | 
			
		||||
        .onComplete(toSync(onComplete, reportRejection))
 | 
			
		||||
        .start()
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
@ -962,6 +972,7 @@ export class CameraControls {
 | 
			
		||||
            // Decrease the FOV
 | 
			
		||||
            currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
 | 
			
		||||
            this.camera.updateProjectionMatrix()
 | 
			
		||||
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
            this.dollyZoom(currentFov)
 | 
			
		||||
            requestAnimationFrame(animateFovChange) // Continue the animation
 | 
			
		||||
          } else if (frameWaitOnFinish > 0) {
 | 
			
		||||
@ -991,6 +1002,7 @@ export class CameraControls {
 | 
			
		||||
      this.lastPerspectiveFov = 4
 | 
			
		||||
      let currentFov = 4
 | 
			
		||||
      const initialCameraUp = this.camera.up.clone()
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      this.usePerspectiveCamera()
 | 
			
		||||
      const tempVec = new Vector3()
 | 
			
		||||
 | 
			
		||||
@ -999,6 +1011,7 @@ export class CameraControls {
 | 
			
		||||
          this.lastPerspectiveFov + (targetFov - this.lastPerspectiveFov) * t
 | 
			
		||||
        const currentUp = tempVec.lerpVectors(initialCameraUp, targetCamUp, t)
 | 
			
		||||
        this.camera.up.copy(currentUp)
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
        this.dollyZoom(currentFov)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -1027,6 +1040,7 @@ export class CameraControls {
 | 
			
		||||
    this.lastPerspectiveFov = 4
 | 
			
		||||
    let currentFov = 4
 | 
			
		||||
    const initialCameraUp = this.camera.up.clone()
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    this.usePerspectiveCamera()
 | 
			
		||||
    const tempVec = new Vector3()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import { cameraMouseDragGuards } from 'lib/cameraControls'
 | 
			
		||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
 | 
			
		||||
import { ARROWHEAD, DEBUG_SHOW_BOTH_SCENES } from './sceneInfra'
 | 
			
		||||
import { ReactCameraProperties } from './CameraControls'
 | 
			
		||||
import { throttle } from 'lib/utils'
 | 
			
		||||
import { throttle, toSync } from 'lib/utils'
 | 
			
		||||
import {
 | 
			
		||||
  sceneInfra,
 | 
			
		||||
  kclManager,
 | 
			
		||||
@ -44,7 +44,7 @@ import {
 | 
			
		||||
  removeSingleConstraintInfo,
 | 
			
		||||
} from 'lang/modifyAst'
 | 
			
		||||
import { ActionButton } from 'components/ActionButton'
 | 
			
		||||
import { err, trap } from 'lib/trap'
 | 
			
		||||
import { err, reportRejection, trap } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
 | 
			
		||||
  const [isCamMoving, setIsCamMoving] = useState(false)
 | 
			
		||||
@ -582,7 +582,7 @@ const ConstraintSymbol = ({
 | 
			
		||||
        }}
 | 
			
		||||
        // disabled={isConstrained || !convertToVarEnabled}
 | 
			
		||||
        // disabled={implicitDesc} TODO why does this change styles that are hard to override?
 | 
			
		||||
        onClick={async () => {
 | 
			
		||||
        onClick={toSync(async () => {
 | 
			
		||||
          if (!isConstrained) {
 | 
			
		||||
            send({
 | 
			
		||||
              type: 'Convert to variable',
 | 
			
		||||
@ -616,13 +616,14 @@ const ConstraintSymbol = ({
 | 
			
		||||
              )
 | 
			
		||||
              if (!transform) return
 | 
			
		||||
              const { modifiedAst } = transform
 | 
			
		||||
              // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
              kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
              console.log('error', e)
 | 
			
		||||
            }
 | 
			
		||||
            toast.success('Constraint removed')
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
        }, reportRejection)}
 | 
			
		||||
      >
 | 
			
		||||
        <CustomIcon name={name} />
 | 
			
		||||
      </button>
 | 
			
		||||
@ -688,7 +689,7 @@ const ConstraintSymbol = ({
 | 
			
		||||
 | 
			
		||||
const throttled = throttle((a: ReactCameraProperties) => {
 | 
			
		||||
  if (a.type === 'perspective' && a.fov) {
 | 
			
		||||
    sceneInfra.camControls.dollyZoom(a.fov)
 | 
			
		||||
    sceneInfra.camControls.dollyZoom(a.fov).catch(reportRejection)
 | 
			
		||||
  }
 | 
			
		||||
}, 1000 / 15)
 | 
			
		||||
 | 
			
		||||
@ -718,6 +719,7 @@ export const CamDebugSettings = () => {
 | 
			
		||||
          if (camSettings.type === 'perspective') {
 | 
			
		||||
            sceneInfra.camControls.useOrthographicCamera()
 | 
			
		||||
          } else {
 | 
			
		||||
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
            sceneInfra.camControls.usePerspectiveCamera(true)
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
@ -725,7 +727,7 @@ export const CamDebugSettings = () => {
 | 
			
		||||
      <div>
 | 
			
		||||
        <button
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            sceneInfra.camControls.resetCameraPosition()
 | 
			
		||||
            sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          Reset Camera Position
 | 
			
		||||
 | 
			
		||||
@ -103,7 +103,7 @@ import {
 | 
			
		||||
  updateRectangleSketch,
 | 
			
		||||
} from 'lib/rectangleTool'
 | 
			
		||||
import { getThemeColorForThreeJs } from 'lib/theme'
 | 
			
		||||
import { err, trap } from 'lib/trap'
 | 
			
		||||
import { err, reportRejection, trap } from 'lib/trap'
 | 
			
		||||
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
 | 
			
		||||
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
 | 
			
		||||
 | 
			
		||||
@ -324,6 +324,7 @@ export class SceneEntities {
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
    sceneInfra.setCallbacks({
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
      onClick: async (args) => {
 | 
			
		||||
        if (!args) return
 | 
			
		||||
        if (args.mouseEvent.which !== 1) return
 | 
			
		||||
@ -634,6 +635,7 @@ export class SceneEntities {
 | 
			
		||||
        draftExpressionsIndices,
 | 
			
		||||
      })
 | 
			
		||||
    sceneInfra.setCallbacks({
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
      onClick: async (args) => {
 | 
			
		||||
        if (!args) return
 | 
			
		||||
        if (args.mouseEvent.which !== 1) return
 | 
			
		||||
@ -701,7 +703,7 @@ export class SceneEntities {
 | 
			
		||||
        if (profileStart) {
 | 
			
		||||
          sceneInfra.modelingSend({ type: 'CancelSketch' })
 | 
			
		||||
        } else {
 | 
			
		||||
          this.setUpDraftSegment(
 | 
			
		||||
          await this.setUpDraftSegment(
 | 
			
		||||
            sketchPathToNode,
 | 
			
		||||
            forward,
 | 
			
		||||
            up,
 | 
			
		||||
@ -771,6 +773,7 @@ export class SceneEntities {
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    sceneInfra.setCallbacks({
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
      onMove: async (args) => {
 | 
			
		||||
        // Update the width and height of the draft rectangle
 | 
			
		||||
        const pathToNodeTwo = structuredClone(sketchPathToNode)
 | 
			
		||||
@ -818,6 +821,7 @@ export class SceneEntities {
 | 
			
		||||
          this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
      onClick: async (args) => {
 | 
			
		||||
        // Commit the rectangle to the full AST/code and return to sketch.idle
 | 
			
		||||
        const cornerPoint = args.intersectionPoint?.twoD
 | 
			
		||||
@ -892,9 +896,11 @@ export class SceneEntities {
 | 
			
		||||
  }) => {
 | 
			
		||||
    let addingNewSegmentStatus: 'nothing' | 'pending' | 'added' = 'nothing'
 | 
			
		||||
    sceneInfra.setCallbacks({
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
      onDragEnd: async () => {
 | 
			
		||||
        if (addingNewSegmentStatus !== 'nothing') {
 | 
			
		||||
          await this.tearDownSketch({ removeAxis: false })
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
          this.setupSketch({
 | 
			
		||||
            sketchPathToNode: pathToNode,
 | 
			
		||||
            maybeModdedAst: kclManager.ast,
 | 
			
		||||
@ -911,6 +917,7 @@ export class SceneEntities {
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
      onDrag: async ({
 | 
			
		||||
        selected,
 | 
			
		||||
        intersectionPoint,
 | 
			
		||||
@ -958,6 +965,7 @@ export class SceneEntities {
 | 
			
		||||
 | 
			
		||||
            await kclManager.executeAstMock(mod.modifiedAst)
 | 
			
		||||
            await this.tearDownSketch({ removeAxis: false })
 | 
			
		||||
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
            this.setupSketch({
 | 
			
		||||
              sketchPathToNode: pathToNode,
 | 
			
		||||
              maybeModdedAst: kclManager.ast,
 | 
			
		||||
@ -1161,7 +1169,7 @@ export class SceneEntities {
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
      sceneInfra.overlayCallbacks(callBacks)
 | 
			
		||||
    })()
 | 
			
		||||
    })().catch(reportRejection)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 | 
			
		||||
@ -151,6 +151,7 @@ export function useCalc({
 | 
			
		||||
        })
 | 
			
		||||
        if (trap(error)) return
 | 
			
		||||
      }
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      executeAst({
 | 
			
		||||
        ast,
 | 
			
		||||
        engineCommandManager,
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
 | 
			
		||||
import { EngineCommandManagerEvents } from 'lang/std/engineConnection'
 | 
			
		||||
import { engineCommandManager, sceneInfra } from 'lib/singletons'
 | 
			
		||||
import { throttle, isReducedMotion } from 'lib/utils'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
const updateDollyZoom = throttle(
 | 
			
		||||
  (newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
 | 
			
		||||
@ -16,8 +17,8 @@ export const CamToggle = () => {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    engineCommandManager.addEventListener(
 | 
			
		||||
      EngineCommandManagerEvents.SceneReady,
 | 
			
		||||
      async () => {
 | 
			
		||||
        sceneInfra.camControls.dollyZoom(fov)
 | 
			
		||||
      () => {
 | 
			
		||||
        sceneInfra.camControls.dollyZoom(fov).catch(reportRejection)
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  }, [])
 | 
			
		||||
@ -26,11 +27,11 @@ export const CamToggle = () => {
 | 
			
		||||
    if (isPerspective) {
 | 
			
		||||
      isReducedMotion()
 | 
			
		||||
        ? sceneInfra.camControls.useOrthographicCamera()
 | 
			
		||||
        : sceneInfra.camControls.animateToOrthographic()
 | 
			
		||||
        : sceneInfra.camControls.animateToOrthographic().catch(reportRejection)
 | 
			
		||||
    } else {
 | 
			
		||||
      isReducedMotion()
 | 
			
		||||
        ? sceneInfra.camControls.usePerspectiveCamera()
 | 
			
		||||
        : sceneInfra.camControls.animateToPerspective()
 | 
			
		||||
        ? sceneInfra.camControls.usePerspectiveCamera().catch(reportRejection)
 | 
			
		||||
        : sceneInfra.camControls.animateToPerspective().catch(reportRejection)
 | 
			
		||||
    }
 | 
			
		||||
    setIsPerspective(!isPerspective)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import { CommandLog } from 'lang/std/engineConnection'
 | 
			
		||||
import { engineCommandManager } from 'lib/singletons'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
import { useState, useEffect } from 'react'
 | 
			
		||||
 | 
			
		||||
export function useEngineCommands(): [CommandLog[], () => void] {
 | 
			
		||||
@ -77,9 +78,11 @@ export const EngineCommands = () => {
 | 
			
		||||
      />
 | 
			
		||||
      <button
 | 
			
		||||
        data-testid="custom-cmd-send-button"
 | 
			
		||||
        onClick={() =>
 | 
			
		||||
          engineCommandManager.sendSceneCommand(JSON.parse(customCmd))
 | 
			
		||||
        }
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          engineCommandManager
 | 
			
		||||
            .sendSceneCommand(JSON.parse(customCmd))
 | 
			
		||||
            .catch(reportRejection)
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        Send custom command
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
@ -176,9 +176,11 @@ const FileTreeItem = ({
 | 
			
		||||
        `import("${fileOrDir.path.replace(project.path, '.')}")\n` +
 | 
			
		||||
          codeManager.code
 | 
			
		||||
      )
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      codeManager.writeToFile()
 | 
			
		||||
 | 
			
		||||
      // Prevent seeing the model built one piece at a time when changing files
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      kclManager.executeCode(true)
 | 
			
		||||
    } else {
 | 
			
		||||
      // Let the lsp servers know we closed a file.
 | 
			
		||||
@ -388,14 +390,14 @@ interface FileTreeProps {
 | 
			
		||||
export const FileTreeMenu = () => {
 | 
			
		||||
  const { send } = useFileContext()
 | 
			
		||||
 | 
			
		||||
  async function createFile() {
 | 
			
		||||
  function createFile() {
 | 
			
		||||
    send({
 | 
			
		||||
      type: 'Create file',
 | 
			
		||||
      data: { name: '', makeDir: false },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function createFolder() {
 | 
			
		||||
  function createFolder() {
 | 
			
		||||
    send({
 | 
			
		||||
      type: 'Create file',
 | 
			
		||||
      data: { name: '', makeDir: true },
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ import {
 | 
			
		||||
} from './ContextMenu'
 | 
			
		||||
import { Popover } from '@headlessui/react'
 | 
			
		||||
import { CustomIcon } from './CustomIcon'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
const CANVAS_SIZE = 80
 | 
			
		||||
const FRUSTUM_SIZE = 0.5
 | 
			
		||||
@ -67,7 +68,9 @@ export default function Gizmo() {
 | 
			
		||||
        <ContextMenuItem
 | 
			
		||||
          key={axisName}
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            sceneInfra.camControls.updateCameraToAxis(axisName as AxisNames)
 | 
			
		||||
            sceneInfra.camControls
 | 
			
		||||
              .updateCameraToAxis(axisName as AxisNames)
 | 
			
		||||
              .catch(reportRejection)
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {axisSemantic} view
 | 
			
		||||
@ -75,7 +78,7 @@ export default function Gizmo() {
 | 
			
		||||
      )),
 | 
			
		||||
      <ContextMenuItem
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          sceneInfra.camControls.resetCameraPosition()
 | 
			
		||||
          sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        Reset view
 | 
			
		||||
@ -299,7 +302,7 @@ const initializeMouseEvents = (
 | 
			
		||||
  const handleClick = () => {
 | 
			
		||||
    if (raycasterIntersect.current) {
 | 
			
		||||
      const axisName = raycasterIntersect.current.object.name as AxisNames
 | 
			
		||||
      sceneInfra.camControls.updateCameraToAxis(axisName)
 | 
			
		||||
      sceneInfra.camControls.updateCameraToAxis(axisName).catch(reportRejection)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import { createAndOpenNewProject } from 'lib/desktopFS'
 | 
			
		||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
 | 
			
		||||
import { useLspContext } from './LspProvider'
 | 
			
		||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
const HelpMenuDivider = () => (
 | 
			
		||||
  <div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
 | 
			
		||||
@ -115,7 +116,9 @@ export function HelpMenu(props: React.PropsWithChildren) {
 | 
			
		||||
            if (isInProject) {
 | 
			
		||||
              navigate(filePath + PATHS.ONBOARDING.INDEX)
 | 
			
		||||
            } else {
 | 
			
		||||
              createAndOpenNewProject({ onProjectOpen, navigate })
 | 
			
		||||
              createAndOpenNewProject({ onProjectOpen, navigate }).catch(
 | 
			
		||||
                reportRejection
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ import { CoreDumpManager } from 'lib/coredump'
 | 
			
		||||
import openWindow, { openExternalBrowserIfDesktop } from 'lib/openWindow'
 | 
			
		||||
import { NetworkMachineIndicator } from './NetworkMachineIndicator'
 | 
			
		||||
import { ModelStateIndicator } from './ModelStateIndicator'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
export function LowerRightControls({
 | 
			
		||||
  children,
 | 
			
		||||
@ -25,7 +26,7 @@ export function LowerRightControls({
 | 
			
		||||
  const linkOverrideClassName =
 | 
			
		||||
    '!text-chalkboard-70 hover:!text-chalkboard-80 dark:!text-chalkboard-40 dark:hover:!text-chalkboard-30'
 | 
			
		||||
 | 
			
		||||
  async function reportbug(event: {
 | 
			
		||||
  function reportbug(event: {
 | 
			
		||||
    preventDefault: () => void
 | 
			
		||||
    stopPropagation: () => void
 | 
			
		||||
  }) {
 | 
			
		||||
@ -34,7 +35,9 @@ export function LowerRightControls({
 | 
			
		||||
 | 
			
		||||
    if (!coreDumpManager) {
 | 
			
		||||
      // open default reporting option
 | 
			
		||||
      openWindow('https://github.com/KittyCAD/modeling-app/issues/new/choose')
 | 
			
		||||
      openWindow(
 | 
			
		||||
        'https://github.com/KittyCAD/modeling-app/issues/new/choose'
 | 
			
		||||
      ).catch(reportRejection)
 | 
			
		||||
    } else {
 | 
			
		||||
      toast
 | 
			
		||||
        .promise(
 | 
			
		||||
@ -56,7 +59,7 @@ export function LowerRightControls({
 | 
			
		||||
          if (err) {
 | 
			
		||||
            openWindow(
 | 
			
		||||
              'https://github.com/KittyCAD/modeling-app/issues/new/choose'
 | 
			
		||||
            )
 | 
			
		||||
            ).catch(reportRejection)
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -160,7 +160,9 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
 | 
			
		||||
                // Update the folding ranges, since the AST has changed.
 | 
			
		||||
                // This is a hack since codemirror does not support async foldService.
 | 
			
		||||
                // When they do we can delete this.
 | 
			
		||||
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
                plugin.updateFoldingRanges()
 | 
			
		||||
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
                plugin.requestSemanticTokens()
 | 
			
		||||
                break
 | 
			
		||||
              case 'kcl/memoryUpdated':
 | 
			
		||||
 | 
			
		||||
@ -73,7 +73,7 @@ import { EditorSelection, Transaction } from '@codemirror/state'
 | 
			
		||||
import { useNavigate, useSearchParams } from 'react-router-dom'
 | 
			
		||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
 | 
			
		||||
import { getVarNameModal } from 'hooks/useToolbarGuards'
 | 
			
		||||
import { err, trap } from 'lib/trap'
 | 
			
		||||
import { err, reportRejection, trap } from 'lib/trap'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { modelingMachineEvent } from 'editor/manager'
 | 
			
		||||
import { hasValidFilletSelection } from 'lang/modifyAst/addFillet'
 | 
			
		||||
@ -152,14 +152,17 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
 | 
			
		||||
            store.videoElement?.pause()
 | 
			
		||||
 | 
			
		||||
            kclManager.executeCode().then(() => {
 | 
			
		||||
              if (engineCommandManager.engineConnection?.idleMode) return
 | 
			
		||||
            kclManager
 | 
			
		||||
              .executeCode()
 | 
			
		||||
              .then(() => {
 | 
			
		||||
                if (engineCommandManager.engineConnection?.idleMode) return
 | 
			
		||||
 | 
			
		||||
              store.videoElement?.play().catch((e) => {
 | 
			
		||||
                console.warn('Video playing was prevented', e)
 | 
			
		||||
                store.videoElement?.play().catch((e) => {
 | 
			
		||||
                  console.warn('Video playing was prevented', e)
 | 
			
		||||
                })
 | 
			
		||||
              })
 | 
			
		||||
            })
 | 
			
		||||
          })()
 | 
			
		||||
              .catch(reportRejection)
 | 
			
		||||
          })().catch(reportRejection)
 | 
			
		||||
        },
 | 
			
		||||
        'Set mouse state': assign(({ context, event }) => {
 | 
			
		||||
          if (event.type !== 'Set mouse state') return {}
 | 
			
		||||
@ -316,9 +319,10 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
              })
 | 
			
		||||
              codeMirrorSelection && dispatchSelection(codeMirrorSelection)
 | 
			
		||||
              engineEvents &&
 | 
			
		||||
                engineEvents.forEach((event) =>
 | 
			
		||||
                engineEvents.forEach((event) => {
 | 
			
		||||
                  // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
                  engineCommandManager.sendSceneCommand(event)
 | 
			
		||||
                )
 | 
			
		||||
                })
 | 
			
		||||
              updateSceneObjectColors()
 | 
			
		||||
 | 
			
		||||
              return {
 | 
			
		||||
@ -349,9 +353,10 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
                  selections,
 | 
			
		||||
                })
 | 
			
		||||
              engineEvents &&
 | 
			
		||||
                engineEvents.forEach((event) =>
 | 
			
		||||
                engineEvents.forEach((event) => {
 | 
			
		||||
                  // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
                  engineCommandManager.sendSceneCommand(event)
 | 
			
		||||
                )
 | 
			
		||||
                })
 | 
			
		||||
              updateSceneObjectColors()
 | 
			
		||||
              return {
 | 
			
		||||
                selectionRanges: selections,
 | 
			
		||||
@ -378,7 +383,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            return {}
 | 
			
		||||
          }
 | 
			
		||||
        ),
 | 
			
		||||
        Make: async ({ event }) => {
 | 
			
		||||
        Make: ({ event }) => {
 | 
			
		||||
          if (event.type !== 'Make') return
 | 
			
		||||
          // Check if we already have an export intent.
 | 
			
		||||
          if (engineCommandManager.exportIntent) {
 | 
			
		||||
@ -410,19 +415,21 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Artificially delay the export in playwright tests
 | 
			
		||||
          toast.promise(
 | 
			
		||||
            exportFromEngine({
 | 
			
		||||
              format: format,
 | 
			
		||||
            }),
 | 
			
		||||
          toast
 | 
			
		||||
            .promise(
 | 
			
		||||
              exportFromEngine({
 | 
			
		||||
                format: format,
 | 
			
		||||
              }),
 | 
			
		||||
 | 
			
		||||
            {
 | 
			
		||||
              loading: 'Starting print...',
 | 
			
		||||
              success: 'Started print successfully',
 | 
			
		||||
              error: 'Error while starting print',
 | 
			
		||||
            }
 | 
			
		||||
          )
 | 
			
		||||
              {
 | 
			
		||||
                loading: 'Starting print...',
 | 
			
		||||
                success: 'Started print successfully',
 | 
			
		||||
                error: 'Error while starting print',
 | 
			
		||||
              }
 | 
			
		||||
            )
 | 
			
		||||
            .catch(reportRejection)
 | 
			
		||||
        },
 | 
			
		||||
        'Engine export': async ({ event }) => {
 | 
			
		||||
        'Engine export': ({ event }) => {
 | 
			
		||||
          if (event.type !== 'Export') return
 | 
			
		||||
          if (engineCommandManager.exportIntent) {
 | 
			
		||||
            toast.error('Already exporting')
 | 
			
		||||
@ -474,23 +481,25 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
            format.selection = { type: 'default_scene' }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          toast.promise(
 | 
			
		||||
            exportFromEngine({
 | 
			
		||||
              format: format as Models['OutputFormat_type'],
 | 
			
		||||
            }),
 | 
			
		||||
            {
 | 
			
		||||
              loading: 'Exporting...',
 | 
			
		||||
              success: 'Exported successfully',
 | 
			
		||||
              error: 'Error while exporting',
 | 
			
		||||
            }
 | 
			
		||||
          )
 | 
			
		||||
          toast
 | 
			
		||||
            .promise(
 | 
			
		||||
              exportFromEngine({
 | 
			
		||||
                format: format as Models['OutputFormat_type'],
 | 
			
		||||
              }),
 | 
			
		||||
              {
 | 
			
		||||
                loading: 'Exporting...',
 | 
			
		||||
                success: 'Exported successfully',
 | 
			
		||||
                error: 'Error while exporting',
 | 
			
		||||
              }
 | 
			
		||||
            )
 | 
			
		||||
            .catch(reportRejection)
 | 
			
		||||
        },
 | 
			
		||||
        'Submit to Text-to-CAD API': async ({ event }) => {
 | 
			
		||||
        'Submit to Text-to-CAD API': ({ event }) => {
 | 
			
		||||
          if (event.type !== 'Text-to-CAD') return
 | 
			
		||||
          const trimmedPrompt = event.data.prompt.trim()
 | 
			
		||||
          if (!trimmedPrompt) return
 | 
			
		||||
 | 
			
		||||
          void submitAndAwaitTextToKcl({
 | 
			
		||||
          submitAndAwaitTextToKcl({
 | 
			
		||||
            trimmedPrompt,
 | 
			
		||||
            fileMachineSend,
 | 
			
		||||
            navigate,
 | 
			
		||||
@ -501,7 +510,7 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
              theme: theme.current,
 | 
			
		||||
              highlightEdges: highlightEdges.current,
 | 
			
		||||
            },
 | 
			
		||||
          })
 | 
			
		||||
          }).catch(reportRejection)
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      guards: {
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import { editorShortcutMeta } from './KclEditorPane'
 | 
			
		||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
 | 
			
		||||
import { kclManager } from 'lib/singletons'
 | 
			
		||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
export const KclEditorMenu = ({ children }: PropsWithChildren) => {
 | 
			
		||||
  const { enable: convertToVarEnabled, handleClick: handleConvertToVarClick } =
 | 
			
		||||
@ -47,7 +48,9 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
 | 
			
		||||
          {convertToVarEnabled && (
 | 
			
		||||
            <Menu.Item>
 | 
			
		||||
              <button
 | 
			
		||||
                onClick={() => handleConvertToVarClick()}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  handleConvertToVarClick().catch(reportRejection)
 | 
			
		||||
                }}
 | 
			
		||||
                className={styles.button}
 | 
			
		||||
              >
 | 
			
		||||
                <span>Convert to Variable</span>
 | 
			
		||||
 | 
			
		||||
@ -57,6 +57,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
 | 
			
		||||
      icon: 'printer3d',
 | 
			
		||||
      iconClassName: '!p-0',
 | 
			
		||||
      keybinding: 'Ctrl + Shift + M',
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
      action: async () => {
 | 
			
		||||
        commandBarSend({
 | 
			
		||||
          type: 'Find and select command',
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,8 @@ import Tooltip from './Tooltip'
 | 
			
		||||
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
 | 
			
		||||
import { useNetworkContext } from '../hooks/useNetworkContext'
 | 
			
		||||
import { NetworkHealthState } from '../hooks/useNetworkStatus'
 | 
			
		||||
import { toSync } from 'lib/utils'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
 | 
			
		||||
  [NetworkHealthState.Ok]: 'Connected',
 | 
			
		||||
@ -160,13 +162,13 @@ export const NetworkHealthIndicator = () => {
 | 
			
		||||
              </div>
 | 
			
		||||
              {issues[name as ConnectingTypeGroup] && (
 | 
			
		||||
                <button
 | 
			
		||||
                  onClick={async () => {
 | 
			
		||||
                  onClick={toSync(async () => {
 | 
			
		||||
                    await navigator.clipboard.writeText(
 | 
			
		||||
                      JSON.stringify(error, null, 2) || ''
 | 
			
		||||
                    )
 | 
			
		||||
                    setHasCopied(true)
 | 
			
		||||
                    setTimeout(() => setHasCopied(false), 5000)
 | 
			
		||||
                  }}
 | 
			
		||||
                  }, reportRejection)}
 | 
			
		||||
                  className="flex w-fit gap-2 items-center bg-transparent text-sm p-1 py-0 my-0 -mx-1 text-destroy-80 dark:text-destroy-10 hover:bg-transparent border-transparent dark:border-transparent hover:border-destroy-80 dark:hover:border-destroy-80 dark:hover:bg-destroy-80"
 | 
			
		||||
                >
 | 
			
		||||
                  {hasCopied ? 'Copied' : 'Copy Error'}
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,8 @@ import Tooltip from '../Tooltip'
 | 
			
		||||
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
 | 
			
		||||
import { ProjectCardRenameForm } from './ProjectCardRenameForm'
 | 
			
		||||
import { Project } from 'lib/project'
 | 
			
		||||
import { toSync } from 'lib/utils'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
function ProjectCard({
 | 
			
		||||
  project,
 | 
			
		||||
@ -165,10 +167,10 @@ function ProjectCard({
 | 
			
		||||
      {isConfirmingDelete && (
 | 
			
		||||
        <DeleteConfirmationDialog
 | 
			
		||||
          title="Delete Project"
 | 
			
		||||
          onConfirm={async () => {
 | 
			
		||||
          onConfirm={toSync(async () => {
 | 
			
		||||
            await handleDeleteProject(project)
 | 
			
		||||
            setIsConfirmingDelete(false)
 | 
			
		||||
          }}
 | 
			
		||||
          }, reportRejection)}
 | 
			
		||||
          onDismiss={() => setIsConfirmingDelete(false)}
 | 
			
		||||
        >
 | 
			
		||||
          <p className="my-4">
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,8 @@ import React, { useMemo } from 'react'
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
import Tooltip from './Tooltip'
 | 
			
		||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
import { toSync } from 'lib/utils'
 | 
			
		||||
 | 
			
		||||
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
 | 
			
		||||
  const { auth } = useSettingsAuthContext()
 | 
			
		||||
@ -50,11 +52,12 @@ export const RefreshButton = ({ children }: React.PropsWithChildren) => {
 | 
			
		||||
        // Window may not be available in some environments
 | 
			
		||||
        window?.location.reload()
 | 
			
		||||
      })
 | 
			
		||||
      .catch(reportRejection)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <button
 | 
			
		||||
      onClick={refresh}
 | 
			
		||||
      onClick={toSync(refresh, reportRejection)}
 | 
			
		||||
      className="p-1 m-0 bg-chalkboard-10/80 dark:bg-chalkboard-100/50 hover:bg-chalkboard-10 dark:hover:bg-chalkboard-100 rounded-full border border-solid border-chalkboard-20 dark:border-chalkboard-90"
 | 
			
		||||
    >
 | 
			
		||||
      <CustomIcon name="exclamationMark" className="w-5 h-5" />
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,8 @@ import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/desktopFS'
 | 
			
		||||
import { useDotDotSlash } from 'hooks/useDotDotSlash'
 | 
			
		||||
import { ForwardedRef, forwardRef, useEffect } from 'react'
 | 
			
		||||
import { useLspContext } from 'components/LspProvider'
 | 
			
		||||
import { toSync } from 'lib/utils'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
interface AllSettingsFieldsProps {
 | 
			
		||||
  searchParamTab: SettingsLevel
 | 
			
		||||
@ -54,7 +56,7 @@ export const AllSettingsFields = forwardRef(
 | 
			
		||||
          )
 | 
			
		||||
        : undefined
 | 
			
		||||
 | 
			
		||||
    async function restartOnboarding() {
 | 
			
		||||
    function restartOnboarding() {
 | 
			
		||||
      send({
 | 
			
		||||
        type: `set.app.onboardingStatus`,
 | 
			
		||||
        data: { level: 'user', value: '' },
 | 
			
		||||
@ -82,6 +84,7 @@ export const AllSettingsFields = forwardRef(
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      navigateToOnboardingStart()
 | 
			
		||||
    }, [isFileSettings, navigate, state])
 | 
			
		||||
 | 
			
		||||
@ -190,7 +193,7 @@ export const AllSettingsFields = forwardRef(
 | 
			
		||||
              {isDesktop() && (
 | 
			
		||||
                <ActionButton
 | 
			
		||||
                  Element="button"
 | 
			
		||||
                  onClick={async () => {
 | 
			
		||||
                  onClick={toSync(async () => {
 | 
			
		||||
                    const paths = await getSettingsFolderPaths(
 | 
			
		||||
                      projectPath ? decodeURIComponent(projectPath) : undefined
 | 
			
		||||
                    )
 | 
			
		||||
@ -199,7 +202,7 @@ export const AllSettingsFields = forwardRef(
 | 
			
		||||
                      return new Error('finalPath undefined')
 | 
			
		||||
                    }
 | 
			
		||||
                    window.electron.showInFolder(finalPath)
 | 
			
		||||
                  }}
 | 
			
		||||
                  }, reportRejection)}
 | 
			
		||||
                  iconStart={{
 | 
			
		||||
                    icon: 'folder',
 | 
			
		||||
                    size: 'sm',
 | 
			
		||||
@ -211,14 +214,14 @@ export const AllSettingsFields = forwardRef(
 | 
			
		||||
              )}
 | 
			
		||||
              <ActionButton
 | 
			
		||||
                Element="button"
 | 
			
		||||
                onClick={async () => {
 | 
			
		||||
                onClick={toSync(async () => {
 | 
			
		||||
                  const defaultDirectory = await getInitialDefaultDir()
 | 
			
		||||
                  send({
 | 
			
		||||
                    type: 'Reset settings',
 | 
			
		||||
                    defaultDirectory,
 | 
			
		||||
                  })
 | 
			
		||||
                  toast.success('Settings restored to default')
 | 
			
		||||
                }}
 | 
			
		||||
                }, reportRejection)}
 | 
			
		||||
                iconStart={{
 | 
			
		||||
                  icon: 'refresh',
 | 
			
		||||
                  size: 'sm',
 | 
			
		||||
 | 
			
		||||
@ -108,6 +108,7 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
          sceneInfra.baseUnit = newBaseUnit
 | 
			
		||||
        },
 | 
			
		||||
        setEngineTheme: ({ context }) => {
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
          engineCommandManager.sendSceneCommand({
 | 
			
		||||
            cmd_id: uuidv4(),
 | 
			
		||||
            type: 'modeling_cmd_req',
 | 
			
		||||
@ -118,6 +119,7 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          const opposingTheme = getOppositeTheme(context.app.theme.current)
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
          engineCommandManager.sendSceneCommand({
 | 
			
		||||
            cmd_id: uuidv4(),
 | 
			
		||||
            type: 'modeling_cmd_req',
 | 
			
		||||
@ -137,6 +139,7 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
          sceneInfra.theme = opposingTheme
 | 
			
		||||
        },
 | 
			
		||||
        setEngineEdges: ({ context }) => {
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
          engineCommandManager.sendSceneCommand({
 | 
			
		||||
            cmd_id: uuidv4(),
 | 
			
		||||
            type: 'modeling_cmd_req',
 | 
			
		||||
@ -186,6 +189,7 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
              resetSettingsIncludesUnitChange
 | 
			
		||||
            ) {
 | 
			
		||||
              // Unit changes requires a re-exec of code
 | 
			
		||||
              // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
              kclManager.executeCode(true)
 | 
			
		||||
            } else {
 | 
			
		||||
              // For any future logging we'd like to do
 | 
			
		||||
@ -197,8 +201,10 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
            console.error('Error executing AST after settings change', e)
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        persistSettings: ({ context }) =>
 | 
			
		||||
          saveSettings(context, loadedProject?.project?.path),
 | 
			
		||||
        persistSettings: ({ context }) => {
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
          saveSettings(context, loadedProject?.project?.path)
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
    { input: loadedSettings }
 | 
			
		||||
@ -289,6 +295,7 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
      actions: {
 | 
			
		||||
        goToSignInPage: () => {
 | 
			
		||||
          navigate(PATHS.SIGN_IN)
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
          logout()
 | 
			
		||||
        },
 | 
			
		||||
        goToIndexPage: () => {
 | 
			
		||||
@ -330,13 +337,11 @@ export const SettingsAuthProviderBase = ({
 | 
			
		||||
 | 
			
		||||
export default SettingsAuthProvider
 | 
			
		||||
 | 
			
		||||
export function logout() {
 | 
			
		||||
export async function logout() {
 | 
			
		||||
  localStorage.removeItem(TOKEN_PERSIST_KEY)
 | 
			
		||||
  return (
 | 
			
		||||
    !isDesktop() &&
 | 
			
		||||
    fetch(withBaseUrl('/logout'), {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      credentials: 'include',
 | 
			
		||||
    })
 | 
			
		||||
  )
 | 
			
		||||
  if (isDesktop()) return Promise.resolve(null)
 | 
			
		||||
  return fetch(withBaseUrl('/logout'), {
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
    credentials: 'include',
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -53,9 +53,10 @@ export const Stream = () => {
 | 
			
		||||
   * executed. If we can find a way to do this from a more
 | 
			
		||||
   * central place, we can move this code there.
 | 
			
		||||
   */
 | 
			
		||||
  async function executeCodeAndPlayStream() {
 | 
			
		||||
    kclManager.executeCode(true).then(() => {
 | 
			
		||||
      videoRef.current?.play().catch((e) => {
 | 
			
		||||
  function executeCodeAndPlayStream() {
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    kclManager.executeCode(true).then(async () => {
 | 
			
		||||
      await videoRef.current?.play().catch((e) => {
 | 
			
		||||
        console.warn('Video playing was prevented', e, videoRef.current)
 | 
			
		||||
      })
 | 
			
		||||
      setStreamState(StreamState.Playing)
 | 
			
		||||
@ -218,12 +219,12 @@ export const Stream = () => {
 | 
			
		||||
   */
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!kclManager.isExecuting) {
 | 
			
		||||
      setTimeout(() =>
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        // execute in the next event loop
 | 
			
		||||
        videoRef.current?.play().catch((e) => {
 | 
			
		||||
          console.warn('Video playing was prevented', e, videoRef.current)
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }, [kclManager.isExecuting])
 | 
			
		||||
 | 
			
		||||
@ -290,6 +291,7 @@ export const Stream = () => {
 | 
			
		||||
    if (state.matches({ idle: 'showPlanes' })) return
 | 
			
		||||
 | 
			
		||||
    if (!context.store?.didDragInStream && btnName(e).left) {
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      sendSelectEventToEngine(
 | 
			
		||||
        e,
 | 
			
		||||
        videoRef.current,
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@ import { ActionButton } from './ActionButton'
 | 
			
		||||
import { commandBarMachine } from 'machines/commandBarMachine'
 | 
			
		||||
import { EventFrom } from 'xstate'
 | 
			
		||||
import { fileMachine } from 'machines/fileMachine'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
const CANVAS_SIZE = 128
 | 
			
		||||
const PROMPT_TRUNCATE_LENGTH = 128
 | 
			
		||||
@ -297,7 +298,7 @@ export function ToastTextToCadSuccess({
 | 
			
		||||
            name={hasCopied ? 'Close' : 'Reject'}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              if (!hasCopied) {
 | 
			
		||||
                sendTelemetry(modelId, 'rejected', token)
 | 
			
		||||
                sendTelemetry(modelId, 'rejected', token).catch(reportRejection)
 | 
			
		||||
              }
 | 
			
		||||
              if (isDesktop()) {
 | 
			
		||||
                // Delete the file from the project
 | 
			
		||||
@ -323,6 +324,7 @@ export function ToastTextToCadSuccess({
 | 
			
		||||
              }}
 | 
			
		||||
              name="Accept"
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
                sendTelemetry(modelId, 'accepted', token)
 | 
			
		||||
                navigate(
 | 
			
		||||
                  `${PATHS.FILE}/${encodeURIComponent(
 | 
			
		||||
@ -342,7 +344,9 @@ export function ToastTextToCadSuccess({
 | 
			
		||||
              }}
 | 
			
		||||
              name="Copy to clipboard"
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
                sendTelemetry(modelId, 'accepted', token)
 | 
			
		||||
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
                navigator.clipboard.writeText(data.code || '// no code found')
 | 
			
		||||
                setShowCopiedUi(true)
 | 
			
		||||
                setHasCopied(true)
 | 
			
		||||
 | 
			
		||||
@ -285,8 +285,9 @@ export default class EditorManager {
 | 
			
		||||
 | 
			
		||||
    this._lastEvent = { event: stringEvent, time: Date.now() }
 | 
			
		||||
    this._modelingSend(eventInfo.modelingEvent)
 | 
			
		||||
    eventInfo.engineEvents.forEach((event) =>
 | 
			
		||||
    eventInfo.engineEvents.forEach((event) => {
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      engineCommandManager.sendSceneCommand(event)
 | 
			
		||||
    )
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,7 @@ import { CopilotCompletionResponse } from 'wasm-lib/kcl/bindings/CopilotCompleti
 | 
			
		||||
import { CopilotAcceptCompletionParams } from 'wasm-lib/kcl/bindings/CopilotAcceptCompletionParams'
 | 
			
		||||
import { CopilotRejectCompletionParams } from 'wasm-lib/kcl/bindings/CopilotRejectCompletionParams'
 | 
			
		||||
import { editorManager } from 'lib/singletons'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
const copilotPluginAnnotation = Annotation.define<boolean>()
 | 
			
		||||
export const copilotPluginEvent = copilotPluginAnnotation.of(true)
 | 
			
		||||
@ -266,7 +267,7 @@ export class CompletionRequester implements PluginValue {
 | 
			
		||||
 | 
			
		||||
    if (!this.client.ready) return
 | 
			
		||||
    try {
 | 
			
		||||
      this.requestCompletions()
 | 
			
		||||
      this.requestCompletions().catch(reportRejection)
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error(e)
 | 
			
		||||
    }
 | 
			
		||||
@ -462,7 +463,7 @@ export class CompletionRequester implements PluginValue {
 | 
			
		||||
      annotations: [copilotPluginEvent, Transaction.addToHistory.of(true)],
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.accept(ghostText.uuid)
 | 
			
		||||
    this.accept(ghostText.uuid).catch(reportRejection)
 | 
			
		||||
    return true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -490,7 +491,7 @@ export class CompletionRequester implements PluginValue {
 | 
			
		||||
      ],
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    this.reject()
 | 
			
		||||
    this.reject().catch(reportRejection)
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -96,6 +96,7 @@ export class KclPlugin implements PluginValue {
 | 
			
		||||
 | 
			
		||||
    const newCode = viewUpdate.state.doc.toString()
 | 
			
		||||
    codeManager.code = newCode
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    codeManager.writeToFile()
 | 
			
		||||
 | 
			
		||||
    this.scheduleUpdateDoc()
 | 
			
		||||
@ -117,6 +118,7 @@ export class KclPlugin implements PluginValue {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.client.ready) return
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    kclManager.executeCode()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ import {
 | 
			
		||||
  CopilotWorkerOptions,
 | 
			
		||||
} from 'editor/plugins/lsp/types'
 | 
			
		||||
import { EngineCommandManager } from 'lang/std/engineConnection'
 | 
			
		||||
import { err } from 'lib/trap'
 | 
			
		||||
import { err, reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
const intoServer: IntoServer = new IntoServer()
 | 
			
		||||
const fromServer: FromServer | Error = FromServer.create()
 | 
			
		||||
@ -60,7 +60,8 @@ export async function kclLspRun(
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onmessage = function (event) {
 | 
			
		||||
// WebWorker message handler.
 | 
			
		||||
onmessage = function (event: MessageEvent) {
 | 
			
		||||
  if (err(fromServer)) return
 | 
			
		||||
  const { worker, eventType, eventData }: LspWorkerEvent = event.data
 | 
			
		||||
 | 
			
		||||
@ -70,7 +71,7 @@ onmessage = function (event) {
 | 
			
		||||
        | KclWorkerOptions
 | 
			
		||||
        | CopilotWorkerOptions
 | 
			
		||||
      initialise(wasmUrl)
 | 
			
		||||
        .then((instantiatedModule) => {
 | 
			
		||||
        .then(async (instantiatedModule) => {
 | 
			
		||||
          console.log('Worker: WASM module loaded', worker, instantiatedModule)
 | 
			
		||||
          const config = new ServerConfig(
 | 
			
		||||
            intoServer,
 | 
			
		||||
@ -81,7 +82,7 @@ onmessage = function (event) {
 | 
			
		||||
          switch (worker) {
 | 
			
		||||
            case LspWorker.Kcl:
 | 
			
		||||
              const kclData = eventData as KclWorkerOptions
 | 
			
		||||
              kclLspRun(
 | 
			
		||||
              await kclLspRun(
 | 
			
		||||
                config,
 | 
			
		||||
                null,
 | 
			
		||||
                kclData.token,
 | 
			
		||||
@ -91,7 +92,11 @@ onmessage = function (event) {
 | 
			
		||||
              break
 | 
			
		||||
            case LspWorker.Copilot:
 | 
			
		||||
              let copilotData = eventData as CopilotWorkerOptions
 | 
			
		||||
              copilotLspRun(config, copilotData.token, copilotData.apiBaseUrl)
 | 
			
		||||
              await copilotLspRun(
 | 
			
		||||
                config,
 | 
			
		||||
                copilotData.token,
 | 
			
		||||
                copilotData.apiBaseUrl
 | 
			
		||||
              )
 | 
			
		||||
              break
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
@ -104,7 +109,7 @@ onmessage = function (event) {
 | 
			
		||||
      intoServer.enqueue(data)
 | 
			
		||||
      const json: jsrpc.JSONRPCRequest = Codec.decode(data)
 | 
			
		||||
      if (null != json.id) {
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-floating-promises, @typescript-eslint/no-non-null-assertion
 | 
			
		||||
        fromServer.responses.get(json.id)!.then((response) => {
 | 
			
		||||
          const encoded = Codec.encode(response as jsrpc.JSONRPCResponse)
 | 
			
		||||
          postMessage(encoded)
 | 
			
		||||
@ -115,19 +120,17 @@ onmessage = function (event) {
 | 
			
		||||
      console.error('Worker: Unknown message type', worker, eventType)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
new Promise<void>(async (resolve) => {
 | 
			
		||||
;(async () => {
 | 
			
		||||
  if (err(fromServer)) return
 | 
			
		||||
  for await (const requests of fromServer.requests) {
 | 
			
		||||
    const encoded = Codec.encode(requests as jsrpc.JSONRPCRequest)
 | 
			
		||||
    postMessage(encoded)
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
new Promise<void>(async (resolve) => {
 | 
			
		||||
})().catch(reportRejection)
 | 
			
		||||
;(async () => {
 | 
			
		||||
  if (err(fromServer)) return
 | 
			
		||||
  for await (const notification of fromServer.notifications) {
 | 
			
		||||
    const encoded = Codec.encode(notification as jsrpc.JSONRPCRequest)
 | 
			
		||||
    postMessage(encoded)
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
})().catch(reportRejection)
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ import {
 | 
			
		||||
  getSolid2dCodeRef,
 | 
			
		||||
  getWallCodeRef,
 | 
			
		||||
} from 'lang/std/artifactGraph'
 | 
			
		||||
import { err } from 'lib/trap'
 | 
			
		||||
import { err, reportRejection } from 'lib/trap'
 | 
			
		||||
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
 | 
			
		||||
import { getNodePathFromSourceRange } from 'lang/queryAst'
 | 
			
		||||
 | 
			
		||||
@ -86,9 +86,11 @@ export function useEngineConnectionSubscriptions() {
 | 
			
		||||
    })
 | 
			
		||||
    const unSubClick = engineCommandManager.subscribeTo({
 | 
			
		||||
      event: 'select_with_point',
 | 
			
		||||
      callback: async (engineEvent) => {
 | 
			
		||||
        const event = await getEventForSelectWithPoint(engineEvent)
 | 
			
		||||
        event && send(event)
 | 
			
		||||
      callback: (engineEvent) => {
 | 
			
		||||
        ;(async () => {
 | 
			
		||||
          const event = await getEventForSelectWithPoint(engineEvent)
 | 
			
		||||
          event && send(event)
 | 
			
		||||
        })().catch(reportRejection)
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
    return () => {
 | 
			
		||||
@ -101,118 +103,120 @@ export function useEngineConnectionSubscriptions() {
 | 
			
		||||
    const unSub = engineCommandManager.subscribeTo({
 | 
			
		||||
      event: 'select_with_point',
 | 
			
		||||
      callback: state.matches('Sketch no face')
 | 
			
		||||
        ? async ({ data }) => {
 | 
			
		||||
            let planeOrFaceId = data.entity_id
 | 
			
		||||
            if (!planeOrFaceId) return
 | 
			
		||||
            if (
 | 
			
		||||
              engineCommandManager.defaultPlanes?.xy === planeOrFaceId ||
 | 
			
		||||
              engineCommandManager.defaultPlanes?.xz === planeOrFaceId ||
 | 
			
		||||
              engineCommandManager.defaultPlanes?.yz === planeOrFaceId ||
 | 
			
		||||
              engineCommandManager.defaultPlanes?.negXy === planeOrFaceId ||
 | 
			
		||||
              engineCommandManager.defaultPlanes?.negXz === planeOrFaceId ||
 | 
			
		||||
              engineCommandManager.defaultPlanes?.negYz === planeOrFaceId
 | 
			
		||||
            ) {
 | 
			
		||||
              let planeId = planeOrFaceId
 | 
			
		||||
              const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
 | 
			
		||||
                [engineCommandManager.defaultPlanes.xy]: 'XY',
 | 
			
		||||
                [engineCommandManager.defaultPlanes.xz]: 'XZ',
 | 
			
		||||
                [engineCommandManager.defaultPlanes.yz]: 'YZ',
 | 
			
		||||
                [engineCommandManager.defaultPlanes.negXy]: '-XY',
 | 
			
		||||
                [engineCommandManager.defaultPlanes.negXz]: '-XZ',
 | 
			
		||||
                [engineCommandManager.defaultPlanes.negYz]: '-YZ',
 | 
			
		||||
              }
 | 
			
		||||
              // TODO can we get this information from rust land when it creates the default planes?
 | 
			
		||||
              // maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
 | 
			
		||||
              let zAxis: [number, number, number] = [0, 0, 1]
 | 
			
		||||
              let yAxis: [number, number, number] = [0, 1, 0]
 | 
			
		||||
        ? ({ data }) => {
 | 
			
		||||
            ;(async () => {
 | 
			
		||||
              let planeOrFaceId = data.entity_id
 | 
			
		||||
              if (!planeOrFaceId) return
 | 
			
		||||
              if (
 | 
			
		||||
                engineCommandManager.defaultPlanes?.xy === planeOrFaceId ||
 | 
			
		||||
                engineCommandManager.defaultPlanes?.xz === planeOrFaceId ||
 | 
			
		||||
                engineCommandManager.defaultPlanes?.yz === planeOrFaceId ||
 | 
			
		||||
                engineCommandManager.defaultPlanes?.negXy === planeOrFaceId ||
 | 
			
		||||
                engineCommandManager.defaultPlanes?.negXz === planeOrFaceId ||
 | 
			
		||||
                engineCommandManager.defaultPlanes?.negYz === planeOrFaceId
 | 
			
		||||
              ) {
 | 
			
		||||
                let planeId = planeOrFaceId
 | 
			
		||||
                const defaultPlaneStrMap: Record<string, DefaultPlaneStr> = {
 | 
			
		||||
                  [engineCommandManager.defaultPlanes.xy]: 'XY',
 | 
			
		||||
                  [engineCommandManager.defaultPlanes.xz]: 'XZ',
 | 
			
		||||
                  [engineCommandManager.defaultPlanes.yz]: 'YZ',
 | 
			
		||||
                  [engineCommandManager.defaultPlanes.negXy]: '-XY',
 | 
			
		||||
                  [engineCommandManager.defaultPlanes.negXz]: '-XZ',
 | 
			
		||||
                  [engineCommandManager.defaultPlanes.negYz]: '-YZ',
 | 
			
		||||
                }
 | 
			
		||||
                // TODO can we get this information from rust land when it creates the default planes?
 | 
			
		||||
                // maybe returned from make_default_planes (src/wasm-lib/src/wasm.rs)
 | 
			
		||||
                let zAxis: [number, number, number] = [0, 0, 1]
 | 
			
		||||
                let yAxis: [number, number, number] = [0, 1, 0]
 | 
			
		||||
 | 
			
		||||
              // get unit vector from camera position to target
 | 
			
		||||
              const camVector = sceneInfra.camControls.camera.position
 | 
			
		||||
                .clone()
 | 
			
		||||
                .sub(sceneInfra.camControls.target)
 | 
			
		||||
                // get unit vector from camera position to target
 | 
			
		||||
                const camVector = sceneInfra.camControls.camera.position
 | 
			
		||||
                  .clone()
 | 
			
		||||
                  .sub(sceneInfra.camControls.target)
 | 
			
		||||
 | 
			
		||||
              if (engineCommandManager.defaultPlanes?.xy === planeId) {
 | 
			
		||||
                zAxis = [0, 0, 1]
 | 
			
		||||
                yAxis = [0, 1, 0]
 | 
			
		||||
                if (camVector.z < 0) {
 | 
			
		||||
                  zAxis = [0, 0, -1]
 | 
			
		||||
                  planeId = engineCommandManager.defaultPlanes?.negXy || ''
 | 
			
		||||
                }
 | 
			
		||||
              } else if (engineCommandManager.defaultPlanes?.yz === planeId) {
 | 
			
		||||
                zAxis = [1, 0, 0]
 | 
			
		||||
                yAxis = [0, 0, 1]
 | 
			
		||||
                if (camVector.x < 0) {
 | 
			
		||||
                  zAxis = [-1, 0, 0]
 | 
			
		||||
                  planeId = engineCommandManager.defaultPlanes?.negYz || ''
 | 
			
		||||
                }
 | 
			
		||||
              } else if (engineCommandManager.defaultPlanes?.xz === planeId) {
 | 
			
		||||
                zAxis = [0, 1, 0]
 | 
			
		||||
                yAxis = [0, 0, 1]
 | 
			
		||||
                planeId = engineCommandManager.defaultPlanes?.negXz || ''
 | 
			
		||||
                if (camVector.y < 0) {
 | 
			
		||||
                  zAxis = [0, -1, 0]
 | 
			
		||||
                  planeId = engineCommandManager.defaultPlanes?.xz || ''
 | 
			
		||||
                if (engineCommandManager.defaultPlanes?.xy === planeId) {
 | 
			
		||||
                  zAxis = [0, 0, 1]
 | 
			
		||||
                  yAxis = [0, 1, 0]
 | 
			
		||||
                  if (camVector.z < 0) {
 | 
			
		||||
                    zAxis = [0, 0, -1]
 | 
			
		||||
                    planeId = engineCommandManager.defaultPlanes?.negXy || ''
 | 
			
		||||
                  }
 | 
			
		||||
                } else if (engineCommandManager.defaultPlanes?.yz === planeId) {
 | 
			
		||||
                  zAxis = [1, 0, 0]
 | 
			
		||||
                  yAxis = [0, 0, 1]
 | 
			
		||||
                  if (camVector.x < 0) {
 | 
			
		||||
                    zAxis = [-1, 0, 0]
 | 
			
		||||
                    planeId = engineCommandManager.defaultPlanes?.negYz || ''
 | 
			
		||||
                  }
 | 
			
		||||
                } else if (engineCommandManager.defaultPlanes?.xz === planeId) {
 | 
			
		||||
                  zAxis = [0, 1, 0]
 | 
			
		||||
                  yAxis = [0, 0, 1]
 | 
			
		||||
                  planeId = engineCommandManager.defaultPlanes?.negXz || ''
 | 
			
		||||
                  if (camVector.y < 0) {
 | 
			
		||||
                    zAxis = [0, -1, 0]
 | 
			
		||||
                    planeId = engineCommandManager.defaultPlanes?.xz || ''
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                sceneInfra.modelingSend({
 | 
			
		||||
                  type: 'Select default plane',
 | 
			
		||||
                  data: {
 | 
			
		||||
                    type: 'defaultPlane',
 | 
			
		||||
                    planeId: planeId,
 | 
			
		||||
                    plane: defaultPlaneStrMap[planeId],
 | 
			
		||||
                    zAxis,
 | 
			
		||||
                    yAxis,
 | 
			
		||||
                  },
 | 
			
		||||
                })
 | 
			
		||||
                return
 | 
			
		||||
              }
 | 
			
		||||
              const faceId = planeOrFaceId
 | 
			
		||||
              const artifact = engineCommandManager.artifactGraph.get(faceId)
 | 
			
		||||
              const extrusion = getExtrusionFromSuspectedExtrudeSurface(
 | 
			
		||||
                faceId,
 | 
			
		||||
                engineCommandManager.artifactGraph
 | 
			
		||||
              )
 | 
			
		||||
 | 
			
		||||
              if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return
 | 
			
		||||
 | 
			
		||||
              const codeRef =
 | 
			
		||||
                artifact.type === 'cap'
 | 
			
		||||
                  ? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
 | 
			
		||||
                  : getWallCodeRef(artifact, engineCommandManager.artifactGraph)
 | 
			
		||||
 | 
			
		||||
              const faceInfo = await getFaceDetails(faceId)
 | 
			
		||||
              if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
 | 
			
		||||
                return
 | 
			
		||||
              const { z_axis, y_axis, origin } = faceInfo
 | 
			
		||||
              const sketchPathToNode = getNodePathFromSourceRange(
 | 
			
		||||
                kclManager.ast,
 | 
			
		||||
                err(codeRef) ? [0, 0] : codeRef.range
 | 
			
		||||
              )
 | 
			
		||||
 | 
			
		||||
              const extrudePathToNode = !err(extrusion)
 | 
			
		||||
                ? getNodePathFromSourceRange(
 | 
			
		||||
                    kclManager.ast,
 | 
			
		||||
                    extrusion.codeRef.range
 | 
			
		||||
                  )
 | 
			
		||||
                : []
 | 
			
		||||
 | 
			
		||||
              sceneInfra.modelingSend({
 | 
			
		||||
                type: 'Select default plane',
 | 
			
		||||
                data: {
 | 
			
		||||
                  type: 'defaultPlane',
 | 
			
		||||
                  planeId: planeId,
 | 
			
		||||
                  plane: defaultPlaneStrMap[planeId],
 | 
			
		||||
                  zAxis,
 | 
			
		||||
                  yAxis,
 | 
			
		||||
                  type: 'extrudeFace',
 | 
			
		||||
                  zAxis: [z_axis.x, z_axis.y, z_axis.z],
 | 
			
		||||
                  yAxis: [y_axis.x, y_axis.y, y_axis.z],
 | 
			
		||||
                  position: [origin.x, origin.y, origin.z].map(
 | 
			
		||||
                    (num) => num / sceneInfra._baseUnitMultiplier
 | 
			
		||||
                  ) as [number, number, number],
 | 
			
		||||
                  sketchPathToNode,
 | 
			
		||||
                  extrudePathToNode,
 | 
			
		||||
                  cap: artifact.type === 'cap' ? artifact.subType : 'none',
 | 
			
		||||
                  faceId: faceId,
 | 
			
		||||
                },
 | 
			
		||||
              })
 | 
			
		||||
              return
 | 
			
		||||
            }
 | 
			
		||||
            const faceId = planeOrFaceId
 | 
			
		||||
            const artifact = engineCommandManager.artifactGraph.get(faceId)
 | 
			
		||||
            const extrusion = getExtrusionFromSuspectedExtrudeSurface(
 | 
			
		||||
              faceId,
 | 
			
		||||
              engineCommandManager.artifactGraph
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if (artifact?.type !== 'cap' && artifact?.type !== 'wall') return
 | 
			
		||||
 | 
			
		||||
            const codeRef =
 | 
			
		||||
              artifact.type === 'cap'
 | 
			
		||||
                ? getCapCodeRef(artifact, engineCommandManager.artifactGraph)
 | 
			
		||||
                : getWallCodeRef(artifact, engineCommandManager.artifactGraph)
 | 
			
		||||
 | 
			
		||||
            const faceInfo = await getFaceDetails(faceId)
 | 
			
		||||
            if (!faceInfo?.origin || !faceInfo?.z_axis || !faceInfo?.y_axis)
 | 
			
		||||
              return
 | 
			
		||||
            const { z_axis, y_axis, origin } = faceInfo
 | 
			
		||||
            const sketchPathToNode = getNodePathFromSourceRange(
 | 
			
		||||
              kclManager.ast,
 | 
			
		||||
              err(codeRef) ? [0, 0] : codeRef.range
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            const extrudePathToNode = !err(extrusion)
 | 
			
		||||
              ? getNodePathFromSourceRange(
 | 
			
		||||
                  kclManager.ast,
 | 
			
		||||
                  extrusion.codeRef.range
 | 
			
		||||
                )
 | 
			
		||||
              : []
 | 
			
		||||
 | 
			
		||||
            sceneInfra.modelingSend({
 | 
			
		||||
              type: 'Select default plane',
 | 
			
		||||
              data: {
 | 
			
		||||
                type: 'extrudeFace',
 | 
			
		||||
                zAxis: [z_axis.x, z_axis.y, z_axis.z],
 | 
			
		||||
                yAxis: [y_axis.x, y_axis.y, y_axis.z],
 | 
			
		||||
                position: [origin.x, origin.y, origin.z].map(
 | 
			
		||||
                  (num) => num / sceneInfra._baseUnitMultiplier
 | 
			
		||||
                ) as [number, number, number],
 | 
			
		||||
                sketchPathToNode,
 | 
			
		||||
                extrudePathToNode,
 | 
			
		||||
                cap: artifact.type === 'cap' ? artifact.subType : 'none',
 | 
			
		||||
                faceId: faceId,
 | 
			
		||||
              },
 | 
			
		||||
            })
 | 
			
		||||
            return
 | 
			
		||||
            })().catch(reportRejection)
 | 
			
		||||
          }
 | 
			
		||||
        : () => {},
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@ -3,13 +3,14 @@ import {
 | 
			
		||||
  createSetVarNameModal,
 | 
			
		||||
} from 'components/SetVarNameModal'
 | 
			
		||||
import { editorManager, kclManager } from 'lib/singletons'
 | 
			
		||||
import { trap } from 'lib/trap'
 | 
			
		||||
import { reportRejection, trap } from 'lib/trap'
 | 
			
		||||
import { moveValueIntoNewVariable } from 'lang/modifyAst'
 | 
			
		||||
import { isNodeSafeToReplace } from 'lang/queryAst'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { useModelingContext } from './useModelingContext'
 | 
			
		||||
import { PathToNode, SourceRange } from 'lang/wasm'
 | 
			
		||||
import { useKclContext } from 'lang/KclProvider'
 | 
			
		||||
import { toSync } from 'lib/utils'
 | 
			
		||||
 | 
			
		||||
export const getVarNameModal = createSetVarNameModal(SetVarNameModal)
 | 
			
		||||
 | 
			
		||||
@ -62,7 +63,7 @@ export function useConvertToVariable(range?: SourceRange) {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  editorManager.convertToVariableCallback = handleClick
 | 
			
		||||
  editorManager.convertToVariableCallback = toSync(handleClick, reportRejection)
 | 
			
		||||
 | 
			
		||||
  return { enable, handleClick }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -129,8 +129,8 @@ export class KclManager {
 | 
			
		||||
    if (!isExecuting && this.executeIsStale) {
 | 
			
		||||
      const args = this.executeIsStale
 | 
			
		||||
      this.executeIsStale = null
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      this.executeAst(args)
 | 
			
		||||
    } else {
 | 
			
		||||
    }
 | 
			
		||||
    this._isExecutingCallback(isExecuting)
 | 
			
		||||
  }
 | 
			
		||||
@ -154,6 +154,7 @@ export class KclManager {
 | 
			
		||||
  constructor(engineCommandManager: EngineCommandManager) {
 | 
			
		||||
    this.engineCommandManager = engineCommandManager
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    this.ensureWasmInit().then(() => {
 | 
			
		||||
      this.ast = this.safeParse(codeManager.code) || this.ast
 | 
			
		||||
    })
 | 
			
		||||
@ -400,9 +401,11 @@ export class KclManager {
 | 
			
		||||
    // Update the code state and the editor.
 | 
			
		||||
    codeManager.updateCodeStateEditor(code)
 | 
			
		||||
    // Write back to the file system.
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    codeManager.writeToFile()
 | 
			
		||||
 | 
			
		||||
    // execute the code.
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    this.executeCode()
 | 
			
		||||
  }
 | 
			
		||||
  // There's overlapping responsibility between updateAst and executeAst.
 | 
			
		||||
@ -541,6 +544,7 @@ function defaultSelectionFilter(
 | 
			
		||||
  programMemory: ProgramMemory,
 | 
			
		||||
  engineCommandManager: EngineCommandManager
 | 
			
		||||
) {
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
  programMemory.hasSketchOrExtrudeGroup() &&
 | 
			
		||||
    engineCommandManager.sendSceneCommand({
 | 
			
		||||
      type: 'modeling_cmd_req',
 | 
			
		||||
 | 
			
		||||
@ -64,6 +64,7 @@ export async function executeAst({
 | 
			
		||||
  try {
 | 
			
		||||
    if (!useFakeExecutor) {
 | 
			
		||||
      engineCommandManager.endSession()
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      engineCommandManager.startNewSession()
 | 
			
		||||
    }
 | 
			
		||||
    const programMemory = await (useFakeExecutor
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { Selection } from 'lib/selections'
 | 
			
		||||
import { err, trap } from 'lib/trap'
 | 
			
		||||
import { err, reportRejection, trap } from 'lib/trap'
 | 
			
		||||
import {
 | 
			
		||||
  Program,
 | 
			
		||||
  CallExpression,
 | 
			
		||||
@ -938,115 +938,119 @@ export async function deleteFromSelection(
 | 
			
		||||
    const expressionIndex = pathToNode[1][0] as number
 | 
			
		||||
    astClone.body.splice(expressionIndex, 1)
 | 
			
		||||
    if (extrudeNameToDelete) {
 | 
			
		||||
      await new Promise(async (resolve) => {
 | 
			
		||||
        let currentVariableName = ''
 | 
			
		||||
        const pathsDependingOnExtrude: Array<{
 | 
			
		||||
          path: PathToNode
 | 
			
		||||
          sketchName: string
 | 
			
		||||
        }> = []
 | 
			
		||||
        traverse(astClone, {
 | 
			
		||||
          leave: (node) => {
 | 
			
		||||
            if (node.type === 'VariableDeclaration') {
 | 
			
		||||
              currentVariableName = ''
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          enter: async (node, path) => {
 | 
			
		||||
            if (node.type === 'VariableDeclaration') {
 | 
			
		||||
              currentVariableName = node.declarations[0].id.name
 | 
			
		||||
            }
 | 
			
		||||
            if (
 | 
			
		||||
              // match startSketchOn(${extrudeNameToDelete})
 | 
			
		||||
              node.type === 'CallExpression' &&
 | 
			
		||||
              node.callee.name === 'startSketchOn' &&
 | 
			
		||||
              node.arguments[0].type === 'Identifier' &&
 | 
			
		||||
              node.arguments[0].name === extrudeNameToDelete
 | 
			
		||||
            ) {
 | 
			
		||||
              pathsDependingOnExtrude.push({
 | 
			
		||||
                path,
 | 
			
		||||
                sketchName: currentVariableName,
 | 
			
		||||
              })
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
        const roundLiteral = (x: number) => createLiteral(roundOff(x))
 | 
			
		||||
        const modificationDetails: {
 | 
			
		||||
          parent: PipeExpression['body']
 | 
			
		||||
          faceDetails: Models['FaceIsPlanar_type']
 | 
			
		||||
          lastKey: number
 | 
			
		||||
        }[] = []
 | 
			
		||||
        for (const { path, sketchName } of pathsDependingOnExtrude) {
 | 
			
		||||
          const parent = getNodeFromPath<PipeExpression['body']>(
 | 
			
		||||
            astClone,
 | 
			
		||||
            path.slice(0, -1)
 | 
			
		||||
          )
 | 
			
		||||
          if (err(parent)) {
 | 
			
		||||
            return
 | 
			
		||||
          }
 | 
			
		||||
          const sketchToPreserve = sketchGroupFromKclValue(
 | 
			
		||||
            programMemory.get(sketchName),
 | 
			
		||||
            sketchName
 | 
			
		||||
          )
 | 
			
		||||
          if (err(sketchToPreserve)) return sketchToPreserve
 | 
			
		||||
          console.log('sketchName', sketchName)
 | 
			
		||||
          // Can't kick off multiple requests at once as getFaceDetails
 | 
			
		||||
          // is three engine calls in one and they conflict
 | 
			
		||||
          const faceDetails = await getFaceDetails(sketchToPreserve.on.id)
 | 
			
		||||
          if (
 | 
			
		||||
            !(
 | 
			
		||||
              faceDetails.origin &&
 | 
			
		||||
              faceDetails.x_axis &&
 | 
			
		||||
              faceDetails.y_axis &&
 | 
			
		||||
              faceDetails.z_axis
 | 
			
		||||
            )
 | 
			
		||||
          ) {
 | 
			
		||||
            return
 | 
			
		||||
          }
 | 
			
		||||
          const lastKey = Number(path.slice(-1)[0][0])
 | 
			
		||||
          modificationDetails.push({
 | 
			
		||||
            parent: parent.node,
 | 
			
		||||
            faceDetails,
 | 
			
		||||
            lastKey,
 | 
			
		||||
      await new Promise((resolve) => {
 | 
			
		||||
        ;(async () => {
 | 
			
		||||
          let currentVariableName = ''
 | 
			
		||||
          const pathsDependingOnExtrude: Array<{
 | 
			
		||||
            path: PathToNode
 | 
			
		||||
            sketchName: string
 | 
			
		||||
          }> = []
 | 
			
		||||
          traverse(astClone, {
 | 
			
		||||
            leave: (node) => {
 | 
			
		||||
              if (node.type === 'VariableDeclaration') {
 | 
			
		||||
                currentVariableName = ''
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            enter: (node, path) => {
 | 
			
		||||
              ;(async () => {
 | 
			
		||||
                if (node.type === 'VariableDeclaration') {
 | 
			
		||||
                  currentVariableName = node.declarations[0].id.name
 | 
			
		||||
                }
 | 
			
		||||
                if (
 | 
			
		||||
                  // match startSketchOn(${extrudeNameToDelete})
 | 
			
		||||
                  node.type === 'CallExpression' &&
 | 
			
		||||
                  node.callee.name === 'startSketchOn' &&
 | 
			
		||||
                  node.arguments[0].type === 'Identifier' &&
 | 
			
		||||
                  node.arguments[0].name === extrudeNameToDelete
 | 
			
		||||
                ) {
 | 
			
		||||
                  pathsDependingOnExtrude.push({
 | 
			
		||||
                    path,
 | 
			
		||||
                    sketchName: currentVariableName,
 | 
			
		||||
                  })
 | 
			
		||||
                }
 | 
			
		||||
              })().catch(reportRejection)
 | 
			
		||||
            },
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
        for (const { parent, faceDetails, lastKey } of modificationDetails) {
 | 
			
		||||
          if (
 | 
			
		||||
            !(
 | 
			
		||||
              faceDetails.origin &&
 | 
			
		||||
              faceDetails.x_axis &&
 | 
			
		||||
              faceDetails.y_axis &&
 | 
			
		||||
              faceDetails.z_axis
 | 
			
		||||
          const roundLiteral = (x: number) => createLiteral(roundOff(x))
 | 
			
		||||
          const modificationDetails: {
 | 
			
		||||
            parent: PipeExpression['body']
 | 
			
		||||
            faceDetails: Models['FaceIsPlanar_type']
 | 
			
		||||
            lastKey: number
 | 
			
		||||
          }[] = []
 | 
			
		||||
          for (const { path, sketchName } of pathsDependingOnExtrude) {
 | 
			
		||||
            const parent = getNodeFromPath<PipeExpression['body']>(
 | 
			
		||||
              astClone,
 | 
			
		||||
              path.slice(0, -1)
 | 
			
		||||
            )
 | 
			
		||||
          ) {
 | 
			
		||||
            continue
 | 
			
		||||
            if (err(parent)) {
 | 
			
		||||
              return
 | 
			
		||||
            }
 | 
			
		||||
            const sketchToPreserve = sketchGroupFromKclValue(
 | 
			
		||||
              programMemory.get(sketchName),
 | 
			
		||||
              sketchName
 | 
			
		||||
            )
 | 
			
		||||
            if (err(sketchToPreserve)) return sketchToPreserve
 | 
			
		||||
            console.log('sketchName', sketchName)
 | 
			
		||||
            // Can't kick off multiple requests at once as getFaceDetails
 | 
			
		||||
            // is three engine calls in one and they conflict
 | 
			
		||||
            const faceDetails = await getFaceDetails(sketchToPreserve.on.id)
 | 
			
		||||
            if (
 | 
			
		||||
              !(
 | 
			
		||||
                faceDetails.origin &&
 | 
			
		||||
                faceDetails.x_axis &&
 | 
			
		||||
                faceDetails.y_axis &&
 | 
			
		||||
                faceDetails.z_axis
 | 
			
		||||
              )
 | 
			
		||||
            ) {
 | 
			
		||||
              return
 | 
			
		||||
            }
 | 
			
		||||
            const lastKey = Number(path.slice(-1)[0][0])
 | 
			
		||||
            modificationDetails.push({
 | 
			
		||||
              parent: parent.node,
 | 
			
		||||
              faceDetails,
 | 
			
		||||
              lastKey,
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
          parent[lastKey] = createCallExpressionStdLib('startSketchOn', [
 | 
			
		||||
            createObjectExpression({
 | 
			
		||||
              plane: createObjectExpression({
 | 
			
		||||
                origin: createObjectExpression({
 | 
			
		||||
                  x: roundLiteral(faceDetails.origin.x),
 | 
			
		||||
                  y: roundLiteral(faceDetails.origin.y),
 | 
			
		||||
                  z: roundLiteral(faceDetails.origin.z),
 | 
			
		||||
                }),
 | 
			
		||||
                x_axis: createObjectExpression({
 | 
			
		||||
                  x: roundLiteral(faceDetails.x_axis.x),
 | 
			
		||||
                  y: roundLiteral(faceDetails.x_axis.y),
 | 
			
		||||
                  z: roundLiteral(faceDetails.x_axis.z),
 | 
			
		||||
                }),
 | 
			
		||||
                y_axis: createObjectExpression({
 | 
			
		||||
                  x: roundLiteral(faceDetails.y_axis.x),
 | 
			
		||||
                  y: roundLiteral(faceDetails.y_axis.y),
 | 
			
		||||
                  z: roundLiteral(faceDetails.y_axis.z),
 | 
			
		||||
                }),
 | 
			
		||||
                z_axis: createObjectExpression({
 | 
			
		||||
                  x: roundLiteral(faceDetails.z_axis.x),
 | 
			
		||||
                  y: roundLiteral(faceDetails.z_axis.y),
 | 
			
		||||
                  z: roundLiteral(faceDetails.z_axis.z),
 | 
			
		||||
          for (const { parent, faceDetails, lastKey } of modificationDetails) {
 | 
			
		||||
            if (
 | 
			
		||||
              !(
 | 
			
		||||
                faceDetails.origin &&
 | 
			
		||||
                faceDetails.x_axis &&
 | 
			
		||||
                faceDetails.y_axis &&
 | 
			
		||||
                faceDetails.z_axis
 | 
			
		||||
              )
 | 
			
		||||
            ) {
 | 
			
		||||
              continue
 | 
			
		||||
            }
 | 
			
		||||
            parent[lastKey] = createCallExpressionStdLib('startSketchOn', [
 | 
			
		||||
              createObjectExpression({
 | 
			
		||||
                plane: createObjectExpression({
 | 
			
		||||
                  origin: createObjectExpression({
 | 
			
		||||
                    x: roundLiteral(faceDetails.origin.x),
 | 
			
		||||
                    y: roundLiteral(faceDetails.origin.y),
 | 
			
		||||
                    z: roundLiteral(faceDetails.origin.z),
 | 
			
		||||
                  }),
 | 
			
		||||
                  x_axis: createObjectExpression({
 | 
			
		||||
                    x: roundLiteral(faceDetails.x_axis.x),
 | 
			
		||||
                    y: roundLiteral(faceDetails.x_axis.y),
 | 
			
		||||
                    z: roundLiteral(faceDetails.x_axis.z),
 | 
			
		||||
                  }),
 | 
			
		||||
                  y_axis: createObjectExpression({
 | 
			
		||||
                    x: roundLiteral(faceDetails.y_axis.x),
 | 
			
		||||
                    y: roundLiteral(faceDetails.y_axis.y),
 | 
			
		||||
                    z: roundLiteral(faceDetails.y_axis.z),
 | 
			
		||||
                  }),
 | 
			
		||||
                  z_axis: createObjectExpression({
 | 
			
		||||
                    x: roundLiteral(faceDetails.z_axis.x),
 | 
			
		||||
                    y: roundLiteral(faceDetails.z_axis.y),
 | 
			
		||||
                    z: roundLiteral(faceDetails.z_axis.z),
 | 
			
		||||
                  }),
 | 
			
		||||
                }),
 | 
			
		||||
              }),
 | 
			
		||||
            }),
 | 
			
		||||
          ])
 | 
			
		||||
        }
 | 
			
		||||
        resolve(true)
 | 
			
		||||
            ])
 | 
			
		||||
          }
 | 
			
		||||
          resolve(true)
 | 
			
		||||
        })().catch(reportRejection)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    // await prom
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ beforeAll(async () => {
 | 
			
		||||
      setMediaStream: () => {},
 | 
			
		||||
      setIsStreamReady: () => {},
 | 
			
		||||
      modifyGrid: async () => {},
 | 
			
		||||
      callbackOnEngineLiteConnect: async () => {
 | 
			
		||||
      callbackOnEngineLiteConnect: () => {
 | 
			
		||||
        resolve(true)
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@ -56,6 +56,7 @@ export function applyFilletToSelection(
 | 
			
		||||
  const { modifiedAst, pathToFilletNode } = result
 | 
			
		||||
 | 
			
		||||
  // 3. update ast
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
  updateAstAndFocus(modifiedAst, pathToFilletNode)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -124,6 +124,7 @@ beforeAll(async () => {
 | 
			
		||||
      setMediaStream: () => {},
 | 
			
		||||
      setIsStreamReady: () => {},
 | 
			
		||||
      modifyGrid: async () => {},
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
      callbackOnEngineLiteConnect: async () => {
 | 
			
		||||
        const cacheEntries = Object.entries(codeToWriteCacheFor) as [
 | 
			
		||||
          CodeKey,
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ import toast from 'react-hot-toast'
 | 
			
		||||
import { SettingsViaQueryString } from 'lib/settings/settingsTypes'
 | 
			
		||||
import { EXECUTE_AST_INTERRUPT_ERROR_MESSAGE } from 'lib/constants'
 | 
			
		||||
import { KclManager } from 'lang/KclSingleton'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
// TODO(paultag): This ought to be tweakable.
 | 
			
		||||
const pingIntervalMs = 5_000
 | 
			
		||||
@ -388,11 +389,12 @@ class EngineConnection extends EventTarget {
 | 
			
		||||
        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().catch(reportRejection)
 | 
			
		||||
          break
 | 
			
		||||
      }
 | 
			
		||||
    }, pingIntervalMs)
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    this.connect()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1464,6 +1466,7 @@ export class EngineCommandManager extends EventTarget {
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
    this.onEngineConnectionOpened = async () => {
 | 
			
		||||
      // Set the stream background color
 | 
			
		||||
      // This takes RGBA values from 0-1
 | 
			
		||||
@ -1480,6 +1483,7 @@ export class EngineCommandManager extends EventTarget {
 | 
			
		||||
 | 
			
		||||
      // Sets the default line colors
 | 
			
		||||
      const opposingTheme = getOppositeTheme(this.settings.theme)
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      this.sendSceneCommand({
 | 
			
		||||
        cmd_id: uuidv4(),
 | 
			
		||||
        type: 'modeling_cmd_req',
 | 
			
		||||
@ -1490,6 +1494,7 @@ export class EngineCommandManager extends EventTarget {
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      // Set the edge lines visibility
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      this.sendSceneCommand({
 | 
			
		||||
        type: 'modeling_cmd_req',
 | 
			
		||||
        cmd_id: uuidv4(),
 | 
			
		||||
@ -1500,6 +1505,7 @@ export class EngineCommandManager extends EventTarget {
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      this._camControlsCameraChange()
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      this.sendSceneCommand({
 | 
			
		||||
        // CameraControls subscribes to default_camera_get_settings response events
 | 
			
		||||
        // firing this at connection ensure the camera's are synced initially
 | 
			
		||||
@ -1512,6 +1518,7 @@ 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
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      this.modifyGrid(!this.settings.showScaleGrid)?.then(async () => {
 | 
			
		||||
        await this.initPlanes()
 | 
			
		||||
        setIsStreamReady(true)
 | 
			
		||||
@ -1715,6 +1722,7 @@ export class EngineCommandManager extends EventTarget {
 | 
			
		||||
        this.onEngineConnectionNewTrack as EventListener
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      this.engineConnection?.connect()
 | 
			
		||||
    }
 | 
			
		||||
    this.engineConnection.addEventListener(
 | 
			
		||||
@ -2125,6 +2133,7 @@ export class EngineCommandManager extends EventTarget {
 | 
			
		||||
   * @param visible - whether to show or hide the scale grid
 | 
			
		||||
   */
 | 
			
		||||
  setScaleGridVisibility(visible: boolean) {
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    this.modifyGrid(!visible)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -360,6 +360,7 @@ export const executor = async (
 | 
			
		||||
): Promise<ProgramMemory> => {
 | 
			
		||||
  if (err(programMemory)) return Promise.reject(programMemory)
 | 
			
		||||
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
  engineCommandManager.startNewSession()
 | 
			
		||||
  const _programMemory = await _executor(
 | 
			
		||||
    node,
 | 
			
		||||
@ -569,6 +570,7 @@ export async function coreDump(
 | 
			
		||||
       a new GitHub issue for the user.
 | 
			
		||||
     */
 | 
			
		||||
    if (openGithubIssue && dump.github_issue_url) {
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      openWindow(dump.github_issue_url)
 | 
			
		||||
    } else {
 | 
			
		||||
      console.error(
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,7 @@
 | 
			
		||||
import { isDesktop } from './isDesktop'
 | 
			
		||||
import { components } from './machine-api'
 | 
			
		||||
import { reportRejection } from './trap'
 | 
			
		||||
import { toSync } from './utils'
 | 
			
		||||
 | 
			
		||||
export type MachinesListing = Array<
 | 
			
		||||
  components['schemas']['MachineInfoResponse']
 | 
			
		||||
@ -17,7 +19,7 @@ export class MachineManager {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.updateMachines()
 | 
			
		||||
    this.updateMachines().catch(reportRejection)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  start() {
 | 
			
		||||
@ -31,11 +33,14 @@ export class MachineManager {
 | 
			
		||||
    let timeoutId: ReturnType<typeof setTimeout> | undefined = undefined
 | 
			
		||||
    const timeoutLoop = () => {
 | 
			
		||||
      clearTimeout(timeoutId)
 | 
			
		||||
      timeoutId = setTimeout(async () => {
 | 
			
		||||
        await this.updateMachineApiIp()
 | 
			
		||||
        await this.updateMachines()
 | 
			
		||||
        timeoutLoop()
 | 
			
		||||
      }, 10000)
 | 
			
		||||
      timeoutId = setTimeout(
 | 
			
		||||
        toSync(async () => {
 | 
			
		||||
          await this.updateMachineApiIp()
 | 
			
		||||
          await this.updateMachines()
 | 
			
		||||
          timeoutLoop()
 | 
			
		||||
        }, reportRejection),
 | 
			
		||||
        10000
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
    timeoutLoop()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,15 @@
 | 
			
		||||
import { MouseEventHandler } from 'react'
 | 
			
		||||
import { isDesktop } from 'lib/isDesktop'
 | 
			
		||||
import { reportRejection } from './trap'
 | 
			
		||||
 | 
			
		||||
export const openExternalBrowserIfDesktop = (to?: string) =>
 | 
			
		||||
  function (e) {
 | 
			
		||||
    if (isDesktop()) {
 | 
			
		||||
      // Ignoring because currentTarget could be a few different things
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      window.electron.openExternal(to || e.currentTarget?.href)
 | 
			
		||||
      window.electron
 | 
			
		||||
        .openExternal(to || e.currentTarget?.href)
 | 
			
		||||
        .catch(reportRejection)
 | 
			
		||||
      e.preventDefault()
 | 
			
		||||
      e.stopPropagation()
 | 
			
		||||
      return false
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,8 @@ import { isDesktop } from 'lib/isDesktop'
 | 
			
		||||
import { useRef } from 'react'
 | 
			
		||||
import { CustomIcon } from 'components/CustomIcon'
 | 
			
		||||
import Tooltip from 'components/Tooltip'
 | 
			
		||||
import { toSync } from 'lib/utils'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A setting that can be set at the user or project level
 | 
			
		||||
@ -206,7 +208,7 @@ export function createSettings() {
 | 
			
		||||
                ref={inputRef}
 | 
			
		||||
              />
 | 
			
		||||
              <button
 | 
			
		||||
                onClick={async () => {
 | 
			
		||||
                onClick={toSync(async () => {
 | 
			
		||||
                  // In desktop end-to-end tests we can't control the file picker,
 | 
			
		||||
                  // so we seed the new directory value in the element's dataset
 | 
			
		||||
                  const inputRefVal = inputRef.current?.dataset.testValue
 | 
			
		||||
@ -225,7 +227,7 @@ export function createSettings() {
 | 
			
		||||
                    if (newPath.canceled) return
 | 
			
		||||
                    updateValue(newPath.filePaths[0])
 | 
			
		||||
                  }
 | 
			
		||||
                }}
 | 
			
		||||
                }, reportRejection)}
 | 
			
		||||
                className="p-0 m-0 border-none hover:bg-primary/10 focus:bg-primary/10 dark:hover:bg-primary/20 dark:focus::bg-primary/20"
 | 
			
		||||
                data-testid="project-directory-button"
 | 
			
		||||
              >
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,8 @@ import { EngineCommand } from 'lang/std/artifactGraph'
 | 
			
		||||
import { Models } from '@kittycad/lib'
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid'
 | 
			
		||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
 | 
			
		||||
import { err } from 'lib/trap'
 | 
			
		||||
import { err, reportRejection } from 'lib/trap'
 | 
			
		||||
import { toSync } from './utils'
 | 
			
		||||
 | 
			
		||||
type WebSocketResponse = Models['WebSocketResponse_type']
 | 
			
		||||
 | 
			
		||||
@ -85,6 +86,7 @@ export async function enginelessExecutor(
 | 
			
		||||
    setIsStreamReady: () => {},
 | 
			
		||||
    setMediaStream: () => {},
 | 
			
		||||
  }) as any as EngineCommandManager
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
  mockEngineCommandManager.startNewSession()
 | 
			
		||||
  const programMemory = await _executor(ast, pm, mockEngineCommandManager, true)
 | 
			
		||||
  await mockEngineCommandManager.waitForAllCommands()
 | 
			
		||||
@ -112,7 +114,8 @@ export async function executor(
 | 
			
		||||
  return new Promise((resolve) => {
 | 
			
		||||
    engineCommandManager.addEventListener(
 | 
			
		||||
      EngineCommandManagerEvents.SceneReady,
 | 
			
		||||
      async () => {
 | 
			
		||||
      toSync(async () => {
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
        engineCommandManager.startNewSession()
 | 
			
		||||
        const programMemory = await _executor(
 | 
			
		||||
          ast,
 | 
			
		||||
@ -121,8 +124,8 @@ export async function executor(
 | 
			
		||||
          false
 | 
			
		||||
        )
 | 
			
		||||
        await engineCommandManager.waitForAllCommands()
 | 
			
		||||
        Promise.resolve(programMemory)
 | 
			
		||||
      }
 | 
			
		||||
        resolve(programMemory)
 | 
			
		||||
      }, reportRejection)
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,8 @@ import { isDesktop } from 'lib/isDesktop'
 | 
			
		||||
import { Themes } from './theme'
 | 
			
		||||
import { commandBarMachine } from 'machines/commandBarMachine'
 | 
			
		||||
import { getNextFileName } from './desktopFS'
 | 
			
		||||
import { reportRejection } from './trap'
 | 
			
		||||
import { toSync } from './utils'
 | 
			
		||||
 | 
			
		||||
export async function submitTextToCadPrompt(
 | 
			
		||||
  prompt: string,
 | 
			
		||||
@ -128,37 +130,42 @@ export async function submitAndAwaitTextToKcl({
 | 
			
		||||
  // Check the status of the text-to-cad API job
 | 
			
		||||
  // until it is completed
 | 
			
		||||
  const textToCadComplete = new Promise<Models['TextToCad_type']>(
 | 
			
		||||
    async (resolve, reject) => {
 | 
			
		||||
      const value = await textToCadQueued
 | 
			
		||||
      if (value instanceof Error) {
 | 
			
		||||
        reject(value)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const MAX_CHECK_TIMEOUT = 3 * 60_000
 | 
			
		||||
      const CHECK_INTERVAL = 3000
 | 
			
		||||
 | 
			
		||||
      let timeElapsed = 0
 | 
			
		||||
      const interval = setInterval(async () => {
 | 
			
		||||
        timeElapsed += CHECK_INTERVAL
 | 
			
		||||
        if (timeElapsed >= MAX_CHECK_TIMEOUT) {
 | 
			
		||||
          clearInterval(interval)
 | 
			
		||||
          reject(new Error('Text-to-CAD API timed out'))
 | 
			
		||||
    (resolve, reject) => {
 | 
			
		||||
      ;(async () => {
 | 
			
		||||
        const value = await textToCadQueued
 | 
			
		||||
        if (value instanceof Error) {
 | 
			
		||||
          reject(value)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const check = await getTextToCadResult(value.id, token)
 | 
			
		||||
        if (check instanceof Error) {
 | 
			
		||||
          clearInterval(interval)
 | 
			
		||||
          reject(check)
 | 
			
		||||
        }
 | 
			
		||||
        const MAX_CHECK_TIMEOUT = 3 * 60_000
 | 
			
		||||
        const CHECK_INTERVAL = 3000
 | 
			
		||||
 | 
			
		||||
        if (check instanceof Error || check.status === 'failed') {
 | 
			
		||||
          clearInterval(interval)
 | 
			
		||||
          reject(check)
 | 
			
		||||
        } else if (check.status === 'completed') {
 | 
			
		||||
          clearInterval(interval)
 | 
			
		||||
          resolve(check)
 | 
			
		||||
        }
 | 
			
		||||
      }, CHECK_INTERVAL)
 | 
			
		||||
        let timeElapsed = 0
 | 
			
		||||
        const interval = setInterval(
 | 
			
		||||
          toSync(async () => {
 | 
			
		||||
            timeElapsed += CHECK_INTERVAL
 | 
			
		||||
            if (timeElapsed >= MAX_CHECK_TIMEOUT) {
 | 
			
		||||
              clearInterval(interval)
 | 
			
		||||
              reject(new Error('Text-to-CAD API timed out'))
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const check = await getTextToCadResult(value.id, token)
 | 
			
		||||
            if (check instanceof Error) {
 | 
			
		||||
              clearInterval(interval)
 | 
			
		||||
              reject(check)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (check instanceof Error || check.status === 'failed') {
 | 
			
		||||
              clearInterval(interval)
 | 
			
		||||
              reject(check)
 | 
			
		||||
            } else if (check.status === 'completed') {
 | 
			
		||||
              clearInterval(interval)
 | 
			
		||||
              resolve(check)
 | 
			
		||||
            }
 | 
			
		||||
          }, reportRejection),
 | 
			
		||||
          CHECK_INTERVAL
 | 
			
		||||
        )
 | 
			
		||||
      })().catch(reportRejection)
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,13 +2,22 @@ import toast from 'react-hot-toast'
 | 
			
		||||
 | 
			
		||||
type ExcludeErr<T> = Exclude<T, Error>
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This is intentionally *not* exported due to misuse.  We'd like to add a lint.
 | 
			
		||||
 */
 | 
			
		||||
function isErr<T>(value: ExcludeErr<T> | Error): value is Error {
 | 
			
		||||
  return value instanceof Error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Used to bubble errors up
 | 
			
		||||
export function err<T>(value: ExcludeErr<T> | Error): value is Error {
 | 
			
		||||
  if (!(value instanceof Error)) {
 | 
			
		||||
  if (!isErr(value)) {
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO: Remove this once we have a lint to prevent misuse of this function.
 | 
			
		||||
  console.error(value)
 | 
			
		||||
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -21,7 +30,7 @@ export function cleanErrs<T>(
 | 
			
		||||
  const argsWOutErr: Array<ExcludeErr<T>> = []
 | 
			
		||||
  const argsWErr: Array<Error> = []
 | 
			
		||||
  for (const v of value) {
 | 
			
		||||
    if (err(v)) {
 | 
			
		||||
    if (isErr(v)) {
 | 
			
		||||
      argsWErr.push(v)
 | 
			
		||||
    } else {
 | 
			
		||||
      argsWOutErr.push(v)
 | 
			
		||||
@ -30,9 +39,28 @@ export function cleanErrs<T>(
 | 
			
		||||
  return [argsWOutErr.length !== value.length, argsWOutErr, argsWErr]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function report(
 | 
			
		||||
  message: string,
 | 
			
		||||
  { showToast }: { showToast: boolean } = { showToast: false }
 | 
			
		||||
) {
 | 
			
		||||
  console.error(message)
 | 
			
		||||
  if (showToast) {
 | 
			
		||||
    toast.error(message, { id: 'error' })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *  Used to report errors to user at a certain point in execution
 | 
			
		||||
 *  @returns boolean
 | 
			
		||||
 * Report a promise rejection.  The type of reason is `any` so that it matches
 | 
			
		||||
 * Promise.prototype.catch.
 | 
			
		||||
 */
 | 
			
		||||
export function reportRejection(reason: any) {
 | 
			
		||||
  report((reason ?? 'Unknown promise rejection').toString())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Report an error to the user.  Trapping is the opposite of propagating an
 | 
			
		||||
 * error.  We should propagate errors in low-level functions and trap at the top
 | 
			
		||||
 * level.
 | 
			
		||||
 */
 | 
			
		||||
export function trap<T>(
 | 
			
		||||
  value: ExcludeErr<T> | Error,
 | 
			
		||||
@ -41,7 +69,7 @@ export function trap<T>(
 | 
			
		||||
    suppress?: boolean
 | 
			
		||||
  }
 | 
			
		||||
): value is Error {
 | 
			
		||||
  if (!err(value)) {
 | 
			
		||||
  if (!isErr(value)) {
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -98,3 +98,18 @@ export function isEnumMember<T extends Record<string, unknown>>(
 | 
			
		||||
export type DeepPartial<T> = {
 | 
			
		||||
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Replace a function's return type with another type.
 | 
			
		||||
 */
 | 
			
		||||
export type WithReturnType<F extends (...args: any[]) => any, NewReturn> = (
 | 
			
		||||
  ...args: Parameters<F>
 | 
			
		||||
) => NewReturn
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Assert that a function type is async, preserving its parameter types.
 | 
			
		||||
 */
 | 
			
		||||
export type AsyncFn<F extends (...args: any[]) => any> = WithReturnType<
 | 
			
		||||
  F,
 | 
			
		||||
  Promise<unknown>
 | 
			
		||||
>
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import { SourceRange } from '../lang/wasm'
 | 
			
		||||
import { v4 } from 'uuid'
 | 
			
		||||
import { isDesktop } from './isDesktop'
 | 
			
		||||
import { AnyMachineSnapshot } from 'xstate'
 | 
			
		||||
import { AsyncFn } from './types'
 | 
			
		||||
 | 
			
		||||
export const uuidv4 = v4
 | 
			
		||||
 | 
			
		||||
@ -106,6 +107,28 @@ export function deferExecution<T>(func: (args: T) => any, wait: number) {
 | 
			
		||||
  return deferred
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Wrap an async function so that it can be called in a sync context, catching
 | 
			
		||||
 * rejections.
 | 
			
		||||
 *
 | 
			
		||||
 * It's common to want to run an async function in a sync context, like an event
 | 
			
		||||
 * handler or callback.  But we want to catch errors.
 | 
			
		||||
 *
 | 
			
		||||
 * Note: The returned function doesn't block.  This isn't magic.
 | 
			
		||||
 *
 | 
			
		||||
 * @param onReject This callback type is from Promise.prototype.catch.
 | 
			
		||||
 */
 | 
			
		||||
export function toSync<F extends AsyncFn<F>>(
 | 
			
		||||
  fn: F,
 | 
			
		||||
  onReject: (
 | 
			
		||||
    reason: any
 | 
			
		||||
  ) => void | PromiseLike<void | null | undefined> | null | undefined
 | 
			
		||||
): (...args: Parameters<F>) => void {
 | 
			
		||||
  return (...args: Parameters<F>) => {
 | 
			
		||||
    fn(...args).catch(onReject)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getNormalisedCoordinates({
 | 
			
		||||
  clientX,
 | 
			
		||||
  clientY,
 | 
			
		||||
 | 
			
		||||
@ -116,6 +116,7 @@ export const authMachine = setup({
 | 
			
		||||
        'Log out': {
 | 
			
		||||
          target: 'loggedOut',
 | 
			
		||||
          actions: () => {
 | 
			
		||||
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
            if (isDesktop()) writeTokenFile('')
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
@ -219,6 +220,7 @@ async function getAndSyncStoredToken(input: {
 | 
			
		||||
  if (token) {
 | 
			
		||||
    // has just logged in, update storage
 | 
			
		||||
    localStorage.setItem(TOKEN_PERSIST_KEY, token)
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    isDesktop() && writeTokenFile(token)
 | 
			
		||||
    return token
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -50,7 +50,7 @@ import {
 | 
			
		||||
  applyConstraintAxisAlign,
 | 
			
		||||
} from 'components/Toolbar/SetAbsDistance'
 | 
			
		||||
import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig'
 | 
			
		||||
import { err, trap } from 'lib/trap'
 | 
			
		||||
import { err, reportRejection, trap } from 'lib/trap'
 | 
			
		||||
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
 | 
			
		||||
import { uuidv4 } from 'lib/utils'
 | 
			
		||||
import { Coords2d } from 'lang/std/sketch'
 | 
			
		||||
@ -492,13 +492,15 @@ export const modelingMachine = setup({
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ),
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
			
		||||
    'hide default planes': () => kclManager.hidePlanes(),
 | 
			
		||||
    'reset sketch metadata': assign({
 | 
			
		||||
      sketchDetails: null,
 | 
			
		||||
      sketchEnginePathId: '',
 | 
			
		||||
      sketchPlaneId: '',
 | 
			
		||||
    }),
 | 
			
		||||
    'reset camera position': () =>
 | 
			
		||||
    'reset camera position': () => {
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      engineCommandManager.sendSceneCommand({
 | 
			
		||||
        type: 'modeling_cmd_req',
 | 
			
		||||
        cmd_id: uuidv4(),
 | 
			
		||||
@ -508,7 +510,8 @@ export const modelingMachine = setup({
 | 
			
		||||
          vantage: { x: 0, y: -1250, z: 580 },
 | 
			
		||||
          up: { x: 0, y: 0, z: 1 },
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    'set new sketch metadata': assign(({ event }) => {
 | 
			
		||||
      if (
 | 
			
		||||
        event.type !== 'xstate.done.actor.animate-to-sketch' &&
 | 
			
		||||
@ -519,77 +522,85 @@ export const modelingMachine = setup({
 | 
			
		||||
        sketchDetails: event.output,
 | 
			
		||||
      }
 | 
			
		||||
    }),
 | 
			
		||||
    'AST extrude': async ({ context: { store }, event }) => {
 | 
			
		||||
    'AST extrude': ({ context: { store }, event }) => {
 | 
			
		||||
      if (event.type !== 'Extrude') return
 | 
			
		||||
      if (!event.data) return
 | 
			
		||||
      const { selection, distance } = event.data
 | 
			
		||||
      let ast = kclManager.ast
 | 
			
		||||
      if (
 | 
			
		||||
        'variableName' in distance &&
 | 
			
		||||
        distance.variableName &&
 | 
			
		||||
        distance.insertIndex !== undefined
 | 
			
		||||
      ) {
 | 
			
		||||
        const newBody = [...ast.body]
 | 
			
		||||
        newBody.splice(distance.insertIndex, 0, distance.variableDeclarationAst)
 | 
			
		||||
        ast.body = newBody
 | 
			
		||||
      }
 | 
			
		||||
      const pathToNode = getNodePathFromSourceRange(
 | 
			
		||||
        ast,
 | 
			
		||||
        selection.codeBasedSelections[0].range
 | 
			
		||||
      )
 | 
			
		||||
      const extrudeSketchRes = extrudeSketch(
 | 
			
		||||
        ast,
 | 
			
		||||
        pathToNode,
 | 
			
		||||
        false,
 | 
			
		||||
        'variableName' in distance
 | 
			
		||||
          ? distance.variableIdentifierAst
 | 
			
		||||
          : distance.valueAst
 | 
			
		||||
      )
 | 
			
		||||
      if (trap(extrudeSketchRes)) return
 | 
			
		||||
      const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
 | 
			
		||||
      ;(async () => {
 | 
			
		||||
        if (!event.data) return
 | 
			
		||||
        const { selection, distance } = event.data
 | 
			
		||||
        let ast = kclManager.ast
 | 
			
		||||
        if (
 | 
			
		||||
          'variableName' in distance &&
 | 
			
		||||
          distance.variableName &&
 | 
			
		||||
          distance.insertIndex !== undefined
 | 
			
		||||
        ) {
 | 
			
		||||
          const newBody = [...ast.body]
 | 
			
		||||
          newBody.splice(
 | 
			
		||||
            distance.insertIndex,
 | 
			
		||||
            0,
 | 
			
		||||
            distance.variableDeclarationAst
 | 
			
		||||
          )
 | 
			
		||||
          ast.body = newBody
 | 
			
		||||
        }
 | 
			
		||||
        const pathToNode = getNodePathFromSourceRange(
 | 
			
		||||
          ast,
 | 
			
		||||
          selection.codeBasedSelections[0].range
 | 
			
		||||
        )
 | 
			
		||||
        const extrudeSketchRes = extrudeSketch(
 | 
			
		||||
          ast,
 | 
			
		||||
          pathToNode,
 | 
			
		||||
          false,
 | 
			
		||||
          'variableName' in distance
 | 
			
		||||
            ? distance.variableIdentifierAst
 | 
			
		||||
            : distance.valueAst
 | 
			
		||||
        )
 | 
			
		||||
        if (trap(extrudeSketchRes)) return
 | 
			
		||||
        const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
 | 
			
		||||
 | 
			
		||||
      store.videoElement?.pause()
 | 
			
		||||
      const updatedAst = await kclManager.updateAst(modifiedAst, true, {
 | 
			
		||||
        focusPath: pathToExtrudeArg,
 | 
			
		||||
        zoomToFit: true,
 | 
			
		||||
        zoomOnRangeAndType: {
 | 
			
		||||
          range: selection.codeBasedSelections[0].range,
 | 
			
		||||
          type: 'path',
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
      if (!engineCommandManager.engineConnection?.idleMode) {
 | 
			
		||||
        store.videoElement?.play().catch((e) => {
 | 
			
		||||
          console.warn('Video playing was prevented', e)
 | 
			
		||||
        store.videoElement?.pause()
 | 
			
		||||
        const updatedAst = await kclManager.updateAst(modifiedAst, true, {
 | 
			
		||||
          focusPath: pathToExtrudeArg,
 | 
			
		||||
          zoomToFit: true,
 | 
			
		||||
          zoomOnRangeAndType: {
 | 
			
		||||
            range: selection.codeBasedSelections[0].range,
 | 
			
		||||
            type: 'path',
 | 
			
		||||
          },
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      if (updatedAst?.selections) {
 | 
			
		||||
        editorManager.selectRange(updatedAst?.selections)
 | 
			
		||||
      }
 | 
			
		||||
        if (!engineCommandManager.engineConnection?.idleMode) {
 | 
			
		||||
          store.videoElement?.play().catch((e) => {
 | 
			
		||||
            console.warn('Video playing was prevented', e)
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
        if (updatedAst?.selections) {
 | 
			
		||||
          editorManager.selectRange(updatedAst?.selections)
 | 
			
		||||
        }
 | 
			
		||||
      })().catch(reportRejection)
 | 
			
		||||
    },
 | 
			
		||||
    'AST delete selection': async ({ context: { selectionRanges } }) => {
 | 
			
		||||
      let ast = kclManager.ast
 | 
			
		||||
    'AST delete selection': ({ context: { selectionRanges } }) => {
 | 
			
		||||
      ;(async () => {
 | 
			
		||||
        let ast = kclManager.ast
 | 
			
		||||
 | 
			
		||||
      const modifiedAst = await deleteFromSelection(
 | 
			
		||||
        ast,
 | 
			
		||||
        selectionRanges.codeBasedSelections[0],
 | 
			
		||||
        kclManager.programMemory,
 | 
			
		||||
        getFaceDetails
 | 
			
		||||
      )
 | 
			
		||||
      if (err(modifiedAst)) return
 | 
			
		||||
        const modifiedAst = await deleteFromSelection(
 | 
			
		||||
          ast,
 | 
			
		||||
          selectionRanges.codeBasedSelections[0],
 | 
			
		||||
          kclManager.programMemory,
 | 
			
		||||
          getFaceDetails
 | 
			
		||||
        )
 | 
			
		||||
        if (err(modifiedAst)) return
 | 
			
		||||
 | 
			
		||||
      const testExecute = await executeAst({
 | 
			
		||||
        ast: modifiedAst,
 | 
			
		||||
        useFakeExecutor: true,
 | 
			
		||||
        engineCommandManager,
 | 
			
		||||
      })
 | 
			
		||||
      if (testExecute.errors.length) {
 | 
			
		||||
        toast.error('Unable to delete part')
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
        const testExecute = await executeAst({
 | 
			
		||||
          ast: modifiedAst,
 | 
			
		||||
          useFakeExecutor: true,
 | 
			
		||||
          engineCommandManager,
 | 
			
		||||
        })
 | 
			
		||||
        if (testExecute.errors.length) {
 | 
			
		||||
          toast.error('Unable to delete part')
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
        await kclManager.updateAst(modifiedAst, true)
 | 
			
		||||
      })().catch(reportRejection)
 | 
			
		||||
    },
 | 
			
		||||
    'AST fillet': async ({ event }) => {
 | 
			
		||||
    'AST fillet': ({ event }) => {
 | 
			
		||||
      if (event.type !== 'Fillet') return
 | 
			
		||||
      if (!event.data) return
 | 
			
		||||
 | 
			
		||||
@ -635,16 +646,18 @@ export const modelingMachine = setup({
 | 
			
		||||
          up: sketchDetails.yAxis,
 | 
			
		||||
          position: sketchDetails.origin,
 | 
			
		||||
        })
 | 
			
		||||
      })()
 | 
			
		||||
      })().catch(reportRejection)
 | 
			
		||||
    },
 | 
			
		||||
    'tear down client sketch': () => {
 | 
			
		||||
      if (sceneEntitiesManager.activeSegments) {
 | 
			
		||||
        // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
        sceneEntitiesManager.tearDownSketch({ removeAxis: false })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    'remove sketch grid': () => sceneEntitiesManager.removeSketchGrid(),
 | 
			
		||||
    'set up draft line': ({ context: { sketchDetails } }) => {
 | 
			
		||||
      if (!sketchDetails) return
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      sceneEntitiesManager.setUpDraftSegment(
 | 
			
		||||
        sketchDetails.sketchPathToNode,
 | 
			
		||||
        sketchDetails.zAxis,
 | 
			
		||||
@ -655,6 +668,7 @@ export const modelingMachine = setup({
 | 
			
		||||
    },
 | 
			
		||||
    'set up draft arc': ({ context: { sketchDetails } }) => {
 | 
			
		||||
      if (!sketchDetails) return
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      sceneEntitiesManager.setUpDraftSegment(
 | 
			
		||||
        sketchDetails.sketchPathToNode,
 | 
			
		||||
        sketchDetails.zAxis,
 | 
			
		||||
@ -683,6 +697,7 @@ export const modelingMachine = setup({
 | 
			
		||||
    'set up draft rectangle': ({ context: { sketchDetails }, event }) => {
 | 
			
		||||
      if (event.type !== 'Add rectangle origin') return
 | 
			
		||||
      if (!sketchDetails || !event.data) return
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      sceneEntitiesManager.setupDraftRectangle(
 | 
			
		||||
        sketchDetails.sketchPathToNode,
 | 
			
		||||
        sketchDetails.zAxis,
 | 
			
		||||
@ -693,6 +708,7 @@ export const modelingMachine = setup({
 | 
			
		||||
    },
 | 
			
		||||
    'set up draft line without teardown': ({ context: { sketchDetails } }) => {
 | 
			
		||||
      if (!sketchDetails) return
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      sceneEntitiesManager.setUpDraftSegment(
 | 
			
		||||
        sketchDetails.sketchPathToNode,
 | 
			
		||||
        sketchDetails.zAxis,
 | 
			
		||||
@ -702,7 +718,10 @@ export const modelingMachine = setup({
 | 
			
		||||
        false
 | 
			
		||||
      )
 | 
			
		||||
    },
 | 
			
		||||
    'show default planes': () => kclManager.showPlanes(),
 | 
			
		||||
    'show default planes': () => {
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      kclManager.showPlanes()
 | 
			
		||||
    },
 | 
			
		||||
    'setup noPoints onClick listener': ({ context: { sketchDetails } }) => {
 | 
			
		||||
      if (!sketchDetails) return
 | 
			
		||||
 | 
			
		||||
@ -732,7 +751,8 @@ export const modelingMachine = setup({
 | 
			
		||||
    'engineToClient cam sync direction': () => {
 | 
			
		||||
      sceneInfra.camControls.syncDirection = 'engineToClient'
 | 
			
		||||
    },
 | 
			
		||||
    'set selection filter to faces only': () =>
 | 
			
		||||
    'set selection filter to faces only': () => {
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      engineCommandManager.sendSceneCommand({
 | 
			
		||||
        type: 'modeling_cmd_req',
 | 
			
		||||
        cmd_id: uuidv4(),
 | 
			
		||||
@ -740,13 +760,15 @@ export const modelingMachine = setup({
 | 
			
		||||
          type: 'set_selection_filter',
 | 
			
		||||
          filter: ['face', 'object'],
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    'set selection filter to defaults': () =>
 | 
			
		||||
      kclManager.defaultSelectionFilter(),
 | 
			
		||||
    'Delete segment': ({ context: { sketchDetails }, event }) => {
 | 
			
		||||
      if (event.type !== 'Delete segment') return
 | 
			
		||||
      if (!sketchDetails || !event.data) return
 | 
			
		||||
      return deleteSegment({
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
      deleteSegment({
 | 
			
		||||
        pathToNode: event.data,
 | 
			
		||||
        sketchDetails,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										42
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								src/main.ts
									
									
									
									
									
								
							@ -19,6 +19,7 @@ import electronUpdater, { type AppUpdater } from 'electron-updater'
 | 
			
		||||
import minimist from 'minimist'
 | 
			
		||||
import getCurrentProjectFile from 'lib/getCurrentProjectFile'
 | 
			
		||||
import os from 'node:os'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
let mainWindow: BrowserWindow | null = null
 | 
			
		||||
 | 
			
		||||
@ -87,28 +88,30 @@ const createWindow = (filePath?: string): BrowserWindow => {
 | 
			
		||||
 | 
			
		||||
  // and load the index.html of the app.
 | 
			
		||||
  if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
 | 
			
		||||
    newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
 | 
			
		||||
    newWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL).catch(reportRejection)
 | 
			
		||||
  } else {
 | 
			
		||||
    getProjectPathAtStartup(filePath).then((projectPath) => {
 | 
			
		||||
      const startIndex = path.join(
 | 
			
		||||
        __dirname,
 | 
			
		||||
        `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
 | 
			
		||||
      )
 | 
			
		||||
    getProjectPathAtStartup(filePath)
 | 
			
		||||
      .then(async (projectPath) => {
 | 
			
		||||
        const startIndex = path.join(
 | 
			
		||||
          __dirname,
 | 
			
		||||
          `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      if (projectPath === null) {
 | 
			
		||||
        newWindow.loadFile(startIndex)
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
        if (projectPath === null) {
 | 
			
		||||
          await newWindow.loadFile(startIndex)
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
      console.log('Loading file', projectPath)
 | 
			
		||||
        console.log('Loading file', projectPath)
 | 
			
		||||
 | 
			
		||||
      const fullUrl = `/file/${encodeURIComponent(projectPath)}`
 | 
			
		||||
      console.log('Full URL', fullUrl)
 | 
			
		||||
        const fullUrl = `/file/${encodeURIComponent(projectPath)}`
 | 
			
		||||
        console.log('Full URL', fullUrl)
 | 
			
		||||
 | 
			
		||||
      newWindow.loadFile(startIndex, {
 | 
			
		||||
        hash: fullUrl,
 | 
			
		||||
        await newWindow.loadFile(startIndex, {
 | 
			
		||||
          hash: fullUrl,
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
      .catch(reportRejection)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Open the DevTools.
 | 
			
		||||
@ -175,6 +178,7 @@ ipcMain.handle('login', async (event, host) => {
 | 
			
		||||
 | 
			
		||||
  const handle = await client.deviceAuthorization()
 | 
			
		||||
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
  shell.openExternal(handle.verification_uri_complete)
 | 
			
		||||
 | 
			
		||||
  // Wait for the user to login.
 | 
			
		||||
@ -241,12 +245,12 @@ export async function checkForUpdates(autoUpdater: AppUpdater) {
 | 
			
		||||
  console.log(result)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
app.on('ready', async () => {
 | 
			
		||||
app.on('ready', () => {
 | 
			
		||||
  const autoUpdater = getAutoUpdater()
 | 
			
		||||
  checkForUpdates(autoUpdater)
 | 
			
		||||
  checkForUpdates(autoUpdater).catch(reportRejection)
 | 
			
		||||
  const fifteenMinutes = 15 * 60 * 1000
 | 
			
		||||
  setInterval(() => {
 | 
			
		||||
    checkForUpdates(autoUpdater)
 | 
			
		||||
    checkForUpdates(autoUpdater).catch(reportRejection)
 | 
			
		||||
  }, fifteenMinutes)
 | 
			
		||||
 | 
			
		||||
  autoUpdater.on('update-available', (info) => {
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,17 @@
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
import { ReportHandler } from 'web-vitals'
 | 
			
		||||
 | 
			
		||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
 | 
			
		||||
  if (onPerfEntry && onPerfEntry instanceof Function) {
 | 
			
		||||
    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
 | 
			
		||||
      getCLS(onPerfEntry)
 | 
			
		||||
      getFID(onPerfEntry)
 | 
			
		||||
      getFCP(onPerfEntry)
 | 
			
		||||
      getLCP(onPerfEntry)
 | 
			
		||||
      getTTFB(onPerfEntry)
 | 
			
		||||
    })
 | 
			
		||||
    import('web-vitals')
 | 
			
		||||
      .then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
 | 
			
		||||
        getCLS(onPerfEntry)
 | 
			
		||||
        getFID(onPerfEntry)
 | 
			
		||||
        getFCP(onPerfEntry)
 | 
			
		||||
        getLCP(onPerfEntry)
 | 
			
		||||
        getTTFB(onPerfEntry)
 | 
			
		||||
      })
 | 
			
		||||
      .catch(reportRejection)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ export default function FutureWork() {
 | 
			
		||||
  useDemoCode()
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    send({ type: 'Cancel' }) // in case the user hit 'Next' while still in sketch mode
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    sceneInfra.camControls.resetCameraPosition()
 | 
			
		||||
  }, [send])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,8 @@ import { IndexLoaderData } from 'lib/types'
 | 
			
		||||
import { PATHS } from 'lib/paths'
 | 
			
		||||
import { useFileContext } from 'hooks/useFileContext'
 | 
			
		||||
import { useLspContext } from 'components/LspProvider'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
import { toSync } from 'lib/utils'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Show either a welcome screen or a warning screen
 | 
			
		||||
@ -80,7 +82,7 @@ function OnboardingWarningDesktop(props: OnboardingResetWarningProps) {
 | 
			
		||||
      <OnboardingButtons
 | 
			
		||||
        className="mt-6"
 | 
			
		||||
        dismiss={dismiss}
 | 
			
		||||
        next={onAccept}
 | 
			
		||||
        next={toSync(onAccept, reportRejection)}
 | 
			
		||||
        nextText="Make a new project"
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
@ -102,14 +104,14 @@ function OnboardingWarningWeb(props: OnboardingResetWarningProps) {
 | 
			
		||||
      <OnboardingButtons
 | 
			
		||||
        className="mt-6"
 | 
			
		||||
        dismiss={dismiss}
 | 
			
		||||
        next={async () => {
 | 
			
		||||
        next={toSync(async () => {
 | 
			
		||||
          // We do want to update both the state and editor here.
 | 
			
		||||
          codeManager.updateCodeStateEditor(bracket)
 | 
			
		||||
          await codeManager.writeToFile()
 | 
			
		||||
 | 
			
		||||
          await kclManager.executeCode(true)
 | 
			
		||||
          props.setShouldShowWarning(false)
 | 
			
		||||
        }}
 | 
			
		||||
        }, reportRejection)}
 | 
			
		||||
        nextText="Overwrite code and continue"
 | 
			
		||||
      />
 | 
			
		||||
    </>
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ export default function Sketching() {
 | 
			
		||||
      await kclManager.executeCode(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-floating-promises
 | 
			
		||||
    clearEditor()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,8 @@ import { ActionButton } from 'components/ActionButton'
 | 
			
		||||
import { onboardingPaths } from 'routes/Onboarding/paths'
 | 
			
		||||
import { codeManager, editorManager, kclManager } from 'lib/singletons'
 | 
			
		||||
import { bracket } from 'lib/exampleKcl'
 | 
			
		||||
import { toSync } from 'lib/utils'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
export const kbdClasses =
 | 
			
		||||
  'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
 | 
			
		||||
@ -80,11 +82,13 @@ export const onboardingRoutes = [
 | 
			
		||||
export function useDemoCode() {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!editorManager.editorView || codeManager.code === bracket) return
 | 
			
		||||
    setTimeout(async () => {
 | 
			
		||||
      codeManager.updateCodeStateEditor(bracket)
 | 
			
		||||
      await kclManager.executeCode(true)
 | 
			
		||||
      await codeManager.writeToFile()
 | 
			
		||||
    })
 | 
			
		||||
    setTimeout(
 | 
			
		||||
      toSync(async () => {
 | 
			
		||||
        codeManager.updateCodeStateEditor(bracket)
 | 
			
		||||
        await kclManager.executeCode(true)
 | 
			
		||||
        await codeManager.writeToFile()
 | 
			
		||||
      }, reportRejection)
 | 
			
		||||
    )
 | 
			
		||||
  }, [editorManager.editorView])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,8 @@ import { CustomIcon } from 'components/CustomIcon'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
import { APP_VERSION } from './Settings'
 | 
			
		||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
 | 
			
		||||
import { toSync } from 'lib/utils'
 | 
			
		||||
import { reportRejection } from 'lib/trap'
 | 
			
		||||
 | 
			
		||||
const subtleBorder =
 | 
			
		||||
  'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
 | 
			
		||||
@ -104,7 +106,7 @@ const SignIn = () => {
 | 
			
		||||
            </p>
 | 
			
		||||
            {isDesktop() ? (
 | 
			
		||||
              <button
 | 
			
		||||
                onClick={signInDesktop}
 | 
			
		||||
                onClick={toSync(signInDesktop, reportRejection)}
 | 
			
		||||
                className={
 | 
			
		||||
                  'm-0 mt-8 flex gap-4 items-center px-3 py-1 ' +
 | 
			
		||||
                  '!border-transparent !text-lg !text-chalkboard-10 !bg-primary hover:hue-rotate-15'
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user