Show when user can't connect because of a bad token (#2105)
* Reapply "Add ping pong health, remove a timeout interval, fix up netwo… (#1771)
This reverts commit 1913519f68.
* Fix build errors
* Add new error states to network status notification
* Remove unused variable
* Refactor to use Context API for network status
* Don't do any stream events if network is not ok
* Catch LSP errors on bad auth
* Show when authentication is bad (cookie header only)
* Fix formatting
* Fix up types
* Revert awaiting on lsp failure
* Fix tsc
* wip
* wip
* fmt
* Fix typing
* Incorporate ping health; yarn make:dev; faster video stream loss notice
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* run ci pls
* run ci pls
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* run ci pls again
* Remove unused variables
* Add new instructions on running Playwright anywhere
* Please the Playwright. Praise the Playwright.
* Correct a vitest
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* ci again
* Fix tests unrelated to this PR
* Fix flakiness in for segments tests
* Bump to 2 workers
* fmt
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* fmt
* fmt
* Fixups
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* ci
* Set workers to 1
* Wait for network status listeners before connecting
* Fix initial connection requirements and trying 2 workers again
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
			
			
							
								
								
									
										14
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,14 @@
 | 
			
		||||
.PHONY: dev
 | 
			
		||||
 | 
			
		||||
WASM_LIB_FILES := $(wildcard src/wasm-lib/**/*.rs)
 | 
			
		||||
 | 
			
		||||
dev: node_modules public/wasm_lib_bg.wasm
 | 
			
		||||
	yarn start
 | 
			
		||||
 | 
			
		||||
public/wasm_lib_bg.wasm: $(WASM_LIB_FILES)
 | 
			
		||||
	yarn build:wasm-dev
 | 
			
		||||
 | 
			
		||||
node_modules: package.json
 | 
			
		||||
 | 
			
		||||
package.json:
 | 
			
		||||
	yarn install
 | 
			
		||||
							
								
								
									
										36
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@ -197,28 +197,32 @@ For more information on fuzzing you can check out
 | 
			
		||||
 | 
			
		||||
### Playwright
 | 
			
		||||
 | 
			
		||||
First time running plawright locally, you'll need to add the secrets file
 | 
			
		||||
For a portable way to run Playwright you'll need Docker.
 | 
			
		||||
 | 
			
		||||
After that, open a terminal and run:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
touch ./e2e/playwright/playwright-secrets.env
 | 
			
		||||
printf 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets.env
 | 
			
		||||
docker run --network host  --rm --init -it playwright/chrome:playwright-1.43.1
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
and in another terminal, run:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ yarn playwright test --project="Google Chrome" <test suite>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
An example of a `<test suite>` is: `e2e/playwright/flow-tests.spec.ts`
 | 
			
		||||
 | 
			
		||||
YOU WILL NEED A PLAYWRIGHT-SECRETS.ENV FILE:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# ./e2e/playwright/playwright-secrets.env
 | 
			
		||||
token=<your-token>
 | 
			
		||||
snapshottoken=<your-snapshot-token>
 | 
			
		||||
```
 | 
			
		||||
then replace "your-token" with a dev token from dev.zoo.dev/account/api-tokens
 | 
			
		||||
 | 
			
		||||
then:
 | 
			
		||||
run playwright
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
yarn playwright test
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
run a specific test suite
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
yarn playwright test src/e2e-tests/example.spec.ts
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
run a specific test change the test from `test('...` to `test.only('...`
 | 
			
		||||
(note if you commit this, the tests will instantly fail without running any of the tests)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,11 @@
 | 
			
		||||
import { test, expect, Page } from '@playwright/test'
 | 
			
		||||
import { makeTemplate, getUtils, doExport } from './test-utils'
 | 
			
		||||
import {
 | 
			
		||||
  makeTemplate,
 | 
			
		||||
  getUtils,
 | 
			
		||||
  getMovementUtils,
 | 
			
		||||
  wiggleMove,
 | 
			
		||||
  doExport,
 | 
			
		||||
} from './test-utils'
 | 
			
		||||
import waitOn from 'wait-on'
 | 
			
		||||
import { XOR, roundOff, uuidv4 } from 'lib/utils'
 | 
			
		||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
 | 
			
		||||
@ -27,6 +33,8 @@ document.addEventListener('mousemove', (e) =>
 | 
			
		||||
)
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const deg = (Math.PI * 2) / 360
 | 
			
		||||
 | 
			
		||||
const commonPoints = {
 | 
			
		||||
  startAt: '[9.06, -12.22]',
 | 
			
		||||
  num1: 9.14,
 | 
			
		||||
@ -35,6 +43,8 @@ const commonPoints = {
 | 
			
		||||
  // num2: 19.19,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Utilities for writing tests that depend on test values
 | 
			
		||||
 | 
			
		||||
test.beforeEach(async ({ context, page }) => {
 | 
			
		||||
  // wait for Vite preview server to be up
 | 
			
		||||
  await waitOn({
 | 
			
		||||
@ -1484,12 +1494,15 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
test('Can add multiple sketches', async ({ page }) => {
 | 
			
		||||
  test.skip(process.platform === 'darwin', 'Can add multiple sketches')
 | 
			
		||||
  const u = await getUtils(page)
 | 
			
		||||
  await page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
  const PUR = 400 / 37.5 //pixeltoUnitRatio
 | 
			
		||||
  const viewportSize = { width: 1200, height: 500 }
 | 
			
		||||
  await page.setViewportSize(viewportSize)
 | 
			
		||||
  await page.goto('/')
 | 
			
		||||
  await u.waitForAuthSkipAppStart()
 | 
			
		||||
  await u.openDebugPanel()
 | 
			
		||||
 | 
			
		||||
  const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
 | 
			
		||||
  const { toSU, click00r, expectCodeToBe } = getMovementUtils({ center, page })
 | 
			
		||||
 | 
			
		||||
  await expect(
 | 
			
		||||
    page.getByRole('button', { name: 'Start Sketch' })
 | 
			
		||||
  ).not.toBeDisabled()
 | 
			
		||||
@ -1502,127 +1515,71 @@ test('Can add multiple sketches', async ({ page }) => {
 | 
			
		||||
    200
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  // select a plane
 | 
			
		||||
  await page.mouse.click(700, 200)
 | 
			
		||||
 | 
			
		||||
  await expect(page.locator('.cm-content')).toHaveText(
 | 
			
		||||
    `const part001 = startSketchOn('XZ')`
 | 
			
		||||
  )
 | 
			
		||||
  let codeStr = "const part001 = startSketchOn('XY')"
 | 
			
		||||
 | 
			
		||||
  await page.mouse.click(center.x, viewportSize.height * 0.55)
 | 
			
		||||
  await expectCodeToBe(codeStr)
 | 
			
		||||
  await u.closeDebugPanel()
 | 
			
		||||
  await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
 | 
			
		||||
 | 
			
		||||
  const startXPx = 600
 | 
			
		||||
  await u.closeDebugPanel()
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
 | 
			
		||||
  await expect(page.locator('.cm-content'))
 | 
			
		||||
    .toHaveText(`const part001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)`)
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
  await click00r(0, 0)
 | 
			
		||||
  codeStr += `  |> startProfileAt(${toSU([0, 0])}, %)`
 | 
			
		||||
  await expectCodeToBe(codeStr)
 | 
			
		||||
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
  await click00r(50, 0)
 | 
			
		||||
  codeStr += `  |> line(${toSU([50, 0])}, %)`
 | 
			
		||||
  await expectCodeToBe(codeStr)
 | 
			
		||||
 | 
			
		||||
  await expect(page.locator('.cm-content'))
 | 
			
		||||
    .toHaveText(`const part001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)
 | 
			
		||||
  |> line([${commonPoints.num1}, 0], %)`)
 | 
			
		||||
  await click00r(0, 50)
 | 
			
		||||
  codeStr += `  |> line(${toSU([0, 50])}, %)`
 | 
			
		||||
  await expectCodeToBe(codeStr)
 | 
			
		||||
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
 | 
			
		||||
  await expect(page.locator('.cm-content'))
 | 
			
		||||
    .toHaveText(`const part001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)
 | 
			
		||||
  |> line([${commonPoints.num1}, 0], %)
 | 
			
		||||
  |> line([0, ${commonPoints.num1}], %)`)
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
  await page.mouse.click(startXPx, 500 - PUR * 20)
 | 
			
		||||
  const finalCodeFirstSketch = `const part001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)
 | 
			
		||||
  |> line([${commonPoints.num1}, 0], %)
 | 
			
		||||
  |> line([0, ${commonPoints.num1}], %)
 | 
			
		||||
  |> line([-${commonPoints.num2}, 0], %)`
 | 
			
		||||
  await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)
 | 
			
		||||
 | 
			
		||||
  // exit the sketch
 | 
			
		||||
  await click00r(-50, 0)
 | 
			
		||||
  codeStr += `  |> line(${toSU([-50, 0])}, %)`
 | 
			
		||||
  await expectCodeToBe(codeStr)
 | 
			
		||||
 | 
			
		||||
  // exit the sketch, reset relative clicker
 | 
			
		||||
  click00r(undefined, undefined)
 | 
			
		||||
  await u.openAndClearDebugPanel()
 | 
			
		||||
  await page.getByRole('button', { name: 'Exit Sketch' }).click()
 | 
			
		||||
 | 
			
		||||
  await u.expectCmdLog('[data-message-type="execution-done"]')
 | 
			
		||||
 | 
			
		||||
  await u.updateCamPosition([100, 100, 100])
 | 
			
		||||
  await page.waitForTimeout(250)
 | 
			
		||||
  await u.clearCommandLogs()
 | 
			
		||||
 | 
			
		||||
  // start a new sketch
 | 
			
		||||
  await u.clearCommandLogs()
 | 
			
		||||
  await page.getByRole('button', { name: 'Start Sketch' }).click()
 | 
			
		||||
  await page.waitForTimeout(400)
 | 
			
		||||
  await page.mouse.click(650, 450)
 | 
			
		||||
 | 
			
		||||
  // when exiting the sketch above the camera is still looking down at XY,
 | 
			
		||||
  // so selecting the plane again is a bit easier.
 | 
			
		||||
  await page.mouse.click(center.x + 30, center.y)
 | 
			
		||||
  await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
 | 
			
		||||
  await u.clearAndCloseDebugPanel()
 | 
			
		||||
 | 
			
		||||
  // on mock os there are issues with getting the camera to update
 | 
			
		||||
  // it should not be selecting the 'XZ' plane here if the camera updated
 | 
			
		||||
  // properly, but if we just role with it we can still verify everything
 | 
			
		||||
  // in the rest of the test
 | 
			
		||||
  const plane = process.platform === 'darwin' ? 'XZ' : 'XY'
 | 
			
		||||
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
 | 
			
		||||
  const startAt2 =
 | 
			
		||||
    process.platform === 'darwin' ? '[9.75, -13.16]' : '[0.93, -1.25]'
 | 
			
		||||
  await expect(
 | 
			
		||||
    (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
 | 
			
		||||
  ).toBe(
 | 
			
		||||
    `${finalCodeFirstSketch}
 | 
			
		||||
const part002 = startSketchOn('${plane}')
 | 
			
		||||
  |> startProfileAt(${startAt2}, %)`.replace(/\s/g, '')
 | 
			
		||||
  )
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
  codeStr += "const part002 = startSketchOn('XY')"
 | 
			
		||||
  await expectCodeToBe(codeStr)
 | 
			
		||||
  await u.closeDebugPanel()
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
 | 
			
		||||
  const num2 = process.platform === 'darwin' ? 9.84 : 0.94
 | 
			
		||||
  await expect(
 | 
			
		||||
    (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
 | 
			
		||||
  ).toBe(
 | 
			
		||||
    `${finalCodeFirstSketch}
 | 
			
		||||
const part002 = startSketchOn('${plane}')
 | 
			
		||||
  |> startProfileAt(${startAt2}, %)
 | 
			
		||||
  |> line([${num2}, 0], %)`.replace(/\s/g, '')
 | 
			
		||||
  )
 | 
			
		||||
  await click00r(30, 0)
 | 
			
		||||
  codeStr += `  |> startProfileAt(${toSU([30, 0])}, %)`
 | 
			
		||||
  await expectCodeToBe(codeStr)
 | 
			
		||||
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
 | 
			
		||||
  await expect(
 | 
			
		||||
    (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
 | 
			
		||||
  ).toBe(
 | 
			
		||||
    `${finalCodeFirstSketch}
 | 
			
		||||
const part002 = startSketchOn('${plane}')
 | 
			
		||||
  |> startProfileAt(${startAt2}, %)
 | 
			
		||||
  |> line([${num2}, 0], %)
 | 
			
		||||
  |> line([0, ${roundOff(
 | 
			
		||||
    num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
 | 
			
		||||
  )}], %)`.replace(/\s/g, '')
 | 
			
		||||
  )
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
  await page.mouse.click(startXPx, 500 - PUR * 20)
 | 
			
		||||
  await expect(
 | 
			
		||||
    (await page.locator('.cm-content').innerText()).replace(/\s/g, '')
 | 
			
		||||
  ).toBe(
 | 
			
		||||
    `${finalCodeFirstSketch}
 | 
			
		||||
const part002 = startSketchOn('${plane}')
 | 
			
		||||
  |> startProfileAt(${startAt2}, %)
 | 
			
		||||
  |> line([${num2}, 0], %)
 | 
			
		||||
  |> line([0, ${roundOff(
 | 
			
		||||
    num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
 | 
			
		||||
  )}], %)
 | 
			
		||||
  |> line([-${process.platform === 'darwin' ? 19.59 : 1.87}, 0], %)`.replace(
 | 
			
		||||
      /\s/g,
 | 
			
		||||
      ''
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
  await click00r(30, 0)
 | 
			
		||||
  codeStr += `  |> line(${toSU([30 - 0.1 /* imprecision */, 0])}, %)`
 | 
			
		||||
  await expectCodeToBe(codeStr)
 | 
			
		||||
 | 
			
		||||
  await click00r(0, 30)
 | 
			
		||||
  codeStr += `  |> line(${toSU([0, 30])}, %)`
 | 
			
		||||
  await expectCodeToBe(codeStr)
 | 
			
		||||
 | 
			
		||||
  await click00r(-30, 0)
 | 
			
		||||
  codeStr += `  |> line(${toSU([-30 + 0.1, 0])}, %)`
 | 
			
		||||
  await expectCodeToBe(codeStr)
 | 
			
		||||
 | 
			
		||||
  click00r(undefined, undefined)
 | 
			
		||||
  await u.openAndClearDebugPanel()
 | 
			
		||||
  await page.getByRole('button', { name: 'Exit Sketch' }).click()
 | 
			
		||||
  await u.expectCmdLog('[data-message-type="execution-done"]')
 | 
			
		||||
  await u.updateCamPosition([100, 100, 100])
 | 
			
		||||
  await page.waitForTimeout(250)
 | 
			
		||||
  await u.clearCommandLogs()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('ProgramMemory can be serialised', async ({ page }) => {
 | 
			
		||||
@ -3246,7 +3203,7 @@ test.describe('Testing segment overlays', () => {
 | 
			
		||||
        expectAfterUnconstrained,
 | 
			
		||||
        expectFinal,
 | 
			
		||||
        ang = 45,
 | 
			
		||||
        steps = 6,
 | 
			
		||||
        steps = 10,
 | 
			
		||||
      }: {
 | 
			
		||||
        hoverPos: { x: number; y: number }
 | 
			
		||||
        constraintType:
 | 
			
		||||
@ -3261,13 +3218,16 @@ test.describe('Testing segment overlays', () => {
 | 
			
		||||
        steps?: number
 | 
			
		||||
      }) => {
 | 
			
		||||
        await expect(page.getByText('Added variable')).not.toBeVisible()
 | 
			
		||||
        const [x, y] = [
 | 
			
		||||
          Math.cos((ang * Math.PI) / 180) * 45,
 | 
			
		||||
          Math.sin((ang * Math.PI) / 180) * 45,
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        await page.mouse.move(hoverPos.x + x, hoverPos.y + y)
 | 
			
		||||
        await page.mouse.move(hoverPos.x, hoverPos.y, { steps })
 | 
			
		||||
        await page.mouse.move(0, 0)
 | 
			
		||||
        await page.waitForTimeout(1000)
 | 
			
		||||
        let x = 0,
 | 
			
		||||
          y = 0
 | 
			
		||||
        x = hoverPos.x + Math.cos(ang * deg) * 32
 | 
			
		||||
        y = hoverPos.y - Math.sin(ang * deg) * 32
 | 
			
		||||
        await page.mouse.move(x, y)
 | 
			
		||||
        await wiggleMove(page, x, y, 20, 30, ang, 10, 5)
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('.cm-content')).toContainText(
 | 
			
		||||
          expectBeforeUnconstrained
 | 
			
		||||
        )
 | 
			
		||||
@ -3283,6 +3243,14 @@ test.describe('Testing segment overlays', () => {
 | 
			
		||||
        await expect(page.locator('.cm-content')).toContainText(
 | 
			
		||||
          expectAfterUnconstrained
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        await page.mouse.move(0, 0)
 | 
			
		||||
        await page.waitForTimeout(1000)
 | 
			
		||||
        x = hoverPos.x + Math.cos(ang * deg) * 32
 | 
			
		||||
        y = hoverPos.y - Math.sin(ang * deg) * 32
 | 
			
		||||
        await page.mouse.move(x, y)
 | 
			
		||||
        await wiggleMove(page, x, y, 20, 30, ang, 10, 5)
 | 
			
		||||
 | 
			
		||||
        const unconstrainedLocator = page.locator(
 | 
			
		||||
          `[data-constraint-type="${constraintType}"][data-is-constrained="false"]`
 | 
			
		||||
        )
 | 
			
		||||
@ -3328,14 +3296,16 @@ test.describe('Testing segment overlays', () => {
 | 
			
		||||
        ang?: number
 | 
			
		||||
        steps?: number
 | 
			
		||||
      }) => {
 | 
			
		||||
        const [x, y] = [
 | 
			
		||||
          Math.cos((ang * Math.PI) / 180) * 45,
 | 
			
		||||
          Math.sin((ang * Math.PI) / 180) * 45,
 | 
			
		||||
        ]
 | 
			
		||||
        await page.mouse.move(hoverPos.x + x, hoverPos.y + y)
 | 
			
		||||
        await page.mouse.move(0, 0)
 | 
			
		||||
        await page.waitForTimeout(1000)
 | 
			
		||||
        let x = 0,
 | 
			
		||||
          y = 0
 | 
			
		||||
        x = hoverPos.x + Math.cos(ang * deg) * 32
 | 
			
		||||
        y = hoverPos.y - Math.sin(ang * deg) * 32
 | 
			
		||||
        await page.mouse.move(x, y)
 | 
			
		||||
        await wiggleMove(page, x, y, 20, 30, ang, 10, 5)
 | 
			
		||||
 | 
			
		||||
        await expect(page.getByText('Added variable')).not.toBeVisible()
 | 
			
		||||
        await page.mouse.move(hoverPos.x, hoverPos.y, { steps })
 | 
			
		||||
        await expect(page.locator('.cm-content')).toContainText(
 | 
			
		||||
          expectBeforeUnconstrained
 | 
			
		||||
        )
 | 
			
		||||
@ -3353,7 +3323,14 @@ test.describe('Testing segment overlays', () => {
 | 
			
		||||
          expectAfterUnconstrained
 | 
			
		||||
        )
 | 
			
		||||
        await expect(page.getByText('Added variable')).not.toBeVisible()
 | 
			
		||||
        await page.mouse.move(hoverPos.x, hoverPos.y, { steps })
 | 
			
		||||
 | 
			
		||||
        await page.mouse.move(0, 0)
 | 
			
		||||
        await page.waitForTimeout(1000)
 | 
			
		||||
        x = hoverPos.x + Math.cos(ang * deg) * 32
 | 
			
		||||
        y = hoverPos.y - Math.sin(ang * deg) * 32
 | 
			
		||||
        await page.mouse.move(x, y)
 | 
			
		||||
        await wiggleMove(page, x, y, 20, 30, ang, 10, 5)
 | 
			
		||||
 | 
			
		||||
        const constrainedLocator = page.locator(
 | 
			
		||||
          `[data-constraint-type="${constraintType}"][data-is-constrained="true"]`
 | 
			
		||||
        )
 | 
			
		||||
@ -3365,6 +3342,7 @@ test.describe('Testing segment overlays', () => {
 | 
			
		||||
        await constrainedLocator.click()
 | 
			
		||||
        await expect(page.locator('.cm-content')).toContainText(expectFinal)
 | 
			
		||||
      }
 | 
			
		||||
    test.setTimeout(120000)
 | 
			
		||||
    test('for segments [line, angledLine, lineTo, xLineTo]', async ({
 | 
			
		||||
      page,
 | 
			
		||||
    }) => {
 | 
			
		||||
@ -3372,24 +3350,24 @@ test.describe('Testing segment overlays', () => {
 | 
			
		||||
        localStorage.setItem(
 | 
			
		||||
          'persistCode',
 | 
			
		||||
          `const part001 = startSketchOn('XZ')
 | 
			
		||||
    |> startProfileAt([0, 0], %)
 | 
			
		||||
    |> startProfileAt([5 + 0, 20 + 0], %)
 | 
			
		||||
    |> line([0.5, -14 + 0], %)
 | 
			
		||||
    |> angledLine({ angle: 3 + 0, length: 32 + 0 }, %)
 | 
			
		||||
    |> lineTo([33, 11.5 + 0], %)
 | 
			
		||||
    |> xLineTo(9 - 5, %)
 | 
			
		||||
    |> yLineTo(-10.77, %, 'a')
 | 
			
		||||
    |> lineTo([5 + 33, 20 + 11.5 + 0], %)
 | 
			
		||||
    |> xLineTo(5 + 9 - 5, %)
 | 
			
		||||
    |> yLineTo(20 + -10.77, %, 'a')
 | 
			
		||||
    |> xLine(26.04, %)
 | 
			
		||||
    |> yLine(21.14 + 0, %)
 | 
			
		||||
    |> angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %)
 | 
			
		||||
    |> angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)
 | 
			
		||||
    |> angledLineToX({ angle: 3 + 0, to: 26 }, %)
 | 
			
		||||
    |> angledLineToY({ angle: 89, to: 9.14 + 0 }, %)
 | 
			
		||||
    |> angledLineToX({ angle: 3 + 0, to: 5 + 26 }, %)
 | 
			
		||||
    |> angledLineToY({ angle: 89, to: 20 + 9.14 + 0 }, %)
 | 
			
		||||
    |> angledLineThatIntersects({
 | 
			
		||||
          angle: 4.14,
 | 
			
		||||
          intersectTag: 'a',
 | 
			
		||||
          offset: 9
 | 
			
		||||
        }, %)
 | 
			
		||||
    |> tangentialArcTo([3.14 + 13, 3.14], %)
 | 
			
		||||
    |> tangentialArcTo([5 + 3.14 + 13, 20 + 3.14], %)
 | 
			
		||||
        `
 | 
			
		||||
        )
 | 
			
		||||
      })
 | 
			
		||||
@ -3423,8 +3401,9 @@ test.describe('Testing segment overlays', () => {
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
      await page.waitForTimeout(100)
 | 
			
		||||
      await u.closeDebugPanel()
 | 
			
		||||
 | 
			
		||||
      await page.getByText('xLineTo(9 - 5, %)').click()
 | 
			
		||||
      await page.getByText('xLineTo(5 + 9 - 5, %)').click()
 | 
			
		||||
      await page.waitForTimeout(100)
 | 
			
		||||
      await page.getByRole('button', { name: 'Edit Sketch' }).click()
 | 
			
		||||
      await page.waitForTimeout(500)
 | 
			
		||||
@ -3434,45 +3413,62 @@ test.describe('Testing segment overlays', () => {
 | 
			
		||||
      const clickUnconstrained = _clickUnconstrained(page)
 | 
			
		||||
      const clickConstrained = _clickConstrained(page)
 | 
			
		||||
 | 
			
		||||
      // Drag the sketch into view
 | 
			
		||||
      await page.mouse.move(600, 64)
 | 
			
		||||
      await page.mouse.down({ button: 'middle' })
 | 
			
		||||
      await page.mouse.move(600, 450, { steps: 10 })
 | 
			
		||||
      await page.mouse.up({ button: 'middle' })
 | 
			
		||||
 | 
			
		||||
      await page.mouse.move(600, 64)
 | 
			
		||||
      await page.mouse.down({ button: 'middle' })
 | 
			
		||||
      await page.mouse.move(600, 120, { steps: 10 })
 | 
			
		||||
      await page.mouse.up({ button: 'middle' })
 | 
			
		||||
 | 
			
		||||
      let ang = 0
 | 
			
		||||
 | 
			
		||||
      const line = await u.getBoundingBox(`[data-overlay-index="${0}"]`)
 | 
			
		||||
      console.log('line1')
 | 
			
		||||
      ang = await u.getAngle(`[data-overlay-index="${0}"]`)
 | 
			
		||||
      console.log('line1', line, ang)
 | 
			
		||||
      await clickConstrained({
 | 
			
		||||
        hoverPos: { x: line.x, y: line.y - 10 },
 | 
			
		||||
        hoverPos: { x: line.x, y: line.y },
 | 
			
		||||
        constraintType: 'yRelative',
 | 
			
		||||
        expectBeforeUnconstrained: '|> line([0.5, -14 + 0], %)',
 | 
			
		||||
        expectAfterUnconstrained: '|> line([0.5, -14], %)',
 | 
			
		||||
        expectFinal: '|> line([0.5, yRel001], %)',
 | 
			
		||||
        ang: 135,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
      console.log('line2')
 | 
			
		||||
      await clickUnconstrained({
 | 
			
		||||
        hoverPos: { x: line.x, y: line.y - 10 },
 | 
			
		||||
        hoverPos: { x: line.x, y: line.y },
 | 
			
		||||
        constraintType: 'xRelative',
 | 
			
		||||
        expectBeforeUnconstrained: '|> line([0.5, yRel001], %)',
 | 
			
		||||
        expectAfterUnconstrained: 'line([xRel001, yRel001], %)',
 | 
			
		||||
        expectFinal: '|> line([0.5, yRel001], %)',
 | 
			
		||||
        ang: -45,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const angledLine = await u.getBoundingBox(`[data-overlay-index="1"]`)
 | 
			
		||||
      ang = await u.getAngle(`[data-overlay-index="1"]`)
 | 
			
		||||
      console.log('angledLine1')
 | 
			
		||||
      await clickConstrained({
 | 
			
		||||
        hoverPos: { x: angledLine.x - 10, y: angledLine.y },
 | 
			
		||||
        hoverPos: { x: angledLine.x, y: angledLine.y },
 | 
			
		||||
        constraintType: 'angle',
 | 
			
		||||
        expectBeforeUnconstrained:
 | 
			
		||||
          'angledLine({ angle: 3 + 0, length: 32 + 0 }, %)',
 | 
			
		||||
        expectAfterUnconstrained: 'angledLine({ angle: 3, length: 32 + 0 }, %)',
 | 
			
		||||
        expectFinal: 'angledLine({ angle: angle001, length: 32 + 0 }, %)',
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
      console.log('angledLine2')
 | 
			
		||||
      await clickConstrained({
 | 
			
		||||
        hoverPos: { x: angledLine.x - 10, y: angledLine.y },
 | 
			
		||||
        hoverPos: { x: angledLine.x, y: angledLine.y },
 | 
			
		||||
        constraintType: 'length',
 | 
			
		||||
        expectBeforeUnconstrained:
 | 
			
		||||
          'angledLine({ angle: angle001, length: 32 + 0 }, %)',
 | 
			
		||||
        expectAfterUnconstrained:
 | 
			
		||||
          'angledLine({ angle: angle001, length: 32 }, %)',
 | 
			
		||||
        expectFinal: 'angledLine({ angle: angle001, length: len001 }, %)',
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await page.mouse.move(700, 250)
 | 
			
		||||
@ -3482,36 +3478,39 @@ test.describe('Testing segment overlays', () => {
 | 
			
		||||
      }
 | 
			
		||||
      await page.waitForTimeout(200)
 | 
			
		||||
 | 
			
		||||
      const lineTo = await u.getBoundingBox(`[data-overlay-index="2"]`)
 | 
			
		||||
      let lineTo = await u.getBoundingBox(`[data-overlay-index="2"]`)
 | 
			
		||||
      ang = await u.getAngle(`[data-overlay-index="2"]`)
 | 
			
		||||
      console.log('lineTo1')
 | 
			
		||||
      await clickConstrained({
 | 
			
		||||
        hoverPos: { x: lineTo.x, y: lineTo.y + 21 },
 | 
			
		||||
        hoverPos: { x: lineTo.x, y: lineTo.y },
 | 
			
		||||
        constraintType: 'yAbsolute',
 | 
			
		||||
        expectBeforeUnconstrained: 'lineTo([33, 11.5 + 0], %)',
 | 
			
		||||
        expectAfterUnconstrained: 'lineTo([33, 11.5], %)',
 | 
			
		||||
        expectFinal: 'lineTo([33, yAbs001], %)',
 | 
			
		||||
        expectBeforeUnconstrained: 'lineTo([5 + 33, 20 + 11.5 + 0], %)',
 | 
			
		||||
        expectAfterUnconstrained: 'lineTo([5 + 33, 31.5], %)',
 | 
			
		||||
        expectFinal: 'lineTo([5 + 33, yAbs001], %)',
 | 
			
		||||
        steps: 8,
 | 
			
		||||
        ang: 55,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
      console.log('lineTo2')
 | 
			
		||||
      await clickUnconstrained({
 | 
			
		||||
        hoverPos: { x: lineTo.x, y: lineTo.y + 25 },
 | 
			
		||||
      await clickConstrained({
 | 
			
		||||
        hoverPos: { x: lineTo.x, y: lineTo.y },
 | 
			
		||||
        constraintType: 'xAbsolute',
 | 
			
		||||
        expectBeforeUnconstrained: 'lineTo([33, yAbs001], %)',
 | 
			
		||||
        expectAfterUnconstrained: 'lineTo([xAbs001, yAbs001], %)',
 | 
			
		||||
        expectFinal: 'lineTo([33, yAbs001], %)',
 | 
			
		||||
        expectBeforeUnconstrained: 'lineTo([5 + 33, yAbs001], %)',
 | 
			
		||||
        expectAfterUnconstrained: 'lineTo([38, yAbs001], %)',
 | 
			
		||||
        expectFinal: 'lineTo([xAbs001, yAbs001], %)',
 | 
			
		||||
        steps: 8,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const xLineTo = await u.getBoundingBox(`[data-overlay-index="3"]`)
 | 
			
		||||
      ang = await u.getAngle(`[data-overlay-index="3"]`)
 | 
			
		||||
      console.log('xlineTo1')
 | 
			
		||||
      await clickConstrained({
 | 
			
		||||
        hoverPos: { x: xLineTo.x + 15, y: xLineTo.y },
 | 
			
		||||
        hoverPos: { x: xLineTo.x, y: xLineTo.y },
 | 
			
		||||
        constraintType: 'xAbsolute',
 | 
			
		||||
        expectBeforeUnconstrained: 'xLineTo(9 - 5, %)',
 | 
			
		||||
        expectAfterUnconstrained: 'xLineTo(4, %)',
 | 
			
		||||
        expectBeforeUnconstrained: 'xLineTo(5 + 9 - 5, %)',
 | 
			
		||||
        expectAfterUnconstrained: 'xLineTo(9, %)',
 | 
			
		||||
        expectFinal: 'xLineTo(xAbs002, %)',
 | 
			
		||||
        ang: -45,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
        steps: 8,
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
@ -3566,26 +3565,31 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
 | 
			
		||||
      await page.waitForTimeout(300)
 | 
			
		||||
 | 
			
		||||
      let ang = 0
 | 
			
		||||
 | 
			
		||||
      const yLineTo = await u.getBoundingBox(`[data-overlay-index="4"]`)
 | 
			
		||||
      ang = await u.getAngle(`[data-overlay-index="4"]`)
 | 
			
		||||
      console.log('ylineTo1')
 | 
			
		||||
      await clickUnconstrained({
 | 
			
		||||
        hoverPos: { x: yLineTo.x, y: yLineTo.y - 30 },
 | 
			
		||||
        hoverPos: { x: yLineTo.x, y: yLineTo.y },
 | 
			
		||||
        constraintType: 'yAbsolute',
 | 
			
		||||
        expectBeforeUnconstrained: "yLineTo(-10.77, %, 'a')",
 | 
			
		||||
        expectAfterUnconstrained: "yLineTo(yAbs002, %, 'a')",
 | 
			
		||||
        expectFinal: "yLineTo(-10.77, %, 'a')",
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const xLine = await u.getBoundingBox(`[data-overlay-index="5"]`)
 | 
			
		||||
      ang = await u.getAngle(`[data-overlay-index="5"]`)
 | 
			
		||||
      console.log('xline')
 | 
			
		||||
      await clickUnconstrained({
 | 
			
		||||
        hoverPos: { x: xLine.x - 25, y: xLine.y },
 | 
			
		||||
        hoverPos: { x: xLine.x, y: xLine.y },
 | 
			
		||||
        constraintType: 'xRelative',
 | 
			
		||||
        expectBeforeUnconstrained: 'xLine(26.04, %)',
 | 
			
		||||
        expectAfterUnconstrained: 'xLine(xRel002, %)',
 | 
			
		||||
        expectFinal: 'xLine(26.04, %)',
 | 
			
		||||
        steps: 10,
 | 
			
		||||
        ang: 50,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
    test('for segments [yLine, angledLineOfXLength, angledLineOfYLength]', async ({
 | 
			
		||||
@ -3625,6 +3629,7 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
      await u.openDebugPanel()
 | 
			
		||||
      await u.expectCmdLog('[data-message-type="execution-done"]')
 | 
			
		||||
      await u.closeDebugPanel()
 | 
			
		||||
      await page.waitForTimeout(500)
 | 
			
		||||
 | 
			
		||||
      await page.getByText('xLineTo(9 - 5, %)').click()
 | 
			
		||||
      await page.waitForTimeout(100)
 | 
			
		||||
@ -3636,7 +3641,10 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
      const clickUnconstrained = _clickUnconstrained(page)
 | 
			
		||||
      const clickConstrained = _clickConstrained(page)
 | 
			
		||||
 | 
			
		||||
      let ang = 0
 | 
			
		||||
 | 
			
		||||
      const yLine = await u.getBoundingBox(`[data-overlay-index="6"]`)
 | 
			
		||||
      ang = await u.getAngle(`[data-overlay-index="6"]`)
 | 
			
		||||
      console.log('yline1')
 | 
			
		||||
      await clickConstrained({
 | 
			
		||||
        hoverPos: { x: yLine.x, y: yLine.y + 20 },
 | 
			
		||||
@ -3644,11 +3652,13 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
        expectBeforeUnconstrained: 'yLine(21.14 + 0, %)',
 | 
			
		||||
        expectAfterUnconstrained: 'yLine(21.14, %)',
 | 
			
		||||
        expectFinal: 'yLine(yRel001, %)',
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const angledLineOfXLength = await u.getBoundingBox(
 | 
			
		||||
        `[data-overlay-index="7"]`
 | 
			
		||||
      )
 | 
			
		||||
      ang = await u.getAngle(`[data-overlay-index="7"]`)
 | 
			
		||||
      console.log('angledLineOfXLength1')
 | 
			
		||||
      await clickConstrained({
 | 
			
		||||
        hoverPos: { x: angledLineOfXLength.x + 20, y: angledLineOfXLength.y },
 | 
			
		||||
@ -3659,6 +3669,7 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
          'angledLineOfXLength({ angle: -179, length: 23.14 }, %)',
 | 
			
		||||
        expectFinal:
 | 
			
		||||
          'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)',
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
      console.log('angledLineOfXLength2')
 | 
			
		||||
      await clickUnconstrained({
 | 
			
		||||
@ -3671,11 +3682,13 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
        expectFinal:
 | 
			
		||||
          'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)',
 | 
			
		||||
        steps: 7,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const angledLineOfYLength = await u.getBoundingBox(
 | 
			
		||||
        `[data-overlay-index="8"]`
 | 
			
		||||
      )
 | 
			
		||||
      ang = await u.getAngle(`[data-overlay-index="8"]`)
 | 
			
		||||
      console.log('angledLineOfYLength1')
 | 
			
		||||
      await clickUnconstrained({
 | 
			
		||||
        hoverPos: { x: angledLineOfYLength.x, y: angledLineOfYLength.y - 20 },
 | 
			
		||||
@ -3685,7 +3698,7 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
        expectAfterUnconstrained:
 | 
			
		||||
          'angledLineOfYLength({ angle: angle002, length: 19 + 0 }, %)',
 | 
			
		||||
        expectFinal: 'angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)',
 | 
			
		||||
        ang: 135,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
        steps: 6,
 | 
			
		||||
      })
 | 
			
		||||
      console.log('angledLineOfYLength2')
 | 
			
		||||
@ -3697,14 +3710,13 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
        expectAfterUnconstrained:
 | 
			
		||||
          'angledLineOfYLength({ angle: -91, length: 19 }, %)',
 | 
			
		||||
        expectFinal: 'angledLineOfYLength({ angle: -91, length: yRel002 }, %)',
 | 
			
		||||
        ang: -45,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
        steps: 7,
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
    test('for segments [angledLineToX, angledLineToY, angledLineThatIntersects]', async ({
 | 
			
		||||
      page,
 | 
			
		||||
    }) => {
 | 
			
		||||
      test.skip(process.platform !== 'darwin', 'too flakey on ubuntu')
 | 
			
		||||
      await page.addInitScript(async () => {
 | 
			
		||||
        localStorage.setItem(
 | 
			
		||||
          'persistCode',
 | 
			
		||||
@ -3750,14 +3762,18 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
      const clickUnconstrained = _clickUnconstrained(page)
 | 
			
		||||
      const clickConstrained = _clickConstrained(page)
 | 
			
		||||
 | 
			
		||||
      let ang = 0
 | 
			
		||||
 | 
			
		||||
      const angledLineToX = await u.getBoundingBox(`[data-overlay-index="9"]`)
 | 
			
		||||
      ang = await u.getAngle(`[data-overlay-index="9"]`)
 | 
			
		||||
      console.log('angledLineToX')
 | 
			
		||||
      await clickConstrained({
 | 
			
		||||
        hoverPos: { x: angledLineToX.x - 20, y: angledLineToX.y },
 | 
			
		||||
        hoverPos: { x: angledLineToX.x, y: angledLineToX.y },
 | 
			
		||||
        constraintType: 'angle',
 | 
			
		||||
        expectBeforeUnconstrained: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)',
 | 
			
		||||
        expectAfterUnconstrained: 'angledLineToX({ angle: 3, to: 26 }, %)',
 | 
			
		||||
        expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)',
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
      console.log('angledLineToX2')
 | 
			
		||||
      await clickUnconstrained({
 | 
			
		||||
@ -3768,12 +3784,14 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
        expectAfterUnconstrained:
 | 
			
		||||
          'angledLineToX({ angle: angle001, to: xAbs001 }, %)',
 | 
			
		||||
        expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)',
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const angledLineToY = await u.getBoundingBox(`[data-overlay-index="10"]`)
 | 
			
		||||
      ang = await u.getAngle(`[data-overlay-index="10"]`)
 | 
			
		||||
      console.log('angledLineToY')
 | 
			
		||||
      await clickUnconstrained({
 | 
			
		||||
        hoverPos: { x: angledLineToY.x, y: angledLineToY.y + 20 },
 | 
			
		||||
        hoverPos: { x: angledLineToY.x, y: angledLineToY.y },
 | 
			
		||||
        constraintType: 'angle',
 | 
			
		||||
        expectBeforeUnconstrained:
 | 
			
		||||
          'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
 | 
			
		||||
@ -3781,7 +3799,7 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
          'angledLineToY({ angle: angle002, to: 9.14 + 0 }, %)',
 | 
			
		||||
        expectFinal: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
 | 
			
		||||
        steps: process.platform === 'darwin' ? 8 : 9,
 | 
			
		||||
        ang: 135,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
      console.log('angledLineToY2')
 | 
			
		||||
      await clickConstrained({
 | 
			
		||||
@ -3791,12 +3809,13 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
          'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
 | 
			
		||||
        expectAfterUnconstrained: 'angledLineToY({ angle: 89, to: 9.14 }, %)',
 | 
			
		||||
        expectFinal: 'angledLineToY({ angle: 89, to: yAbs001 }, %)',
 | 
			
		||||
        ang: 135,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const angledLineThatIntersects = await u.getBoundingBox(
 | 
			
		||||
        `[data-overlay-index="11"]`
 | 
			
		||||
      )
 | 
			
		||||
      ang = await u.getAngle(`[data-overlay-index="11"]`)
 | 
			
		||||
      console.log('angledLineThatIntersects')
 | 
			
		||||
      await clickUnconstrained({
 | 
			
		||||
        hoverPos: {
 | 
			
		||||
@ -3819,7 +3838,7 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
      offset: 9,
 | 
			
		||||
      intersectTag: 'a'
 | 
			
		||||
    }, %)`,
 | 
			
		||||
        ang: -45,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
      console.log('angledLineThatIntersects2')
 | 
			
		||||
      await clickUnconstrained({
 | 
			
		||||
@ -3843,7 +3862,7 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
      offset: 9,
 | 
			
		||||
      intersectTag: 'a'
 | 
			
		||||
    }, %)`,
 | 
			
		||||
        ang: -25,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
    test('for segment [tangentialArcTo]', async ({ page }) => {
 | 
			
		||||
@ -3895,24 +3914,25 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
      const tangentialArcTo = await u.getBoundingBox(
 | 
			
		||||
        `[data-overlay-index="12"]`
 | 
			
		||||
      )
 | 
			
		||||
      let ang = await u.getAngle(`[data-overlay-index="12"]`)
 | 
			
		||||
      console.log('tangentialArcTo')
 | 
			
		||||
      await clickConstrained({
 | 
			
		||||
        hoverPos: { x: tangentialArcTo.x - 10, y: tangentialArcTo.y + 20 },
 | 
			
		||||
        hoverPos: { x: tangentialArcTo.x, y: tangentialArcTo.y },
 | 
			
		||||
        constraintType: 'xAbsolute',
 | 
			
		||||
        expectBeforeUnconstrained: 'tangentialArcTo([3.14 + 13, -3.14], %)',
 | 
			
		||||
        expectAfterUnconstrained: 'tangentialArcTo([16.14, -3.14], %)',
 | 
			
		||||
        expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)',
 | 
			
		||||
        ang: -45,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
        steps: 6,
 | 
			
		||||
      })
 | 
			
		||||
      console.log('tangentialArcTo2')
 | 
			
		||||
      await clickUnconstrained({
 | 
			
		||||
        hoverPos: { x: tangentialArcTo.x - 10, y: tangentialArcTo.y + 20 },
 | 
			
		||||
        hoverPos: { x: tangentialArcTo.x, y: tangentialArcTo.y },
 | 
			
		||||
        constraintType: 'yAbsolute',
 | 
			
		||||
        expectBeforeUnconstrained: 'tangentialArcTo([xAbs001, -3.14], %)',
 | 
			
		||||
        expectAfterUnconstrained: 'tangentialArcTo([xAbs001, yAbs001], %)',
 | 
			
		||||
        expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)',
 | 
			
		||||
        ang: -135,
 | 
			
		||||
        ang: ang + 180,
 | 
			
		||||
        steps: 10,
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
@ -4007,25 +4027,7 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
        steps: 6,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(0)
 | 
			
		||||
      await deleteSegmentSequence({
 | 
			
		||||
        hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 20 },
 | 
			
		||||
        codeToBeDeleted: 'line([0.5, -14 + 0], %)',
 | 
			
		||||
        stdLibFnName: 'line',
 | 
			
		||||
        ang: -45,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(0)
 | 
			
		||||
      await deleteSegmentSequence({
 | 
			
		||||
        hoverPos: { x: segmentToDelete.x - 20, y: segmentToDelete.y },
 | 
			
		||||
        codeToBeDeleted: 'angledLine({ angle: 3 + 0, length: 32 + 0 }, %)',
 | 
			
		||||
        stdLibFnName: 'angledLine',
 | 
			
		||||
        ang: 135,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await page.waitForTimeout(200)
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(9)
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(11)
 | 
			
		||||
      await deleteSegmentSequence({
 | 
			
		||||
        hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
 | 
			
		||||
        codeToBeDeleted: `angledLineThatIntersects({
 | 
			
		||||
@ -4038,21 +4040,21 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
        steps: 7,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(8)
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(10)
 | 
			
		||||
      await deleteSegmentSequence({
 | 
			
		||||
        hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
 | 
			
		||||
        codeToBeDeleted: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
 | 
			
		||||
        stdLibFnName: 'angledLineToY',
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(7)
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(9)
 | 
			
		||||
      await deleteSegmentSequence({
 | 
			
		||||
        hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y },
 | 
			
		||||
        codeToBeDeleted: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)',
 | 
			
		||||
        stdLibFnName: 'angledLineToX',
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(6)
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(8)
 | 
			
		||||
      await deleteSegmentSequence({
 | 
			
		||||
        hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 },
 | 
			
		||||
        codeToBeDeleted:
 | 
			
		||||
@ -4060,7 +4062,7 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
        stdLibFnName: 'angledLineOfYLength',
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(5)
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(7)
 | 
			
		||||
      await deleteSegmentSequence({
 | 
			
		||||
        hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
 | 
			
		||||
        codeToBeDeleted:
 | 
			
		||||
@ -4068,42 +4070,36 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
        stdLibFnName: 'angledLineOfXLength',
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(4)
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(6)
 | 
			
		||||
      await deleteSegmentSequence({
 | 
			
		||||
        hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y + 10 },
 | 
			
		||||
        codeToBeDeleted: 'yLine(21.14 + 0, %)',
 | 
			
		||||
        stdLibFnName: 'yLine',
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(3)
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(5)
 | 
			
		||||
      await deleteSegmentSequence({
 | 
			
		||||
        hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y },
 | 
			
		||||
        codeToBeDeleted: 'xLine(26.04, %)',
 | 
			
		||||
        stdLibFnName: 'xLine',
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(2)
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(4)
 | 
			
		||||
      await deleteSegmentSequence({
 | 
			
		||||
        hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 },
 | 
			
		||||
        codeToBeDeleted: "yLineTo(-10.77, %, 'a')",
 | 
			
		||||
        stdLibFnName: 'yLineTo',
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(1)
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(3)
 | 
			
		||||
      await deleteSegmentSequence({
 | 
			
		||||
        hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
 | 
			
		||||
        codeToBeDeleted: 'xLineTo(9 - 5, %)',
 | 
			
		||||
        stdLibFnName: 'xLineTo',
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      for (let i = 0; i < 15; i++) {
 | 
			
		||||
        await page.mouse.wheel(0, 100)
 | 
			
		||||
        await page.waitForTimeout(25)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await page.waitForTimeout(200)
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(0)
 | 
			
		||||
      // Not sure why this is diff. from the others - Kurt, ideas?
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(2)
 | 
			
		||||
      const hoverPos = { x: segmentToDelete.x - 10, y: segmentToDelete.y + 10 }
 | 
			
		||||
      await expect(page.getByText('Added variable')).not.toBeVisible()
 | 
			
		||||
      const [x, y] = [
 | 
			
		||||
@ -4122,6 +4118,24 @@ const part001 = startSketchOn('XZ')
 | 
			
		||||
      await expect(page.locator('.cm-content')).not.toContainText(
 | 
			
		||||
        codeToBeDeleted
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(1)
 | 
			
		||||
      await deleteSegmentSequence({
 | 
			
		||||
        hoverPos: { x: segmentToDelete.x - 20, y: segmentToDelete.y },
 | 
			
		||||
        codeToBeDeleted: 'angledLine({ angle: 3 + 0, length: 32 + 0 }, %)',
 | 
			
		||||
        stdLibFnName: 'angledLine',
 | 
			
		||||
        ang: 135,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      segmentToDelete = await getOverlayByIndex(0)
 | 
			
		||||
      await deleteSegmentSequence({
 | 
			
		||||
        hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 20 },
 | 
			
		||||
        codeToBeDeleted: 'line([0.5, -14 + 0], %)',
 | 
			
		||||
        stdLibFnName: 'line',
 | 
			
		||||
        ang: -45,
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await page.waitForTimeout(200)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
  test.describe('Testing delete with dependent segments', () => {
 | 
			
		||||
@ -4541,6 +4555,11 @@ test('simulate network down and network little widget', async ({ page }) => {
 | 
			
		||||
  await page.goto('/')
 | 
			
		||||
  await u.waitForAuthSkipAppStart()
 | 
			
		||||
 | 
			
		||||
  // This is how we wait until the stream is online
 | 
			
		||||
  await expect(
 | 
			
		||||
    page.getByRole('button', { name: 'Start Sketch' })
 | 
			
		||||
  ).not.toBeDisabled({ timeout: 15000 })
 | 
			
		||||
 | 
			
		||||
  const networkWidget = page.locator('[data-testid="network-toggle"]')
 | 
			
		||||
  await expect(networkWidget).toBeVisible()
 | 
			
		||||
  await networkWidget.hover()
 | 
			
		||||
@ -4548,7 +4567,7 @@ test('simulate network down and network little widget', async ({ page }) => {
 | 
			
		||||
  const networkPopover = page.locator('[data-testid="network-popover"]')
 | 
			
		||||
  await expect(networkPopover).not.toBeVisible()
 | 
			
		||||
 | 
			
		||||
  // Expect the network to be up
 | 
			
		||||
  // (First check) Expect the network to be up
 | 
			
		||||
  await expect(page.getByText('Network Health (Connected)')).toBeVisible()
 | 
			
		||||
 | 
			
		||||
  // Click the network widget
 | 
			
		||||
@ -4580,7 +4599,7 @@ test('simulate network down and network little widget', async ({ page }) => {
 | 
			
		||||
  await expect(networkPopover).toBeVisible()
 | 
			
		||||
 | 
			
		||||
  // Click off the modal.
 | 
			
		||||
  await page.mouse.click(100, 100)
 | 
			
		||||
  await page.mouse.click(0, 0)
 | 
			
		||||
  await expect(networkPopover).not.toBeVisible()
 | 
			
		||||
 | 
			
		||||
  // Turn back on the network
 | 
			
		||||
@ -4592,7 +4611,11 @@ test('simulate network down and network little widget', async ({ page }) => {
 | 
			
		||||
    uploadThroughput: -1,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Expect the network to be up
 | 
			
		||||
  await expect(
 | 
			
		||||
    page.getByRole('button', { name: 'Start Sketch' })
 | 
			
		||||
  ).not.toBeDisabled({ timeout: 15000 })
 | 
			
		||||
 | 
			
		||||
  // (Second check) expect the network to be up
 | 
			
		||||
  await expect(page.getByText('Network Health (Connected)')).toBeVisible()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -4606,8 +4629,7 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
 | 
			
		||||
 | 
			
		||||
  await expect(
 | 
			
		||||
    page.getByRole('button', { name: 'Start Sketch' })
 | 
			
		||||
  ).not.toBeDisabled()
 | 
			
		||||
  await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
 | 
			
		||||
  ).not.toBeDisabled({ timeout: 15000 })
 | 
			
		||||
 | 
			
		||||
  // click on "Start Sketch" button
 | 
			
		||||
  await u.clearCommandLogs()
 | 
			
		||||
@ -4670,6 +4692,10 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Wait for the app to be ready for use
 | 
			
		||||
  await expect(
 | 
			
		||||
    page.getByRole('button', { name: 'Start Sketch' })
 | 
			
		||||
  ).not.toBeDisabled({ timeout: 15000 })
 | 
			
		||||
 | 
			
		||||
  // Expect the network to be up
 | 
			
		||||
  await expect(page.getByText('Network Health (Connected)')).toBeVisible()
 | 
			
		||||
 | 
			
		||||
@ -4697,15 +4723,15 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
 | 
			
		||||
    .toHaveText(`const part001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)
 | 
			
		||||
  |> line([${commonPoints.num1}, 0], %)
 | 
			
		||||
  |> line([-11.59, 11.1], %)`)
 | 
			
		||||
  |> line([-11.64, 11.11], %)`)
 | 
			
		||||
  await page.waitForTimeout(100)
 | 
			
		||||
  await page.mouse.click(startXPx, 500 - PUR * 20)
 | 
			
		||||
  await expect(page.locator('.cm-content'))
 | 
			
		||||
    .toHaveText(`const part001 = startSketchOn('XZ')
 | 
			
		||||
  |> startProfileAt(${commonPoints.startAt}, %)
 | 
			
		||||
  |> line([${commonPoints.num1}, 0], %)
 | 
			
		||||
  |> line([-11.59, 11.1], %)
 | 
			
		||||
  |> line([-6.61, 0], %)`)
 | 
			
		||||
  |> line([-11.64, 11.11], %)
 | 
			
		||||
  |> line([-6.56, 0], %)`)
 | 
			
		||||
 | 
			
		||||
  // Unequip line tool
 | 
			
		||||
  await page.keyboard.press('Escape')
 | 
			
		||||
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB  | 
| 
		 Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB  | 
| 
		 Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB  | 
| 
		 Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB  | 
| 
		 Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB  | 
| 
		 Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB  | 
| 
		 Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB  | 
| 
		 Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB  | 
| 
		 Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 46 KiB  | 
| 
		 Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB  | 
| 
		 Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB  | 
@ -96,6 +96,79 @@ async function waitForCmdReceive(page: Page, commandType: string) {
 | 
			
		||||
    .waitFor()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const wiggleMove = async (
 | 
			
		||||
  page: any,
 | 
			
		||||
  x: number,
 | 
			
		||||
  y: number,
 | 
			
		||||
  steps: number,
 | 
			
		||||
  dist: number,
 | 
			
		||||
  ang: number,
 | 
			
		||||
  amplitude: number,
 | 
			
		||||
  freq: number
 | 
			
		||||
) => {
 | 
			
		||||
  const tau = Math.PI * 2
 | 
			
		||||
  const deg = tau / 360
 | 
			
		||||
  const step = dist / steps
 | 
			
		||||
  for (let i = 0, j = 0; i < dist; i += step, j += 1) {
 | 
			
		||||
    const [x1, y1] = [0, Math.sin((tau / steps) * j * freq) * amplitude]
 | 
			
		||||
    const [x2, y2] = [
 | 
			
		||||
      Math.cos(-ang * deg) * i - Math.sin(-ang * deg) * y1,
 | 
			
		||||
      Math.sin(-ang * deg) * i + Math.cos(-ang * deg) * y1,
 | 
			
		||||
    ]
 | 
			
		||||
    const [xr, yr] = [x2, y2]
 | 
			
		||||
    await page.mouse.move(x + xr, y + yr, { steps: 2 })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getMovementUtils = (opts: any) => {
 | 
			
		||||
  // The way we truncate is kinda odd apparently, so we need this function
 | 
			
		||||
  // "[k]itty[c]ad round"
 | 
			
		||||
  const kcRound = (n: number) => Math.trunc(n * 100) / 100
 | 
			
		||||
 | 
			
		||||
  // To translate between screen and engine ("[U]nit") coordinates
 | 
			
		||||
  // NOTE: these pretty much can't be perfect because of screen scaling.
 | 
			
		||||
  // Handle on a case-by-case.
 | 
			
		||||
  const toU = (x: number, y: number) => [
 | 
			
		||||
    kcRound(x * 0.0854),
 | 
			
		||||
    kcRound(-y * 0.0854), // Y is inverted in our coordinate system
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  // Turn the array into a string with specific formatting
 | 
			
		||||
  const fromUToString = (xy: number[]) => `[${xy[0]}, ${xy[1]}]`
 | 
			
		||||
 | 
			
		||||
  // Combine because used often
 | 
			
		||||
  const toSU = (xy: number[]) => fromUToString(toU(xy[0], xy[1]))
 | 
			
		||||
 | 
			
		||||
  // Make it easier to click around from center ("click [from] zero zero")
 | 
			
		||||
  const click00 = (x: number, y: number) =>
 | 
			
		||||
    opts.page.mouse.click(opts.center.x + x, opts.center.y + y)
 | 
			
		||||
 | 
			
		||||
  // Relative clicker, must keep state
 | 
			
		||||
  let last = { x: 0, y: 0 }
 | 
			
		||||
  const click00r = (x?: number, y?: number) => {
 | 
			
		||||
    // reset relative coordinates when anything is undefined
 | 
			
		||||
    if (x === undefined || y === undefined) {
 | 
			
		||||
      last.x = 0
 | 
			
		||||
      last.y = 0
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const ret = click00(last.x + x, last.y + y)
 | 
			
		||||
    last.x += x
 | 
			
		||||
    last.y += y
 | 
			
		||||
 | 
			
		||||
    // Returns the new absolute coordinate if you need it.
 | 
			
		||||
    return ret.then(() => [last.x, last.y])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const expectCodeToBe = async (str: string) => {
 | 
			
		||||
    await expect(opts.page.locator('.cm-content')).toHaveText(str)
 | 
			
		||||
    await opts.page.waitForTimeout(100)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return { toSU, click00r, expectCodeToBe }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getUtils(page: Page) {
 | 
			
		||||
  // Chrome devtools protocol session only works in Chromium
 | 
			
		||||
  const browserType = page.context().browser()?.browserType().name()
 | 
			
		||||
@ -145,11 +218,15 @@ export async function getUtils(page: Page) {
 | 
			
		||||
        y: bbox.y - angleYOffset,
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    getAngle: async (locator: string) => {
 | 
			
		||||
      const overlay = page.locator(locator)
 | 
			
		||||
      return Number(await overlay.getAttribute('data-overlay-angle'))
 | 
			
		||||
    },
 | 
			
		||||
    getBoundingBox: async (locator: string) =>
 | 
			
		||||
      page
 | 
			
		||||
        .locator(locator)
 | 
			
		||||
        .boundingBox()
 | 
			
		||||
        .then((box) => ({ x: box?.x || 0, y: box?.y || 0 })),
 | 
			
		||||
        .then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
 | 
			
		||||
    doAndWaitForCmd: async (
 | 
			
		||||
      fn: () => Promise<void>,
 | 
			
		||||
      commandType: string,
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@
 | 
			
		||||
    "@fortawesome/react-fontawesome": "^0.2.0",
 | 
			
		||||
    "@headlessui/react": "^1.7.19",
 | 
			
		||||
    "@headlessui/tailwindcss": "^0.2.0",
 | 
			
		||||
    "@kittycad/lib": "^0.0.63",
 | 
			
		||||
    "@kittycad/lib": "^0.0.64",
 | 
			
		||||
    "@lezer/javascript": "^1.4.9",
 | 
			
		||||
    "@open-rpc/client-js": "^1.8.1",
 | 
			
		||||
    "@react-hook/resize-observer": "^2.0.1",
 | 
			
		||||
@ -95,7 +95,8 @@
 | 
			
		||||
    "lint": "eslint --fix src",
 | 
			
		||||
    "bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
 | 
			
		||||
    "postinstall": "yarn xstate:typegen",
 | 
			
		||||
    "xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
 | 
			
		||||
    "xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
 | 
			
		||||
    "make:dev": "make dev"
 | 
			
		||||
  },
 | 
			
		||||
  "prettier": {
 | 
			
		||||
    "trailingComma": "es5",
 | 
			
		||||
 | 
			
		||||
@ -12,12 +12,12 @@ import { defineConfig, devices } from '@playwright/test'
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  testDir: './e2e/playwright',
 | 
			
		||||
  /* Run tests in files in parallel */
 | 
			
		||||
  fullyParallel: true,
 | 
			
		||||
  fullyParallel: false,
 | 
			
		||||
  /* Fail the build on CI if you accidentally left test.only in the source code. */
 | 
			
		||||
  forbidOnly: !!process.env.CI,
 | 
			
		||||
  /* Retry on CI only */
 | 
			
		||||
  retries: process.env.CI ? 3 : 0,
 | 
			
		||||
  /* Opt out of parallel tests on CI. */
 | 
			
		||||
  /* Different amount of parallelism on CI and local. */
 | 
			
		||||
  workers: process.env.CI ? 1 : 1,
 | 
			
		||||
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
 | 
			
		||||
  reporter: 'html',
 | 
			
		||||
@ -72,7 +72,7 @@ export default defineConfig({
 | 
			
		||||
 | 
			
		||||
  /* Run your local dev server before starting the tests */
 | 
			
		||||
  webServer: {
 | 
			
		||||
    command: 'yarn serve',
 | 
			
		||||
    command: 'yarn start',
 | 
			
		||||
    // url: 'http://127.0.0.1:3000',
 | 
			
		||||
    reuseExistingServer: !process.env.CI,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,8 @@ import SignIn from './routes/SignIn'
 | 
			
		||||
import { Auth } from './Auth'
 | 
			
		||||
import { isTauri } from './lib/isTauri'
 | 
			
		||||
import Home from './routes/Home'
 | 
			
		||||
import { NetworkContext } from './hooks/useNetworkContext'
 | 
			
		||||
import { useNetworkStatus } from './hooks/useNetworkStatus'
 | 
			
		||||
import makeUrlPathRelative from './lib/makeUrlPathRelative'
 | 
			
		||||
import DownloadAppBanner from 'components/DownloadAppBanner'
 | 
			
		||||
import { WasmErrBanner } from 'components/WasmErrBanner'
 | 
			
		||||
@ -155,5 +157,11 @@ const router = createBrowserRouter([
 | 
			
		||||
 * @returns RouterProvider
 | 
			
		||||
 */
 | 
			
		||||
export const Router = () => {
 | 
			
		||||
  return <RouterProvider router={router} />
 | 
			
		||||
  const networkStatus = useNetworkStatus()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <NetworkContext.Provider value={networkStatus}>
 | 
			
		||||
      <RouterProvider router={router} />
 | 
			
		||||
    </NetworkContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,13 +3,11 @@ import { isCursorInSketchCommandRange } from 'lang/util'
 | 
			
		||||
import { engineCommandManager, kclManager } from 'lib/singletons'
 | 
			
		||||
import { useModelingContext } from 'hooks/useModelingContext'
 | 
			
		||||
import { useCommandsContext } from 'hooks/useCommandsContext'
 | 
			
		||||
import { useNetworkContext } from 'hooks/useNetworkContext'
 | 
			
		||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
 | 
			
		||||
import { ActionButton } from 'components/ActionButton'
 | 
			
		||||
import { isSingleCursorInPipe } from 'lang/queryAst'
 | 
			
		||||
import { useKclContext } from 'lang/KclProvider'
 | 
			
		||||
import {
 | 
			
		||||
  NetworkHealthState,
 | 
			
		||||
  useNetworkStatus,
 | 
			
		||||
} from 'components/NetworkHealthIndicator'
 | 
			
		||||
import { useStore } from 'useStore'
 | 
			
		||||
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
@ -38,14 +36,16 @@ export function Toolbar({
 | 
			
		||||
  }, [engineCommandManager.artifactMap, context.selectionRanges])
 | 
			
		||||
 | 
			
		||||
  const toolbarButtonsRef = useRef<HTMLUListElement>(null)
 | 
			
		||||
 | 
			
		||||
  const { overallState } = useNetworkStatus()
 | 
			
		||||
  const { overallState } = useNetworkContext()
 | 
			
		||||
  const { isExecuting } = useKclContext()
 | 
			
		||||
  const { isStreamReady } = useStore((s) => ({
 | 
			
		||||
    isStreamReady: s.isStreamReady,
 | 
			
		||||
  }))
 | 
			
		||||
  const disableAllButtons =
 | 
			
		||||
    overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
 | 
			
		||||
    (overallState !== NetworkHealthState.Ok &&
 | 
			
		||||
      overallState !== NetworkHealthState.Weak) ||
 | 
			
		||||
    isExecuting ||
 | 
			
		||||
    !isStreamReady
 | 
			
		||||
 | 
			
		||||
  useHotkeys(
 | 
			
		||||
    'l',
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,65 @@
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  EngineConnectionStateType,
 | 
			
		||||
  DisconnectingType,
 | 
			
		||||
  EngineCommandManagerEvents,
 | 
			
		||||
  EngineConnectionEvents,
 | 
			
		||||
  ConnectionError,
 | 
			
		||||
  CONNECTION_ERROR_TEXT,
 | 
			
		||||
} from '../lang/std/engineConnection'
 | 
			
		||||
 | 
			
		||||
import { engineCommandManager } from '../lib/singletons'
 | 
			
		||||
 | 
			
		||||
const Loading = ({ children }: React.PropsWithChildren) => {
 | 
			
		||||
  const [hasLongLoadTime, setHasLongLoadTime] = useState(false)
 | 
			
		||||
  const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const onConnectionStateChange = ({ detail: state }: CustomEvent) => {
 | 
			
		||||
      if (
 | 
			
		||||
        (state.type !== EngineConnectionStateType.Disconnected ||
 | 
			
		||||
          state.type !== EngineConnectionStateType.Disconnecting) &&
 | 
			
		||||
        state.value?.type !== DisconnectingType.Error
 | 
			
		||||
      )
 | 
			
		||||
        return
 | 
			
		||||
      setError(state.value.value.error)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const onEngineAvailable = ({ detail: engineConnection }: CustomEvent) => {
 | 
			
		||||
      engineConnection.addEventListener(
 | 
			
		||||
        EngineConnectionEvents.ConnectionStateChanged,
 | 
			
		||||
        onConnectionStateChange as EventListener
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    engineCommandManager.addEventListener(
 | 
			
		||||
      EngineCommandManagerEvents.EngineAvailable,
 | 
			
		||||
      onEngineAvailable as EventListener
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      engineCommandManager.removeEventListener(
 | 
			
		||||
        EngineCommandManagerEvents.EngineAvailable,
 | 
			
		||||
        onEngineAvailable as EventListener
 | 
			
		||||
      )
 | 
			
		||||
      engineCommandManager.engineConnection?.removeEventListener(
 | 
			
		||||
        EngineConnectionEvents.ConnectionStateChanged,
 | 
			
		||||
        onConnectionStateChange as EventListener
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // Don't set long loading time if there's a more severe error
 | 
			
		||||
    if (error > ConnectionError.LongLoadingTime) return
 | 
			
		||||
 | 
			
		||||
    const timer = setTimeout(() => {
 | 
			
		||||
      setHasLongLoadTime(true)
 | 
			
		||||
      setError(ConnectionError.LongLoadingTime)
 | 
			
		||||
    }, 4000)
 | 
			
		||||
 | 
			
		||||
    return () => clearTimeout(timer)
 | 
			
		||||
  }, [setHasLongLoadTime])
 | 
			
		||||
  }, [error, setError])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className="body-bg flex flex-col items-center justify-center h-screen"
 | 
			
		||||
@ -29,10 +80,10 @@ const Loading = ({ children }: React.PropsWithChildren) => {
 | 
			
		||||
      <p
 | 
			
		||||
        className={
 | 
			
		||||
          'text-sm mt-4 text-primary/60 transition-opacity duration-500' +
 | 
			
		||||
          (hasLongLoadTime ? ' opacity-100' : ' opacity-0')
 | 
			
		||||
          (error !== ConnectionError.Unset ? ' opacity-100' : ' opacity-0')
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        Loading is taking longer than expected.
 | 
			
		||||
        {CONNECTION_ERROR_TEXT[error]}
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,6 @@ import { LanguageSupport } from '@codemirror/language'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
import { paths } from 'lib/paths'
 | 
			
		||||
import { FileEntry } from 'lib/types'
 | 
			
		||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
 | 
			
		||||
import Worker from 'editor/plugins/lsp/worker.ts?worker'
 | 
			
		||||
import {
 | 
			
		||||
  LspWorkerEventType,
 | 
			
		||||
@ -23,6 +22,8 @@ import {
 | 
			
		||||
} from 'editor/plugins/lsp/types'
 | 
			
		||||
import { wasmUrl } from 'lang/wasm'
 | 
			
		||||
import { PROJECT_ENTRYPOINT } from 'lib/constants'
 | 
			
		||||
import { useNetworkContext } from 'hooks/useNetworkContext'
 | 
			
		||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
 | 
			
		||||
 | 
			
		||||
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
 | 
			
		||||
  return []
 | 
			
		||||
@ -86,7 +87,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
 | 
			
		||||
  } = useSettingsAuthContext()
 | 
			
		||||
  const token = auth?.context.token
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const { overallState } = useNetworkStatus()
 | 
			
		||||
  const { overallState } = useNetworkContext()
 | 
			
		||||
  const isNetworkOkay = overallState === NetworkHealthState.Ok
 | 
			
		||||
 | 
			
		||||
  // So this is a bit weird, we need to initialize the lsp server and client.
 | 
			
		||||
 | 
			
		||||
@ -5,8 +5,8 @@ import { CommandBarProvider } from './CommandBar/CommandBarProvider'
 | 
			
		||||
import {
 | 
			
		||||
  NETWORK_HEALTH_TEXT,
 | 
			
		||||
  NetworkHealthIndicator,
 | 
			
		||||
  NetworkHealthState,
 | 
			
		||||
} from './NetworkHealthIndicator'
 | 
			
		||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
 | 
			
		||||
 | 
			
		||||
function TestWrap({ children }: { children: React.ReactNode }) {
 | 
			
		||||
  // wrap in router and xState context
 | 
			
		||||
@ -19,6 +19,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Our Playwright tests for this are much more comprehensive.
 | 
			
		||||
describe('NetworkHealthIndicator tests', () => {
 | 
			
		||||
  test('Renders the network indicator', () => {
 | 
			
		||||
    render(
 | 
			
		||||
@ -29,21 +30,7 @@ describe('NetworkHealthIndicator tests', () => {
 | 
			
		||||
 | 
			
		||||
    fireEvent.click(screen.getByTestId('network-toggle'))
 | 
			
		||||
 | 
			
		||||
    expect(screen.getByTestId('network')).toHaveTextContent(
 | 
			
		||||
      NETWORK_HEALTH_TEXT[NetworkHealthState.Ok]
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  test('Responds to network changes', () => {
 | 
			
		||||
    render(
 | 
			
		||||
      <TestWrap>
 | 
			
		||||
        <NetworkHealthIndicator />
 | 
			
		||||
      </TestWrap>
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    fireEvent.offline(window)
 | 
			
		||||
    fireEvent.click(screen.getByTestId('network-toggle'))
 | 
			
		||||
 | 
			
		||||
    // Starts as disconnected
 | 
			
		||||
    expect(screen.getByTestId('network')).toHaveTextContent(
 | 
			
		||||
      NETWORK_HEALTH_TEXT[NetworkHealthState.Disconnected]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@ -1,26 +1,13 @@
 | 
			
		||||
import { Popover } from '@headlessui/react'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
 | 
			
		||||
import {
 | 
			
		||||
  ConnectingType,
 | 
			
		||||
  ConnectingTypeGroup,
 | 
			
		||||
  DisconnectingType,
 | 
			
		||||
  EngineConnectionState,
 | 
			
		||||
  EngineConnectionStateType,
 | 
			
		||||
  ErrorType,
 | 
			
		||||
  initialConnectingTypeGroupState,
 | 
			
		||||
} from '../lang/std/engineConnection'
 | 
			
		||||
import { engineCommandManager } from '../lib/singletons'
 | 
			
		||||
import Tooltip from './Tooltip'
 | 
			
		||||
 | 
			
		||||
export enum NetworkHealthState {
 | 
			
		||||
  Ok,
 | 
			
		||||
  Issue,
 | 
			
		||||
  Disconnected,
 | 
			
		||||
}
 | 
			
		||||
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
 | 
			
		||||
import { useNetworkContext } from '../hooks/useNetworkContext'
 | 
			
		||||
import { NetworkHealthState } from '../hooks/useNetworkStatus'
 | 
			
		||||
 | 
			
		||||
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
 | 
			
		||||
  [NetworkHealthState.Ok]: 'Connected',
 | 
			
		||||
  [NetworkHealthState.Weak]: 'Weak',
 | 
			
		||||
  [NetworkHealthState.Issue]: 'Problem',
 | 
			
		||||
  [NetworkHealthState.Disconnected]: 'Offline',
 | 
			
		||||
}
 | 
			
		||||
@ -61,6 +48,10 @@ const overallConnectionStateColor: Record<NetworkHealthState, IconColorConfig> =
 | 
			
		||||
      icon: 'text-succeed-80 dark:text-succeed-10',
 | 
			
		||||
      bg: 'bg-succeed-10/30 dark:bg-succeed-80/50',
 | 
			
		||||
    },
 | 
			
		||||
    [NetworkHealthState.Weak]: {
 | 
			
		||||
      icon: 'text-warn-80 dark:text-warn-10',
 | 
			
		||||
      bg: 'bg-warn-10 dark:bg-warn-80/80',
 | 
			
		||||
    },
 | 
			
		||||
    [NetworkHealthState.Issue]: {
 | 
			
		||||
      icon: 'text-destroy-80 dark:text-destroy-10',
 | 
			
		||||
      bg: 'bg-destroy-10 dark:bg-destroy-80/80',
 | 
			
		||||
@ -76,125 +67,11 @@ const overallConnectionStateIcon: Record<
 | 
			
		||||
  ActionIconProps['icon']
 | 
			
		||||
> = {
 | 
			
		||||
  [NetworkHealthState.Ok]: 'network',
 | 
			
		||||
  [NetworkHealthState.Weak]: 'network',
 | 
			
		||||
  [NetworkHealthState.Issue]: 'networkCrossedOut',
 | 
			
		||||
  [NetworkHealthState.Disconnected]: 'networkCrossedOut',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useNetworkStatus() {
 | 
			
		||||
  const [steps, setSteps] = useState(initialConnectingTypeGroupState)
 | 
			
		||||
  const [internetConnected, setInternetConnected] = useState<boolean>(true)
 | 
			
		||||
  const [overallState, setOverallState] = useState<NetworkHealthState>(
 | 
			
		||||
    NetworkHealthState.Ok
 | 
			
		||||
  )
 | 
			
		||||
  const [hasCopied, setHasCopied] = useState<boolean>(false)
 | 
			
		||||
 | 
			
		||||
  const [error, setError] = useState<ErrorType | undefined>(undefined)
 | 
			
		||||
 | 
			
		||||
  const issues: Record<ConnectingTypeGroup, boolean> = {
 | 
			
		||||
    [ConnectingTypeGroup.WebSocket]: steps[ConnectingTypeGroup.WebSocket].some(
 | 
			
		||||
      (a: [ConnectingType, boolean | undefined]) => a[1] === false
 | 
			
		||||
    ),
 | 
			
		||||
    [ConnectingTypeGroup.ICE]: steps[ConnectingTypeGroup.ICE].some(
 | 
			
		||||
      (a: [ConnectingType, boolean | undefined]) => a[1] === false
 | 
			
		||||
    ),
 | 
			
		||||
    [ConnectingTypeGroup.WebRTC]: steps[ConnectingTypeGroup.WebRTC].some(
 | 
			
		||||
      (a: [ConnectingType, boolean | undefined]) => a[1] === false
 | 
			
		||||
    ),
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const hasIssues: boolean =
 | 
			
		||||
    issues[ConnectingTypeGroup.WebSocket] ||
 | 
			
		||||
    issues[ConnectingTypeGroup.ICE] ||
 | 
			
		||||
    issues[ConnectingTypeGroup.WebRTC]
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setOverallState(
 | 
			
		||||
      !internetConnected
 | 
			
		||||
        ? NetworkHealthState.Disconnected
 | 
			
		||||
        : hasIssues
 | 
			
		||||
        ? NetworkHealthState.Issue
 | 
			
		||||
        : NetworkHealthState.Ok
 | 
			
		||||
    )
 | 
			
		||||
  }, [hasIssues, internetConnected])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const onlineCallback = () => {
 | 
			
		||||
      setSteps(initialConnectingTypeGroupState)
 | 
			
		||||
      setInternetConnected(true)
 | 
			
		||||
    }
 | 
			
		||||
    const offlineCallback = () => {
 | 
			
		||||
      setInternetConnected(false)
 | 
			
		||||
    }
 | 
			
		||||
    window.addEventListener('online', onlineCallback)
 | 
			
		||||
    window.addEventListener('offline', offlineCallback)
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener('online', onlineCallback)
 | 
			
		||||
      window.removeEventListener('offline', offlineCallback)
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    engineCommandManager.onConnectionStateChange(
 | 
			
		||||
      (engineConnectionState: EngineConnectionState) => {
 | 
			
		||||
        let hasSetAStep = false
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
          engineConnectionState.type === EngineConnectionStateType.Connecting
 | 
			
		||||
        ) {
 | 
			
		||||
          const groups = Object.values(steps)
 | 
			
		||||
          for (let group of groups) {
 | 
			
		||||
            for (let step of group) {
 | 
			
		||||
              if (step[0] !== engineConnectionState.value.type) continue
 | 
			
		||||
              step[1] = true
 | 
			
		||||
              hasSetAStep = true
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
          engineConnectionState.type === EngineConnectionStateType.Disconnecting
 | 
			
		||||
        ) {
 | 
			
		||||
          const groups = Object.values(steps)
 | 
			
		||||
          for (let group of groups) {
 | 
			
		||||
            for (let step of group) {
 | 
			
		||||
              if (
 | 
			
		||||
                engineConnectionState.value.type === DisconnectingType.Error
 | 
			
		||||
              ) {
 | 
			
		||||
                if (
 | 
			
		||||
                  engineConnectionState.value.value.lastConnectingValue
 | 
			
		||||
                    ?.type === step[0]
 | 
			
		||||
                ) {
 | 
			
		||||
                  step[1] = false
 | 
			
		||||
                  hasSetAStep = true
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (engineConnectionState.value.type === DisconnectingType.Error) {
 | 
			
		||||
              setError(engineConnectionState.value.value)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (hasSetAStep) {
 | 
			
		||||
          setSteps(steps)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    hasIssues,
 | 
			
		||||
    overallState,
 | 
			
		||||
    internetConnected,
 | 
			
		||||
    steps,
 | 
			
		||||
    issues,
 | 
			
		||||
    error,
 | 
			
		||||
    setHasCopied,
 | 
			
		||||
    hasCopied,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const NetworkHealthIndicator = () => {
 | 
			
		||||
  const {
 | 
			
		||||
    hasIssues,
 | 
			
		||||
@ -205,7 +82,7 @@ export const NetworkHealthIndicator = () => {
 | 
			
		||||
    error,
 | 
			
		||||
    setHasCopied,
 | 
			
		||||
    hasCopied,
 | 
			
		||||
  } = useNetworkStatus()
 | 
			
		||||
  } = useNetworkContext()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Popover className="relative">
 | 
			
		||||
@ -259,18 +136,18 @@ export const NetworkHealthIndicator = () => {
 | 
			
		||||
                    size="lg"
 | 
			
		||||
                    icon={
 | 
			
		||||
                      hasIssueToIcon[
 | 
			
		||||
                        issues[name as ConnectingTypeGroup].toString()
 | 
			
		||||
                        String(issues[name as ConnectingTypeGroup])
 | 
			
		||||
                      ]
 | 
			
		||||
                    }
 | 
			
		||||
                    iconClassName={
 | 
			
		||||
                      hasIssueToIconColors[
 | 
			
		||||
                        issues[name as ConnectingTypeGroup].toString()
 | 
			
		||||
                        String(issues[name as ConnectingTypeGroup])
 | 
			
		||||
                      ].icon
 | 
			
		||||
                    }
 | 
			
		||||
                    bgClassName={
 | 
			
		||||
                      'rounded-sm ' +
 | 
			
		||||
                      hasIssueToIconColors[
 | 
			
		||||
                        issues[name as ConnectingTypeGroup].toString()
 | 
			
		||||
                        String(issues[name as ConnectingTypeGroup])
 | 
			
		||||
                      ].bg
 | 
			
		||||
                    }
 | 
			
		||||
                  />
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import { paths } from 'lib/paths'
 | 
			
		||||
import { isTauri } from '../lib/isTauri'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
import { Fragment } from 'react'
 | 
			
		||||
import { FileTree } from './FileTree'
 | 
			
		||||
import { sep } from '@tauri-apps/api/path'
 | 
			
		||||
import { Logo } from './Logo'
 | 
			
		||||
import { APP_NAME } from 'lib/constants'
 | 
			
		||||
 | 
			
		||||
@ -4,8 +4,9 @@ import { getNormalisedCoordinates } from '../lib/utils'
 | 
			
		||||
import Loading from './Loading'
 | 
			
		||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
 | 
			
		||||
import { useModelingContext } from 'hooks/useModelingContext'
 | 
			
		||||
import { useNetworkContext } from 'hooks/useNetworkContext'
 | 
			
		||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
 | 
			
		||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
 | 
			
		||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
 | 
			
		||||
import { butName } from 'lib/cameraControls'
 | 
			
		||||
import { sendSelectEventToEngine } from 'lib/selections'
 | 
			
		||||
 | 
			
		||||
@ -28,8 +29,11 @@ export const Stream = ({ className = '' }: { className?: string }) => {
 | 
			
		||||
  }))
 | 
			
		||||
  const { settings } = useSettingsAuthContext()
 | 
			
		||||
  const { state } = useModelingContext()
 | 
			
		||||
  const { overallState } = useNetworkStatus()
 | 
			
		||||
  const isNetworkOkay = overallState === NetworkHealthState.Ok
 | 
			
		||||
  const { overallState } = useNetworkContext()
 | 
			
		||||
 | 
			
		||||
  const isNetworkOkay =
 | 
			
		||||
    overallState === NetworkHealthState.Ok ||
 | 
			
		||||
    overallState === NetworkHealthState.Weak
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (
 | 
			
		||||
@ -43,6 +47,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
 | 
			
		||||
  }, [mediaStream])
 | 
			
		||||
 | 
			
		||||
  const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
 | 
			
		||||
    if (!isNetworkOkay) return
 | 
			
		||||
    if (!videoRef.current) return
 | 
			
		||||
    if (state.matches('Sketch')) return
 | 
			
		||||
    if (state.matches('Sketch no face')) return
 | 
			
		||||
@ -58,6 +63,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
 | 
			
		||||
    if (!isNetworkOkay) return
 | 
			
		||||
    if (!videoRef.current) return
 | 
			
		||||
    setButtonDownInStream(undefined)
 | 
			
		||||
    if (state.matches('Sketch')) return
 | 
			
		||||
@ -72,6 +78,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
 | 
			
		||||
    if (!isNetworkOkay) return
 | 
			
		||||
    if (state.matches('Sketch')) return
 | 
			
		||||
    if (state.matches('Sketch no face')) return
 | 
			
		||||
    if (!clickCoords) return
 | 
			
		||||
@ -112,7 +119,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
 | 
			
		||||
      {!isNetworkOkay && !isLoading && (
 | 
			
		||||
        <div className="text-center absolute inset-0">
 | 
			
		||||
          <Loading>
 | 
			
		||||
            <span data-testid="loading-stream">Stream disconnected</span>
 | 
			
		||||
            <span data-testid="loading-stream">Stream disconnected...</span>
 | 
			
		||||
          </Loading>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
@ -474,19 +474,13 @@ const completionRequester = (client: LanguageServerClient) => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
 | 
			
		||||
  let plugin: LanguageServerPlugin | null = null
 | 
			
		||||
 | 
			
		||||
  return [
 | 
			
		||||
    documentUri.of(options.documentUri),
 | 
			
		||||
    languageId.of('kcl'),
 | 
			
		||||
    workspaceFolders.of(options.workspaceFolders),
 | 
			
		||||
    ViewPlugin.define(
 | 
			
		||||
      (view) =>
 | 
			
		||||
        (plugin = new LanguageServerPlugin(
 | 
			
		||||
          options.client,
 | 
			
		||||
          view,
 | 
			
		||||
          options.allowHTMLContent
 | 
			
		||||
        ))
 | 
			
		||||
        new LanguageServerPlugin(options.client, view, options.allowHTMLContent)
 | 
			
		||||
    ),
 | 
			
		||||
    completionDecoration,
 | 
			
		||||
    Prec.highest(completionPlugin(options.client)),
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										25
									
								
								src/hooks/useNetworkContext.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,25 @@
 | 
			
		||||
import { createContext, useContext } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  ConnectingTypeGroup,
 | 
			
		||||
  initialConnectingTypeGroupState,
 | 
			
		||||
} from '../lang/std/engineConnection'
 | 
			
		||||
import { NetworkStatus, NetworkHealthState } from './useNetworkStatus'
 | 
			
		||||
 | 
			
		||||
export const NetworkContext = createContext<NetworkStatus>({
 | 
			
		||||
  hasIssues: undefined,
 | 
			
		||||
  overallState: NetworkHealthState.Disconnected,
 | 
			
		||||
  internetConnected: true,
 | 
			
		||||
  steps: structuredClone(initialConnectingTypeGroupState),
 | 
			
		||||
  issues: {
 | 
			
		||||
    [ConnectingTypeGroup.WebSocket]: undefined,
 | 
			
		||||
    [ConnectingTypeGroup.ICE]: undefined,
 | 
			
		||||
    [ConnectingTypeGroup.WebRTC]: undefined,
 | 
			
		||||
  },
 | 
			
		||||
  error: undefined,
 | 
			
		||||
  setHasCopied: (b: boolean) => {},
 | 
			
		||||
  hasCopied: false,
 | 
			
		||||
  pingPongHealth: undefined,
 | 
			
		||||
} as NetworkStatus)
 | 
			
		||||
export const useNetworkContext = () => {
 | 
			
		||||
  return useContext(NetworkContext)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										228
									
								
								src/hooks/useNetworkStatus.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,228 @@
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  ConnectingType,
 | 
			
		||||
  ConnectingTypeGroup,
 | 
			
		||||
  DisconnectingType,
 | 
			
		||||
  EngineCommandManagerEvents,
 | 
			
		||||
  EngineConnectionEvents,
 | 
			
		||||
  EngineConnectionStateType,
 | 
			
		||||
  ErrorType,
 | 
			
		||||
  initialConnectingTypeGroupState,
 | 
			
		||||
} from '../lang/std/engineConnection'
 | 
			
		||||
import { engineCommandManager } from '../lib/singletons'
 | 
			
		||||
 | 
			
		||||
export enum NetworkHealthState {
 | 
			
		||||
  Ok,
 | 
			
		||||
  Weak,
 | 
			
		||||
  Issue,
 | 
			
		||||
  Disconnected,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface NetworkStatus {
 | 
			
		||||
  hasIssues: boolean | undefined
 | 
			
		||||
  overallState: NetworkHealthState
 | 
			
		||||
  internetConnected: boolean
 | 
			
		||||
  steps: typeof initialConnectingTypeGroupState
 | 
			
		||||
  issues: Record<ConnectingTypeGroup, boolean | undefined>
 | 
			
		||||
  error: ErrorType | undefined
 | 
			
		||||
  setHasCopied: (b: boolean) => void
 | 
			
		||||
  hasCopied: boolean
 | 
			
		||||
  pingPongHealth: undefined | 'OK' | 'TIMEOUT'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Must be called from one place in the application.
 | 
			
		||||
// We've chosen the <Router /> component for this.
 | 
			
		||||
export function useNetworkStatus() {
 | 
			
		||||
  const [steps, setSteps] = useState(
 | 
			
		||||
    structuredClone(initialConnectingTypeGroupState)
 | 
			
		||||
  )
 | 
			
		||||
  const [internetConnected, setInternetConnected] = useState<boolean>(true)
 | 
			
		||||
  const [overallState, setOverallState] = useState<NetworkHealthState>(
 | 
			
		||||
    NetworkHealthState.Disconnected
 | 
			
		||||
  )
 | 
			
		||||
  const [pingPongHealth, setPingPongHealth] = useState<
 | 
			
		||||
    undefined | 'OK' | 'TIMEOUT'
 | 
			
		||||
  >(undefined)
 | 
			
		||||
  const [hasCopied, setHasCopied] = useState<boolean>(false)
 | 
			
		||||
 | 
			
		||||
  const [error, setError] = useState<ErrorType | undefined>(undefined)
 | 
			
		||||
 | 
			
		||||
  const hasIssue = (i: [ConnectingType, boolean | undefined]) =>
 | 
			
		||||
    i[1] === undefined ? i[1] : !i[1]
 | 
			
		||||
 | 
			
		||||
  const [issues, setIssues] = useState<
 | 
			
		||||
    Record<ConnectingTypeGroup, boolean | undefined>
 | 
			
		||||
  >({
 | 
			
		||||
    [ConnectingTypeGroup.WebSocket]: undefined,
 | 
			
		||||
    [ConnectingTypeGroup.ICE]: undefined,
 | 
			
		||||
    [ConnectingTypeGroup.WebRTC]: undefined,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const [hasIssues, setHasIssues] = useState<boolean | undefined>(undefined)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setOverallState(
 | 
			
		||||
      !internetConnected
 | 
			
		||||
        ? NetworkHealthState.Disconnected
 | 
			
		||||
        : hasIssues || hasIssues === undefined
 | 
			
		||||
        ? NetworkHealthState.Issue
 | 
			
		||||
        : pingPongHealth === 'TIMEOUT'
 | 
			
		||||
        ? NetworkHealthState.Weak
 | 
			
		||||
        : NetworkHealthState.Ok
 | 
			
		||||
    )
 | 
			
		||||
  }, [hasIssues, internetConnected, pingPongHealth])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const onlineCallback = () => {
 | 
			
		||||
      setInternetConnected(true)
 | 
			
		||||
    }
 | 
			
		||||
    const offlineCallback = () => {
 | 
			
		||||
      setInternetConnected(false)
 | 
			
		||||
      setSteps(structuredClone(initialConnectingTypeGroupState))
 | 
			
		||||
    }
 | 
			
		||||
    window.addEventListener('online', onlineCallback)
 | 
			
		||||
    window.addEventListener('offline', offlineCallback)
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener('online', onlineCallback)
 | 
			
		||||
      window.removeEventListener('offline', offlineCallback)
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const issues = {
 | 
			
		||||
      [ConnectingTypeGroup.WebSocket]: steps[
 | 
			
		||||
        ConnectingTypeGroup.WebSocket
 | 
			
		||||
      ].reduce(
 | 
			
		||||
        (acc: boolean | undefined, a) =>
 | 
			
		||||
          acc === true || acc === undefined ? acc : hasIssue(a),
 | 
			
		||||
        false
 | 
			
		||||
      ),
 | 
			
		||||
      [ConnectingTypeGroup.ICE]: steps[ConnectingTypeGroup.ICE].reduce(
 | 
			
		||||
        (acc: boolean | undefined, a) =>
 | 
			
		||||
          acc === true || acc === undefined ? acc : hasIssue(a),
 | 
			
		||||
        false
 | 
			
		||||
      ),
 | 
			
		||||
      [ConnectingTypeGroup.WebRTC]: steps[ConnectingTypeGroup.WebRTC].reduce(
 | 
			
		||||
        (acc: boolean | undefined, a) =>
 | 
			
		||||
          acc === true || acc === undefined ? acc : hasIssue(a),
 | 
			
		||||
        false
 | 
			
		||||
      ),
 | 
			
		||||
    }
 | 
			
		||||
    setIssues(issues)
 | 
			
		||||
  }, [steps])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setHasIssues(
 | 
			
		||||
      issues[ConnectingTypeGroup.WebSocket] ||
 | 
			
		||||
        issues[ConnectingTypeGroup.ICE] ||
 | 
			
		||||
        issues[ConnectingTypeGroup.WebRTC]
 | 
			
		||||
    )
 | 
			
		||||
  }, [issues])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const onPingPongChange = ({ detail: state }: CustomEvent) => {
 | 
			
		||||
      setPingPongHealth(state)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const onConnectionStateChange = ({
 | 
			
		||||
      detail: engineConnectionState,
 | 
			
		||||
    }: CustomEvent) => {
 | 
			
		||||
      setSteps((steps) => {
 | 
			
		||||
        let nextSteps = structuredClone(steps)
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
          engineConnectionState.type === EngineConnectionStateType.Connecting
 | 
			
		||||
        ) {
 | 
			
		||||
          const groups = Object.values(nextSteps)
 | 
			
		||||
          for (let group of groups) {
 | 
			
		||||
            for (let step of group) {
 | 
			
		||||
              if (step[0] !== engineConnectionState.value.type) continue
 | 
			
		||||
              step[1] = true
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
          engineConnectionState.type === EngineConnectionStateType.Disconnecting
 | 
			
		||||
        ) {
 | 
			
		||||
          const groups = Object.values(nextSteps)
 | 
			
		||||
          for (let group of groups) {
 | 
			
		||||
            for (let step of group) {
 | 
			
		||||
              if (
 | 
			
		||||
                engineConnectionState.value.type === DisconnectingType.Error
 | 
			
		||||
              ) {
 | 
			
		||||
                if (
 | 
			
		||||
                  engineConnectionState.value.value.lastConnectingValue
 | 
			
		||||
                    ?.type === step[0]
 | 
			
		||||
                ) {
 | 
			
		||||
                  step[1] = false
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (engineConnectionState.value.type === DisconnectingType.Error) {
 | 
			
		||||
              setError(engineConnectionState.value.value)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Reset the state of all steps if we have disconnected.
 | 
			
		||||
        if (
 | 
			
		||||
          engineConnectionState.type === EngineConnectionStateType.Disconnected
 | 
			
		||||
        ) {
 | 
			
		||||
          return structuredClone(initialConnectingTypeGroupState)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return nextSteps
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const onEngineAvailable = ({ detail: engineConnection }: CustomEvent) => {
 | 
			
		||||
      engineConnection.addEventListener(
 | 
			
		||||
        EngineConnectionEvents.PingPongChanged,
 | 
			
		||||
        onPingPongChange as EventListener
 | 
			
		||||
      )
 | 
			
		||||
      engineConnection.addEventListener(
 | 
			
		||||
        EngineConnectionEvents.ConnectionStateChanged,
 | 
			
		||||
        onConnectionStateChange as EventListener
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      // Tell EngineConnection to start firing events.
 | 
			
		||||
      window.dispatchEvent(new CustomEvent('use-network-status-ready', {}))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    engineCommandManager.addEventListener(
 | 
			
		||||
      EngineCommandManagerEvents.EngineAvailable,
 | 
			
		||||
      onEngineAvailable as EventListener
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      engineCommandManager.removeEventListener(
 | 
			
		||||
        EngineCommandManagerEvents.EngineAvailable,
 | 
			
		||||
        onEngineAvailable as EventListener
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      // When the component is unmounted these should be assigned, but it's possible
 | 
			
		||||
      // the component mounts and unmounts before engine is available.
 | 
			
		||||
      engineCommandManager.engineConnection?.addEventListener(
 | 
			
		||||
        EngineConnectionEvents.PingPongChanged,
 | 
			
		||||
        onPingPongChange as EventListener
 | 
			
		||||
      )
 | 
			
		||||
      engineCommandManager.engineConnection?.addEventListener(
 | 
			
		||||
        EngineConnectionEvents.ConnectionStateChanged,
 | 
			
		||||
        onConnectionStateChange as EventListener
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    hasIssues,
 | 
			
		||||
    overallState,
 | 
			
		||||
    internetConnected,
 | 
			
		||||
    steps,
 | 
			
		||||
    issues,
 | 
			
		||||
    error,
 | 
			
		||||
    setHasCopied,
 | 
			
		||||
    hasCopied,
 | 
			
		||||
    pingPongHealth,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -43,7 +43,7 @@ export function useSetupEngineManager(
 | 
			
		||||
    engineCommandManager.pool = settings.pool
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useLayoutEffect(() => {
 | 
			
		||||
  const startEngineInstance = () => {
 | 
			
		||||
    // Load the engine command manager once with the initial width and height,
 | 
			
		||||
    // then we do not want to reload it.
 | 
			
		||||
    const { width: quadWidth, height: quadHeight } = getDimensions(
 | 
			
		||||
@ -73,7 +73,12 @@ export function useSetupEngineManager(
 | 
			
		||||
      })
 | 
			
		||||
      hasSetNonZeroDimensions.current = true
 | 
			
		||||
    }
 | 
			
		||||
  }, [streamRef?.current?.offsetWidth, streamRef?.current?.offsetHeight])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useLayoutEffect(startEngineInstance, [
 | 
			
		||||
    streamRef?.current?.offsetWidth,
 | 
			
		||||
    streamRef?.current?.offsetHeight,
 | 
			
		||||
  ])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleResize = deferExecution(() => {
 | 
			
		||||
@ -96,8 +101,20 @@ export function useSetupEngineManager(
 | 
			
		||||
      }
 | 
			
		||||
    }, 500)
 | 
			
		||||
 | 
			
		||||
    const onOnline = () => {
 | 
			
		||||
      startEngineInstance()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const onOffline = () => {
 | 
			
		||||
      engineCommandManager.tearDown()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    window.addEventListener('online', onOnline)
 | 
			
		||||
    window.addEventListener('offline', onOffline)
 | 
			
		||||
    window.addEventListener('resize', handleResize)
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener('online', onOnline)
 | 
			
		||||
      window.removeEventListener('offline', onOffline)
 | 
			
		||||
      window.removeEventListener('resize', handleResize)
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
@ -7,12 +7,10 @@ import { authMachine } from 'machines/authMachine'
 | 
			
		||||
import { settingsMachine } from 'machines/settingsMachine'
 | 
			
		||||
import { homeMachine } from 'machines/homeMachine'
 | 
			
		||||
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
 | 
			
		||||
import {
 | 
			
		||||
  NetworkHealthState,
 | 
			
		||||
  useNetworkStatus,
 | 
			
		||||
} from 'components/NetworkHealthIndicator'
 | 
			
		||||
import { useKclContext } from 'lang/KclProvider'
 | 
			
		||||
import { useStore } from 'useStore'
 | 
			
		||||
import { useNetworkContext } from 'hooks/useNetworkContext'
 | 
			
		||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
 | 
			
		||||
 | 
			
		||||
// This might not be necessary, AnyStateMachine from xstate is working
 | 
			
		||||
export type AllMachines =
 | 
			
		||||
@ -47,7 +45,7 @@ export default function useStateMachineCommands<
 | 
			
		||||
  onCancel,
 | 
			
		||||
}: UseStateMachineCommandsArgs<T, S>) {
 | 
			
		||||
  const { commandBarSend } = useCommandsContext()
 | 
			
		||||
  const { overallState } = useNetworkStatus()
 | 
			
		||||
  const { overallState } = useNetworkContext()
 | 
			
		||||
  const { isExecuting } = useKclContext()
 | 
			
		||||
  const { isStreamReady } = useStore((s) => ({
 | 
			
		||||
    isStreamReady: s.isStreamReady,
 | 
			
		||||
@ -55,7 +53,10 @@ export default function useStateMachineCommands<
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const disableAllButtons =
 | 
			
		||||
      overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
 | 
			
		||||
      (overallState !== NetworkHealthState.Ok &&
 | 
			
		||||
        overallState !== NetworkHealthState.Weak) ||
 | 
			
		||||
      isExecuting ||
 | 
			
		||||
      !isStreamReady
 | 
			
		||||
    const newCommands = state.nextEvents
 | 
			
		||||
      .filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
 | 
			
		||||
      .filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
 | 
			
		||||
 | 
			
		||||
@ -318,7 +318,6 @@ function resetAndSetEngineEntitySelectionCmds(
 | 
			
		||||
  selections: SelectionToEngine[]
 | 
			
		||||
): Models['WebSocketRequest_type'][] {
 | 
			
		||||
  if (!engineCommandManager.engineConnection?.isReady()) {
 | 
			
		||||
    console.log('engine connection is not ready')
 | 
			
		||||
    return []
 | 
			
		||||
  }
 | 
			
		||||
  return [
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										39
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						@ -1880,10 +1880,10 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
 | 
			
		||||
  integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
 | 
			
		||||
 | 
			
		||||
"@kittycad/lib@^0.0.63":
 | 
			
		||||
  version "0.0.63"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.63.tgz#cc70cf1c0780543bbca6f55aae40d0904cfd45d7"
 | 
			
		||||
  integrity sha512-fDpGnycumT1xI/tSubRZzU9809/7s+m06w2EuJzxowgFrdIlvThnIHVf3EYvSujdFb0bHR/LZjodAw2ocXkXZw==
 | 
			
		||||
"@kittycad/lib@^0.0.64":
 | 
			
		||||
  version "0.0.64"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.64.tgz#0cea0788cd8af4a8964ddbf7152028affadcb17f"
 | 
			
		||||
  integrity sha512-qHyvNYKbhsfR5aXLFrdKrBQ4JI+0G0v096oROD3HatJ+AIzg5H0THmI+rMnQ9L4zx4U6n1A9gLi7ZQjSsZsleg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    node-fetch "3.3.2"
 | 
			
		||||
    openapi-types "^12.0.0"
 | 
			
		||||
@ -8234,16 +8234,7 @@ string-natural-compare@^3.0.1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
 | 
			
		||||
  integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
 | 
			
		||||
 | 
			
		||||
"string-width-cjs@npm:string-width@^4.2.0":
 | 
			
		||||
  version "4.2.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
 | 
			
		||||
  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    emoji-regex "^8.0.0"
 | 
			
		||||
    is-fullwidth-code-point "^3.0.0"
 | 
			
		||||
    strip-ansi "^6.0.1"
 | 
			
		||||
 | 
			
		||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
 | 
			
		||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
 | 
			
		||||
  version "4.2.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
 | 
			
		||||
  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
 | 
			
		||||
@ -8316,14 +8307,7 @@ string_decoder@~1.1.1:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    safe-buffer "~5.1.0"
 | 
			
		||||
 | 
			
		||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
 | 
			
		||||
  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    ansi-regex "^5.0.1"
 | 
			
		||||
 | 
			
		||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
 | 
			
		||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
 | 
			
		||||
  version "6.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
 | 
			
		||||
  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
 | 
			
		||||
@ -9305,7 +9289,7 @@ workerpool@6.2.1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
 | 
			
		||||
  integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
 | 
			
		||||
 | 
			
		||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
 | 
			
		||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
 | 
			
		||||
  version "7.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
 | 
			
		||||
  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
 | 
			
		||||
@ -9323,15 +9307,6 @@ wrap-ansi@^6.2.0:
 | 
			
		||||
    string-width "^4.1.0"
 | 
			
		||||
    strip-ansi "^6.0.0"
 | 
			
		||||
 | 
			
		||||
wrap-ansi@^7.0.0:
 | 
			
		||||
  version "7.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
 | 
			
		||||
  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    ansi-styles "^4.0.0"
 | 
			
		||||
    string-width "^4.1.0"
 | 
			
		||||
    strip-ansi "^6.0.0"
 | 
			
		||||
 | 
			
		||||
wrap-ansi@^8.1.0:
 | 
			
		||||
  version "8.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
 | 
			
		||||
 | 
			
		||||