Compare commits
37 Commits
jess/chang
...
kurt-conne
Author | SHA1 | Date | |
---|---|---|---|
b090f9959b | |||
37f0f7b568 | |||
08c1745be0 | |||
a157dfe6d7 | |||
80b6688d04 | |||
b7b87a38c4 | |||
ab56a165a4 | |||
9bcc307f32 | |||
5a3c6a3858 | |||
280f08945c | |||
e9fb3f7256 | |||
56f0c43e4e | |||
4f9e8cbe15 | |||
0f21ca90c8 | |||
c0bed02d72 | |||
3ab33e3810 | |||
27ba89f867 | |||
c52e4dcbe6 | |||
791c0487ae | |||
bd9c02fde9 | |||
cda70687f4 | |||
a7d785ab88 | |||
16a64b55db | |||
343ed04f7d | |||
66ddce1348 | |||
fade3b3995 | |||
8e7465a823 | |||
05521d3ef3 | |||
37df4c6fc9 | |||
db08e67215 | |||
32f2411394 | |||
cc0377bb00 | |||
ff08cef9f8 | |||
39a6d265f2 | |||
545e89f7aa | |||
d6ae23d881 | |||
25b599d3e7 |
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
|
### 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
|
```bash
|
||||||
touch ./e2e/playwright/playwright-secrets.env
|
docker run --network host --rm --init -it playwright/chrome:playwright-1.43.1
|
||||||
printf 'token="your-token"\nsnapshottoken="your-snapshot-token"' > ./e2e/playwright/playwright-secrets.env
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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 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('...`
|
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)
|
(note if you commit this, the tests will instantly fail without running any of the tests)
|
||||||
|
|
||||||
|
@ -27,6 +27,8 @@ document.addEventListener('mousemove', (e) =>
|
|||||||
)
|
)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const deg = (Math.PI * 2) / 360
|
||||||
|
|
||||||
const commonPoints = {
|
const commonPoints = {
|
||||||
startAt: '[9.06, -12.22]',
|
startAt: '[9.06, -12.22]',
|
||||||
num1: 9.14,
|
num1: 9.14,
|
||||||
@ -656,6 +658,7 @@ test('re-executes', async ({ page }) => {
|
|||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Can create sketches on all planes and their back sides', () => {
|
||||||
const sketchOnPlaneAndBackSideTest = async (
|
const sketchOnPlaneAndBackSideTest = async (
|
||||||
page: any,
|
page: any,
|
||||||
plane: string,
|
plane: string,
|
||||||
@ -722,8 +725,6 @@ const sketchOnPlaneAndBackSideTest = async (
|
|||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
await u.removeCurrentCode()
|
await u.removeCurrentCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe('Can create sketches on all planes and their back sides', () => {
|
|
||||||
test('XY', async ({ page }) => {
|
test('XY', async ({ page }) => {
|
||||||
await sketchOnPlaneAndBackSideTest(
|
await sketchOnPlaneAndBackSideTest(
|
||||||
page,
|
page,
|
||||||
@ -1484,12 +1485,15 @@ const part001 = startSketchOn('XZ')
|
|||||||
test('Can add multiple sketches', async ({ page }) => {
|
test('Can add multiple sketches', async ({ page }) => {
|
||||||
test.skip(process.platform === 'darwin', 'Can add multiple sketches')
|
test.skip(process.platform === 'darwin', 'Can add multiple sketches')
|
||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
await page.setViewportSize({ width: 1200, height: 500 })
|
const viewportSize = { width: 1200, height: 500 }
|
||||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
await page.setViewportSize(viewportSize)
|
||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
await u.waitForAuthSkipAppStart()
|
||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
|
|
||||||
|
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
|
||||||
|
u.click00rSetCenter(center.x, center.y)
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled()
|
).not.toBeDisabled()
|
||||||
@ -1502,127 +1506,71 @@ test('Can add multiple sketches', async ({ page }) => {
|
|||||||
200
|
200
|
||||||
)
|
)
|
||||||
|
|
||||||
// select a plane
|
let codeStr = "const part001 = startSketchOn('XY')"
|
||||||
await page.mouse.click(700, 200)
|
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(
|
|
||||||
`const part001 = startSketchOn('XZ')`
|
|
||||||
)
|
|
||||||
|
|
||||||
|
await page.mouse.click(center.x, viewportSize.height * 0.55)
|
||||||
|
await u.expectCodeToBe(codeStr)
|
||||||
|
await u.closeDebugPanel()
|
||||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
|
|
||||||
const startXPx = 600
|
await u.click00r(0, 0)
|
||||||
await u.closeDebugPanel()
|
codeStr += ` |> startProfileAt(${u.toSU([0, 0])}, %)`
|
||||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
await u.expectCodeToBe(codeStr)
|
||||||
await expect(page.locator('.cm-content'))
|
|
||||||
.toHaveText(`const part001 = startSketchOn('XZ')
|
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
|
||||||
await page.waitForTimeout(100)
|
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
await u.click00r(50, 0)
|
||||||
await page.waitForTimeout(100)
|
codeStr += ` |> line(${u.toSU([50, 0])}, %)`
|
||||||
|
await u.expectCodeToBe(codeStr)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content'))
|
await u.click00r(0, 50)
|
||||||
.toHaveText(`const part001 = startSketchOn('XZ')
|
codeStr += ` |> line(${u.toSU([0, 50])}, %)`
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
await u.expectCodeToBe(codeStr)
|
||||||
|> line([${commonPoints.num1}, 0], %)`)
|
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await u.click00r(-50, 0)
|
||||||
await expect(page.locator('.cm-content'))
|
codeStr += ` |> line(${u.toSU([-50, 0])}, %)`
|
||||||
.toHaveText(`const part001 = startSketchOn('XZ')
|
await u.expectCodeToBe(codeStr)
|
||||||
|> 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
|
|
||||||
|
|
||||||
|
// exit the sketch, reset relative clicker
|
||||||
|
await u.click00r(undefined, undefined)
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
|
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
|
||||||
await u.updateCamPosition([100, 100, 100])
|
|
||||||
await page.waitForTimeout(250)
|
await page.waitForTimeout(250)
|
||||||
|
await u.clearCommandLogs()
|
||||||
|
|
||||||
// start a new sketch
|
// start a new sketch
|
||||||
await u.clearCommandLogs()
|
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
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 page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||||
await u.clearAndCloseDebugPanel()
|
codeStr += "const part002 = startSketchOn('XY')"
|
||||||
|
await u.expectCodeToBe(codeStr)
|
||||||
// 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)
|
|
||||||
|
|
||||||
await u.closeDebugPanel()
|
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 u.click00r(30, 0)
|
||||||
await expect(
|
codeStr += ` |> startProfileAt(${u.toSU([30, 0])}, %)`
|
||||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
await u.expectCodeToBe(codeStr)
|
||||||
).toBe(
|
|
||||||
`${finalCodeFirstSketch}
|
|
||||||
const part002 = startSketchOn('${plane}')
|
|
||||||
|> startProfileAt(${startAt2}, %)
|
|
||||||
|> line([${num2}, 0], %)`.replace(/\s/g, '')
|
|
||||||
)
|
|
||||||
|
|
||||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
await u.click00r(30, 0)
|
||||||
await expect(
|
codeStr += ` |> line(${u.toSU([30 - 0.1 /* imprecision */, 0])}, %)`
|
||||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
await u.expectCodeToBe(codeStr)
|
||||||
).toBe(
|
|
||||||
`${finalCodeFirstSketch}
|
await u.click00r(0, 30)
|
||||||
const part002 = startSketchOn('${plane}')
|
codeStr += ` |> line(${u.toSU([0, 30])}, %)`
|
||||||
|> startProfileAt(${startAt2}, %)
|
await u.expectCodeToBe(codeStr)
|
||||||
|> line([${num2}, 0], %)
|
|
||||||
|> line([0, ${roundOff(
|
await u.click00r(-30, 0)
|
||||||
num2 + (process.platform === 'darwin' ? 0.01 : -0.01)
|
codeStr += ` |> line(${u.toSU([-30 + 0.1, 0])}, %)`
|
||||||
)}], %)`.replace(/\s/g, '')
|
await u.expectCodeToBe(codeStr)
|
||||||
)
|
|
||||||
await page.waitForTimeout(100)
|
await u.click00r(undefined, undefined)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await u.openAndClearDebugPanel()
|
||||||
await expect(
|
await page.getByRole('button', { name: 'Exit Sketch' }).click()
|
||||||
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
).toBe(
|
await u.updateCamPosition([100, 100, 100])
|
||||||
`${finalCodeFirstSketch}
|
await page.waitForTimeout(250)
|
||||||
const part002 = startSketchOn('${plane}')
|
await u.clearCommandLogs()
|
||||||
|> 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,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('ProgramMemory can be serialised', async ({ page }) => {
|
test('ProgramMemory can be serialised', async ({ page }) => {
|
||||||
@ -2105,6 +2053,7 @@ test('Can edit segments by dragging their handles', async ({ page }) => {
|
|||||||
|> tangentialArcTo([26.92, -3.32], %)`)
|
|> tangentialArcTo([26.92, -3.32], %)`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Snap to close works (at any scale)', () => {
|
||||||
const doSnapAtDifferentScales = async (
|
const doSnapAtDifferentScales = async (
|
||||||
page: any,
|
page: any,
|
||||||
camPos: [number, number, number],
|
camPos: [number, number, number],
|
||||||
@ -2127,7 +2076,9 @@ const doSnapAtDifferentScales = async (
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled()
|
).not.toBeDisabled()
|
||||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).toBeVisible()
|
||||||
|
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||||
@ -2175,10 +2126,9 @@ const doSnapAtDifferentScales = async (
|
|||||||
|
|
||||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||||
// Assert the tool was unequipped
|
// Assert the tool was unequipped
|
||||||
await expect(page.getByRole('button', { name: 'Line' })).not.toHaveAttribute(
|
await expect(
|
||||||
'aria-pressed',
|
page.getByRole('button', { name: 'Line' })
|
||||||
'true'
|
).not.toHaveAttribute('aria-pressed', 'true')
|
||||||
)
|
|
||||||
|
|
||||||
// exit sketch
|
// exit sketch
|
||||||
await u.openAndClearDebugPanel()
|
await u.openAndClearDebugPanel()
|
||||||
@ -2186,8 +2136,6 @@ const doSnapAtDifferentScales = async (
|
|||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.removeCurrentCode()
|
await u.removeCurrentCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe('Snap to close works (at any scale)', () => {
|
|
||||||
test('[0, 100, 100]', async ({ page }) => {
|
test('[0, 100, 100]', async ({ page }) => {
|
||||||
await doSnapAtDifferentScales(page, [0, 100, 100], 0.01, 0.01)
|
await doSnapAtDifferentScales(page, [0, 100, 100], 0.01, 0.01)
|
||||||
})
|
})
|
||||||
@ -2979,7 +2927,7 @@ test.describe('Testing segment overlays', () => {
|
|||||||
* @param {number} options.steps - The number of steps to perform
|
* @param {number} options.steps - The number of steps to perform
|
||||||
*/
|
*/
|
||||||
const _clickConstrained =
|
const _clickConstrained =
|
||||||
(page: Page) =>
|
(page: Page, u: any) =>
|
||||||
async ({
|
async ({
|
||||||
hoverPos,
|
hoverPos,
|
||||||
constraintType,
|
constraintType,
|
||||||
@ -2987,7 +2935,6 @@ test.describe('Testing segment overlays', () => {
|
|||||||
expectAfterUnconstrained,
|
expectAfterUnconstrained,
|
||||||
expectFinal,
|
expectFinal,
|
||||||
ang = 45,
|
ang = 45,
|
||||||
steps = 6,
|
|
||||||
}: {
|
}: {
|
||||||
hoverPos: { x: number; y: number }
|
hoverPos: { x: number; y: number }
|
||||||
constraintType:
|
constraintType:
|
||||||
@ -3002,13 +2949,16 @@ test.describe('Testing segment overlays', () => {
|
|||||||
steps?: number
|
steps?: number
|
||||||
}) => {
|
}) => {
|
||||||
await expect(page.getByText('Added variable')).not.toBeVisible()
|
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(0, 0)
|
||||||
await page.mouse.move(hoverPos.x, hoverPos.y, { steps })
|
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 u.wiggleMove(x, y, 20, 30, ang, 10, 5)
|
||||||
|
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
await expect(page.locator('.cm-content')).toContainText(
|
||||||
expectBeforeUnconstrained
|
expectBeforeUnconstrained
|
||||||
)
|
)
|
||||||
@ -3024,6 +2974,14 @@ test.describe('Testing segment overlays', () => {
|
|||||||
await expect(page.locator('.cm-content')).toContainText(
|
await expect(page.locator('.cm-content')).toContainText(
|
||||||
expectAfterUnconstrained
|
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 u.wiggleMove(x, y, 20, 30, ang, 10, 5)
|
||||||
|
|
||||||
const unconstrainedLocator = page.locator(
|
const unconstrainedLocator = page.locator(
|
||||||
`[data-constraint-type="${constraintType}"][data-is-constrained="false"]`
|
`[data-constraint-type="${constraintType}"][data-is-constrained="false"]`
|
||||||
)
|
)
|
||||||
@ -3047,7 +3005,7 @@ test.describe('Testing segment overlays', () => {
|
|||||||
* @param {number} options.steps - The number of steps to perform
|
* @param {number} options.steps - The number of steps to perform
|
||||||
*/
|
*/
|
||||||
const _clickUnconstrained =
|
const _clickUnconstrained =
|
||||||
(page: Page) =>
|
(page: Page, u: any) =>
|
||||||
async ({
|
async ({
|
||||||
hoverPos,
|
hoverPos,
|
||||||
constraintType,
|
constraintType,
|
||||||
@ -3055,7 +3013,6 @@ test.describe('Testing segment overlays', () => {
|
|||||||
expectAfterUnconstrained,
|
expectAfterUnconstrained,
|
||||||
expectFinal,
|
expectFinal,
|
||||||
ang = 45,
|
ang = 45,
|
||||||
steps = 5,
|
|
||||||
}: {
|
}: {
|
||||||
hoverPos: { x: number; y: number }
|
hoverPos: { x: number; y: number }
|
||||||
constraintType:
|
constraintType:
|
||||||
@ -3069,14 +3026,16 @@ test.describe('Testing segment overlays', () => {
|
|||||||
ang?: number
|
ang?: number
|
||||||
steps?: number
|
steps?: number
|
||||||
}) => {
|
}) => {
|
||||||
const [x, y] = [
|
await page.mouse.move(0, 0)
|
||||||
Math.cos((ang * Math.PI) / 180) * 45,
|
await page.waitForTimeout(1000)
|
||||||
Math.sin((ang * Math.PI) / 180) * 45,
|
let x = 0,
|
||||||
]
|
y = 0
|
||||||
await page.mouse.move(hoverPos.x + x, hoverPos.y + y)
|
x = hoverPos.x + Math.cos(ang * deg) * 32
|
||||||
|
y = hoverPos.y - Math.sin(ang * deg) * 32
|
||||||
|
await page.mouse.move(x, y)
|
||||||
|
await u.wiggleMove(x, y, 20, 30, ang, 10, 5)
|
||||||
|
|
||||||
await expect(page.getByText('Added variable')).not.toBeVisible()
|
await expect(page.getByText('Added variable')).not.toBeVisible()
|
||||||
await page.mouse.move(hoverPos.x, hoverPos.y, { steps })
|
|
||||||
await expect(page.locator('.cm-content')).toContainText(
|
await expect(page.locator('.cm-content')).toContainText(
|
||||||
expectBeforeUnconstrained
|
expectBeforeUnconstrained
|
||||||
)
|
)
|
||||||
@ -3094,7 +3053,14 @@ test.describe('Testing segment overlays', () => {
|
|||||||
expectAfterUnconstrained
|
expectAfterUnconstrained
|
||||||
)
|
)
|
||||||
await expect(page.getByText('Added variable')).not.toBeVisible()
|
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 u.wiggleMove(x, y, 20, 30, ang, 10, 5)
|
||||||
|
|
||||||
const constrainedLocator = page.locator(
|
const constrainedLocator = page.locator(
|
||||||
`[data-constraint-type="${constraintType}"][data-is-constrained="true"]`
|
`[data-constraint-type="${constraintType}"][data-is-constrained="true"]`
|
||||||
)
|
)
|
||||||
@ -3109,28 +3075,29 @@ test.describe('Testing segment overlays', () => {
|
|||||||
test('for segments [line, angledLine, lineTo, xLineTo]', async ({
|
test('for segments [line, angledLine, lineTo, xLineTo]', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
test.setTimeout(120000)
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
`const part001 = startSketchOn('XZ')
|
`const part001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([0, 0], %)
|
|> startProfileAt([5 + 0, 20 + 0], %)
|
||||||
|> line([0.5, -14 + 0], %)
|
|> line([0.5, -14 + 0], %)
|
||||||
|> angledLine({ angle: 3 + 0, length: 32 + 0 }, %)
|
|> angledLine({ angle: 3 + 0, length: 32 + 0 }, %)
|
||||||
|> lineTo([33, 11.5 + 0], %)
|
|> lineTo([5 + 33, 20 + 11.5 + 0], %)
|
||||||
|> xLineTo(9 - 5, %)
|
|> xLineTo(5 + 9 - 5, %)
|
||||||
|> yLineTo(-10.77, %, 'a')
|
|> yLineTo(20 + -10.77, %, 'a')
|
||||||
|> xLine(26.04, %)
|
|> xLine(26.04, %)
|
||||||
|> yLine(21.14 + 0, %)
|
|> yLine(21.14 + 0, %)
|
||||||
|> angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %)
|
|> angledLineOfXLength({ angle: 181 + 0, length: 23.14 }, %)
|
||||||
|> angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)
|
|> angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)
|
||||||
|> angledLineToX({ angle: 3 + 0, to: 26 }, %)
|
|> angledLineToX({ angle: 3 + 0, to: 5 + 26 }, %)
|
||||||
|> angledLineToY({ angle: 89, to: 9.14 + 0 }, %)
|
|> angledLineToY({ angle: 89, to: 20 + 9.14 + 0 }, %)
|
||||||
|> angledLineThatIntersects({
|
|> angledLineThatIntersects({
|
||||||
angle: 4.14,
|
angle: 4.14,
|
||||||
intersectTag: 'a',
|
intersectTag: 'a',
|
||||||
offset: 9
|
offset: 9
|
||||||
}, %)
|
}, %)
|
||||||
|> tangentialArcTo([3.14 + 13, 3.14], %)
|
|> tangentialArcTo([5 + 3.14 + 13, 20 + 3.14], %)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -3164,56 +3131,75 @@ test.describe('Testing segment overlays', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
await page.waitForTimeout(100)
|
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.waitForTimeout(100)
|
||||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
||||||
|
|
||||||
const clickUnconstrained = _clickUnconstrained(page)
|
const clickUnconstrained = _clickUnconstrained(page, u)
|
||||||
const clickConstrained = _clickConstrained(page)
|
const clickConstrained = _clickConstrained(page, u)
|
||||||
|
|
||||||
|
// 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' })
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
let ang = 0
|
||||||
|
|
||||||
const line = await u.getBoundingBox(`[data-overlay-index="${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({
|
await clickConstrained({
|
||||||
hoverPos: { x: line.x, y: line.y - 10 },
|
hoverPos: { x: line.x, y: line.y },
|
||||||
constraintType: 'yRelative',
|
constraintType: 'yRelative',
|
||||||
expectBeforeUnconstrained: '|> line([0.5, -14 + 0], %)',
|
expectBeforeUnconstrained: '|> line([0.5, -14 + 0], %)',
|
||||||
expectAfterUnconstrained: '|> line([0.5, -14], %)',
|
expectAfterUnconstrained: '|> line([0.5, -14], %)',
|
||||||
expectFinal: '|> line([0.5, yRel001], %)',
|
expectFinal: '|> line([0.5, yRel001], %)',
|
||||||
ang: 135,
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
console.log('line2')
|
console.log('line2')
|
||||||
await clickUnconstrained({
|
await clickUnconstrained({
|
||||||
hoverPos: { x: line.x, y: line.y - 10 },
|
hoverPos: { x: line.x, y: line.y },
|
||||||
constraintType: 'xRelative',
|
constraintType: 'xRelative',
|
||||||
expectBeforeUnconstrained: '|> line([0.5, yRel001], %)',
|
expectBeforeUnconstrained: '|> line([0.5, yRel001], %)',
|
||||||
expectAfterUnconstrained: 'line([xRel001, yRel001], %)',
|
expectAfterUnconstrained: 'line([xRel001, yRel001], %)',
|
||||||
expectFinal: '|> line([0.5, yRel001], %)',
|
expectFinal: '|> line([0.5, yRel001], %)',
|
||||||
ang: -45,
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
|
|
||||||
const angledLine = await u.getBoundingBox(`[data-overlay-index="1"]`)
|
const angledLine = await u.getBoundingBox(`[data-overlay-index="1"]`)
|
||||||
|
ang = await u.getAngle(`[data-overlay-index="1"]`)
|
||||||
console.log('angledLine1')
|
console.log('angledLine1')
|
||||||
await clickConstrained({
|
await clickConstrained({
|
||||||
hoverPos: { x: angledLine.x - 10, y: angledLine.y },
|
hoverPos: { x: angledLine.x, y: angledLine.y },
|
||||||
constraintType: 'angle',
|
constraintType: 'angle',
|
||||||
expectBeforeUnconstrained:
|
expectBeforeUnconstrained:
|
||||||
'angledLine({ angle: 3 + 0, length: 32 + 0 }, %)',
|
'angledLine({ angle: 3 + 0, length: 32 + 0 }, %)',
|
||||||
expectAfterUnconstrained: 'angledLine({ angle: 3, length: 32 + 0 }, %)',
|
expectAfterUnconstrained: 'angledLine({ angle: 3, length: 32 + 0 }, %)',
|
||||||
expectFinal: 'angledLine({ angle: angle001, length: 32 + 0 }, %)',
|
expectFinal: 'angledLine({ angle: angle001, length: 32 + 0 }, %)',
|
||||||
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
console.log('angledLine2')
|
console.log('angledLine2')
|
||||||
await clickConstrained({
|
await clickConstrained({
|
||||||
hoverPos: { x: angledLine.x - 10, y: angledLine.y },
|
hoverPos: { x: angledLine.x, y: angledLine.y },
|
||||||
constraintType: 'length',
|
constraintType: 'length',
|
||||||
expectBeforeUnconstrained:
|
expectBeforeUnconstrained:
|
||||||
'angledLine({ angle: angle001, length: 32 + 0 }, %)',
|
'angledLine({ angle: angle001, length: 32 + 0 }, %)',
|
||||||
expectAfterUnconstrained:
|
expectAfterUnconstrained:
|
||||||
'angledLine({ angle: angle001, length: 32 }, %)',
|
'angledLine({ angle: angle001, length: 32 }, %)',
|
||||||
expectFinal: 'angledLine({ angle: angle001, length: len001 }, %)',
|
expectFinal: 'angledLine({ angle: angle001, length: len001 }, %)',
|
||||||
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
|
|
||||||
await page.mouse.move(700, 250)
|
await page.mouse.move(700, 250)
|
||||||
@ -3223,36 +3209,39 @@ test.describe('Testing segment overlays', () => {
|
|||||||
}
|
}
|
||||||
await page.waitForTimeout(200)
|
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')
|
console.log('lineTo1')
|
||||||
await clickConstrained({
|
await clickConstrained({
|
||||||
hoverPos: { x: lineTo.x, y: lineTo.y + 21 },
|
hoverPos: { x: lineTo.x, y: lineTo.y },
|
||||||
constraintType: 'yAbsolute',
|
constraintType: 'yAbsolute',
|
||||||
expectBeforeUnconstrained: 'lineTo([33, 11.5 + 0], %)',
|
expectBeforeUnconstrained: 'lineTo([5 + 33, 20 + 11.5 + 0], %)',
|
||||||
expectAfterUnconstrained: 'lineTo([33, 11.5], %)',
|
expectAfterUnconstrained: 'lineTo([5 + 33, 31.5], %)',
|
||||||
expectFinal: 'lineTo([33, yAbs001], %)',
|
expectFinal: 'lineTo([5 + 33, yAbs001], %)',
|
||||||
steps: 8,
|
steps: 8,
|
||||||
ang: 55,
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
console.log('lineTo2')
|
console.log('lineTo2')
|
||||||
await clickUnconstrained({
|
await clickConstrained({
|
||||||
hoverPos: { x: lineTo.x, y: lineTo.y + 25 },
|
hoverPos: { x: lineTo.x, y: lineTo.y },
|
||||||
constraintType: 'xAbsolute',
|
constraintType: 'xAbsolute',
|
||||||
expectBeforeUnconstrained: 'lineTo([33, yAbs001], %)',
|
expectBeforeUnconstrained: 'lineTo([5 + 33, yAbs001], %)',
|
||||||
expectAfterUnconstrained: 'lineTo([xAbs001, yAbs001], %)',
|
expectAfterUnconstrained: 'lineTo([38, yAbs001], %)',
|
||||||
expectFinal: 'lineTo([33, yAbs001], %)',
|
expectFinal: 'lineTo([xAbs001, yAbs001], %)',
|
||||||
steps: 8,
|
steps: 8,
|
||||||
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
|
|
||||||
const xLineTo = await u.getBoundingBox(`[data-overlay-index="3"]`)
|
const xLineTo = await u.getBoundingBox(`[data-overlay-index="3"]`)
|
||||||
|
ang = await u.getAngle(`[data-overlay-index="3"]`)
|
||||||
console.log('xlineTo1')
|
console.log('xlineTo1')
|
||||||
await clickConstrained({
|
await clickConstrained({
|
||||||
hoverPos: { x: xLineTo.x + 15, y: xLineTo.y },
|
hoverPos: { x: xLineTo.x, y: xLineTo.y },
|
||||||
constraintType: 'xAbsolute',
|
constraintType: 'xAbsolute',
|
||||||
expectBeforeUnconstrained: 'xLineTo(9 - 5, %)',
|
expectBeforeUnconstrained: 'xLineTo(5 + 9 - 5, %)',
|
||||||
expectAfterUnconstrained: 'xLineTo(4, %)',
|
expectAfterUnconstrained: 'xLineTo(9, %)',
|
||||||
expectFinal: 'xLineTo(xAbs002, %)',
|
expectFinal: 'xLineTo(xAbs002, %)',
|
||||||
ang: -45,
|
ang: ang + 180,
|
||||||
steps: 8,
|
steps: 8,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -3297,7 +3286,7 @@ const part001 = startSketchOn('XZ')
|
|||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(8)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(8)
|
||||||
|
|
||||||
const clickUnconstrained = _clickUnconstrained(page)
|
const clickUnconstrained = _clickUnconstrained(page, u)
|
||||||
|
|
||||||
await page.mouse.move(700, 250)
|
await page.mouse.move(700, 250)
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
@ -3307,26 +3296,31 @@ const part001 = startSketchOn('XZ')
|
|||||||
|
|
||||||
await page.waitForTimeout(300)
|
await page.waitForTimeout(300)
|
||||||
|
|
||||||
|
let ang = 0
|
||||||
|
|
||||||
const yLineTo = await u.getBoundingBox(`[data-overlay-index="4"]`)
|
const yLineTo = await u.getBoundingBox(`[data-overlay-index="4"]`)
|
||||||
|
ang = await u.getAngle(`[data-overlay-index="4"]`)
|
||||||
console.log('ylineTo1')
|
console.log('ylineTo1')
|
||||||
await clickUnconstrained({
|
await clickUnconstrained({
|
||||||
hoverPos: { x: yLineTo.x, y: yLineTo.y - 30 },
|
hoverPos: { x: yLineTo.x, y: yLineTo.y },
|
||||||
constraintType: 'yAbsolute',
|
constraintType: 'yAbsolute',
|
||||||
expectBeforeUnconstrained: "yLineTo(-10.77, %, 'a')",
|
expectBeforeUnconstrained: "yLineTo(-10.77, %, 'a')",
|
||||||
expectAfterUnconstrained: "yLineTo(yAbs002, %, 'a')",
|
expectAfterUnconstrained: "yLineTo(yAbs002, %, 'a')",
|
||||||
expectFinal: "yLineTo(-10.77, %, 'a')",
|
expectFinal: "yLineTo(-10.77, %, 'a')",
|
||||||
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
|
|
||||||
const xLine = await u.getBoundingBox(`[data-overlay-index="5"]`)
|
const xLine = await u.getBoundingBox(`[data-overlay-index="5"]`)
|
||||||
|
ang = await u.getAngle(`[data-overlay-index="5"]`)
|
||||||
console.log('xline')
|
console.log('xline')
|
||||||
await clickUnconstrained({
|
await clickUnconstrained({
|
||||||
hoverPos: { x: xLine.x - 25, y: xLine.y },
|
hoverPos: { x: xLine.x, y: xLine.y },
|
||||||
constraintType: 'xRelative',
|
constraintType: 'xRelative',
|
||||||
expectBeforeUnconstrained: 'xLine(26.04, %)',
|
expectBeforeUnconstrained: 'xLine(26.04, %)',
|
||||||
expectAfterUnconstrained: 'xLine(xRel002, %)',
|
expectAfterUnconstrained: 'xLine(xRel002, %)',
|
||||||
expectFinal: 'xLine(26.04, %)',
|
expectFinal: 'xLine(26.04, %)',
|
||||||
steps: 10,
|
steps: 10,
|
||||||
ang: 50,
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
test('for segments [yLine, angledLineOfXLength, angledLineOfYLength]', async ({
|
test('for segments [yLine, angledLineOfXLength, angledLineOfYLength]', async ({
|
||||||
@ -3366,6 +3360,7 @@ const part001 = startSketchOn('XZ')
|
|||||||
await u.openDebugPanel()
|
await u.openDebugPanel()
|
||||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
await u.closeDebugPanel()
|
await u.closeDebugPanel()
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
|
||||||
await page.getByText('xLineTo(9 - 5, %)').click()
|
await page.getByText('xLineTo(9 - 5, %)').click()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
@ -3374,10 +3369,13 @@ const part001 = startSketchOn('XZ')
|
|||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
||||||
|
|
||||||
const clickUnconstrained = _clickUnconstrained(page)
|
const clickUnconstrained = _clickUnconstrained(page, u)
|
||||||
const clickConstrained = _clickConstrained(page)
|
const clickConstrained = _clickConstrained(page, u)
|
||||||
|
|
||||||
|
let ang = 0
|
||||||
|
|
||||||
const yLine = await u.getBoundingBox(`[data-overlay-index="6"]`)
|
const yLine = await u.getBoundingBox(`[data-overlay-index="6"]`)
|
||||||
|
ang = await u.getAngle(`[data-overlay-index="6"]`)
|
||||||
console.log('yline1')
|
console.log('yline1')
|
||||||
await clickConstrained({
|
await clickConstrained({
|
||||||
hoverPos: { x: yLine.x, y: yLine.y + 20 },
|
hoverPos: { x: yLine.x, y: yLine.y + 20 },
|
||||||
@ -3385,11 +3383,13 @@ const part001 = startSketchOn('XZ')
|
|||||||
expectBeforeUnconstrained: 'yLine(21.14 + 0, %)',
|
expectBeforeUnconstrained: 'yLine(21.14 + 0, %)',
|
||||||
expectAfterUnconstrained: 'yLine(21.14, %)',
|
expectAfterUnconstrained: 'yLine(21.14, %)',
|
||||||
expectFinal: 'yLine(yRel001, %)',
|
expectFinal: 'yLine(yRel001, %)',
|
||||||
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
|
|
||||||
const angledLineOfXLength = await u.getBoundingBox(
|
const angledLineOfXLength = await u.getBoundingBox(
|
||||||
`[data-overlay-index="7"]`
|
`[data-overlay-index="7"]`
|
||||||
)
|
)
|
||||||
|
ang = await u.getAngle(`[data-overlay-index="7"]`)
|
||||||
console.log('angledLineOfXLength1')
|
console.log('angledLineOfXLength1')
|
||||||
await clickConstrained({
|
await clickConstrained({
|
||||||
hoverPos: { x: angledLineOfXLength.x + 20, y: angledLineOfXLength.y },
|
hoverPos: { x: angledLineOfXLength.x + 20, y: angledLineOfXLength.y },
|
||||||
@ -3400,6 +3400,7 @@ const part001 = startSketchOn('XZ')
|
|||||||
'angledLineOfXLength({ angle: -179, length: 23.14 }, %)',
|
'angledLineOfXLength({ angle: -179, length: 23.14 }, %)',
|
||||||
expectFinal:
|
expectFinal:
|
||||||
'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)',
|
'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)',
|
||||||
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
console.log('angledLineOfXLength2')
|
console.log('angledLineOfXLength2')
|
||||||
await clickUnconstrained({
|
await clickUnconstrained({
|
||||||
@ -3412,11 +3413,13 @@ const part001 = startSketchOn('XZ')
|
|||||||
expectFinal:
|
expectFinal:
|
||||||
'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)',
|
'angledLineOfXLength({ angle: angle001, length: 23.14 }, %)',
|
||||||
steps: 7,
|
steps: 7,
|
||||||
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
|
|
||||||
const angledLineOfYLength = await u.getBoundingBox(
|
const angledLineOfYLength = await u.getBoundingBox(
|
||||||
`[data-overlay-index="8"]`
|
`[data-overlay-index="8"]`
|
||||||
)
|
)
|
||||||
|
ang = await u.getAngle(`[data-overlay-index="8"]`)
|
||||||
console.log('angledLineOfYLength1')
|
console.log('angledLineOfYLength1')
|
||||||
await clickUnconstrained({
|
await clickUnconstrained({
|
||||||
hoverPos: { x: angledLineOfYLength.x, y: angledLineOfYLength.y - 20 },
|
hoverPos: { x: angledLineOfYLength.x, y: angledLineOfYLength.y - 20 },
|
||||||
@ -3426,7 +3429,7 @@ const part001 = startSketchOn('XZ')
|
|||||||
expectAfterUnconstrained:
|
expectAfterUnconstrained:
|
||||||
'angledLineOfYLength({ angle: angle002, length: 19 + 0 }, %)',
|
'angledLineOfYLength({ angle: angle002, length: 19 + 0 }, %)',
|
||||||
expectFinal: 'angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)',
|
expectFinal: 'angledLineOfYLength({ angle: -91, length: 19 + 0 }, %)',
|
||||||
ang: 135,
|
ang: ang + 180,
|
||||||
steps: 6,
|
steps: 6,
|
||||||
})
|
})
|
||||||
console.log('angledLineOfYLength2')
|
console.log('angledLineOfYLength2')
|
||||||
@ -3438,14 +3441,13 @@ const part001 = startSketchOn('XZ')
|
|||||||
expectAfterUnconstrained:
|
expectAfterUnconstrained:
|
||||||
'angledLineOfYLength({ angle: -91, length: 19 }, %)',
|
'angledLineOfYLength({ angle: -91, length: 19 }, %)',
|
||||||
expectFinal: 'angledLineOfYLength({ angle: -91, length: yRel002 }, %)',
|
expectFinal: 'angledLineOfYLength({ angle: -91, length: yRel002 }, %)',
|
||||||
ang: -45,
|
ang: ang + 180,
|
||||||
steps: 7,
|
steps: 7,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
test('for segments [angledLineToX, angledLineToY, angledLineThatIntersects]', async ({
|
test('for segments [angledLineToX, angledLineToY, angledLineThatIntersects]', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
test.skip(process.platform !== 'darwin', 'too flakey on ubuntu')
|
|
||||||
await page.addInitScript(async () => {
|
await page.addInitScript(async () => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
'persistCode',
|
'persistCode',
|
||||||
@ -3488,17 +3490,21 @@ const part001 = startSketchOn('XZ')
|
|||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
||||||
|
|
||||||
const clickUnconstrained = _clickUnconstrained(page)
|
const clickUnconstrained = _clickUnconstrained(page, u)
|
||||||
const clickConstrained = _clickConstrained(page)
|
const clickConstrained = _clickConstrained(page, u)
|
||||||
|
|
||||||
|
let ang = 0
|
||||||
|
|
||||||
const angledLineToX = await u.getBoundingBox(`[data-overlay-index="9"]`)
|
const angledLineToX = await u.getBoundingBox(`[data-overlay-index="9"]`)
|
||||||
|
ang = await u.getAngle(`[data-overlay-index="9"]`)
|
||||||
console.log('angledLineToX')
|
console.log('angledLineToX')
|
||||||
await clickConstrained({
|
await clickConstrained({
|
||||||
hoverPos: { x: angledLineToX.x - 20, y: angledLineToX.y },
|
hoverPos: { x: angledLineToX.x, y: angledLineToX.y },
|
||||||
constraintType: 'angle',
|
constraintType: 'angle',
|
||||||
expectBeforeUnconstrained: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)',
|
expectBeforeUnconstrained: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)',
|
||||||
expectAfterUnconstrained: 'angledLineToX({ angle: 3, to: 26 }, %)',
|
expectAfterUnconstrained: 'angledLineToX({ angle: 3, to: 26 }, %)',
|
||||||
expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)',
|
expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)',
|
||||||
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
console.log('angledLineToX2')
|
console.log('angledLineToX2')
|
||||||
await clickUnconstrained({
|
await clickUnconstrained({
|
||||||
@ -3509,12 +3515,14 @@ const part001 = startSketchOn('XZ')
|
|||||||
expectAfterUnconstrained:
|
expectAfterUnconstrained:
|
||||||
'angledLineToX({ angle: angle001, to: xAbs001 }, %)',
|
'angledLineToX({ angle: angle001, to: xAbs001 }, %)',
|
||||||
expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)',
|
expectFinal: 'angledLineToX({ angle: angle001, to: 26 }, %)',
|
||||||
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
|
|
||||||
const angledLineToY = await u.getBoundingBox(`[data-overlay-index="10"]`)
|
const angledLineToY = await u.getBoundingBox(`[data-overlay-index="10"]`)
|
||||||
|
ang = await u.getAngle(`[data-overlay-index="10"]`)
|
||||||
console.log('angledLineToY')
|
console.log('angledLineToY')
|
||||||
await clickUnconstrained({
|
await clickUnconstrained({
|
||||||
hoverPos: { x: angledLineToY.x, y: angledLineToY.y + 20 },
|
hoverPos: { x: angledLineToY.x, y: angledLineToY.y },
|
||||||
constraintType: 'angle',
|
constraintType: 'angle',
|
||||||
expectBeforeUnconstrained:
|
expectBeforeUnconstrained:
|
||||||
'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
|
'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
|
||||||
@ -3522,7 +3530,7 @@ const part001 = startSketchOn('XZ')
|
|||||||
'angledLineToY({ angle: angle002, to: 9.14 + 0 }, %)',
|
'angledLineToY({ angle: angle002, to: 9.14 + 0 }, %)',
|
||||||
expectFinal: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
|
expectFinal: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
|
||||||
steps: process.platform === 'darwin' ? 8 : 9,
|
steps: process.platform === 'darwin' ? 8 : 9,
|
||||||
ang: 135,
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
console.log('angledLineToY2')
|
console.log('angledLineToY2')
|
||||||
await clickConstrained({
|
await clickConstrained({
|
||||||
@ -3532,12 +3540,13 @@ const part001 = startSketchOn('XZ')
|
|||||||
'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
|
'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
|
||||||
expectAfterUnconstrained: 'angledLineToY({ angle: 89, to: 9.14 }, %)',
|
expectAfterUnconstrained: 'angledLineToY({ angle: 89, to: 9.14 }, %)',
|
||||||
expectFinal: 'angledLineToY({ angle: 89, to: yAbs001 }, %)',
|
expectFinal: 'angledLineToY({ angle: 89, to: yAbs001 }, %)',
|
||||||
ang: 135,
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
|
|
||||||
const angledLineThatIntersects = await u.getBoundingBox(
|
const angledLineThatIntersects = await u.getBoundingBox(
|
||||||
`[data-overlay-index="11"]`
|
`[data-overlay-index="11"]`
|
||||||
)
|
)
|
||||||
|
ang = await u.getAngle(`[data-overlay-index="11"]`)
|
||||||
console.log('angledLineThatIntersects')
|
console.log('angledLineThatIntersects')
|
||||||
await clickUnconstrained({
|
await clickUnconstrained({
|
||||||
hoverPos: {
|
hoverPos: {
|
||||||
@ -3560,7 +3569,7 @@ const part001 = startSketchOn('XZ')
|
|||||||
offset: 9,
|
offset: 9,
|
||||||
intersectTag: 'a'
|
intersectTag: 'a'
|
||||||
}, %)`,
|
}, %)`,
|
||||||
ang: -45,
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
console.log('angledLineThatIntersects2')
|
console.log('angledLineThatIntersects2')
|
||||||
await clickUnconstrained({
|
await clickUnconstrained({
|
||||||
@ -3584,7 +3593,7 @@ const part001 = startSketchOn('XZ')
|
|||||||
offset: 9,
|
offset: 9,
|
||||||
intersectTag: 'a'
|
intersectTag: 'a'
|
||||||
}, %)`,
|
}, %)`,
|
||||||
ang: -25,
|
ang: ang + 180,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
test('for segment [tangentialArcTo]', async ({ page }) => {
|
test('for segment [tangentialArcTo]', async ({ page }) => {
|
||||||
@ -3630,30 +3639,31 @@ const part001 = startSketchOn('XZ')
|
|||||||
|
|
||||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
||||||
|
|
||||||
const clickUnconstrained = _clickUnconstrained(page)
|
const clickUnconstrained = _clickUnconstrained(page, u)
|
||||||
const clickConstrained = _clickConstrained(page)
|
const clickConstrained = _clickConstrained(page, u)
|
||||||
|
|
||||||
const tangentialArcTo = await u.getBoundingBox(
|
const tangentialArcTo = await u.getBoundingBox(
|
||||||
`[data-overlay-index="12"]`
|
`[data-overlay-index="12"]`
|
||||||
)
|
)
|
||||||
|
let ang = await u.getAngle(`[data-overlay-index="12"]`)
|
||||||
console.log('tangentialArcTo')
|
console.log('tangentialArcTo')
|
||||||
await clickConstrained({
|
await clickConstrained({
|
||||||
hoverPos: { x: tangentialArcTo.x - 10, y: tangentialArcTo.y + 20 },
|
hoverPos: { x: tangentialArcTo.x, y: tangentialArcTo.y },
|
||||||
constraintType: 'xAbsolute',
|
constraintType: 'xAbsolute',
|
||||||
expectBeforeUnconstrained: 'tangentialArcTo([3.14 + 13, -3.14], %)',
|
expectBeforeUnconstrained: 'tangentialArcTo([3.14 + 13, -3.14], %)',
|
||||||
expectAfterUnconstrained: 'tangentialArcTo([16.14, -3.14], %)',
|
expectAfterUnconstrained: 'tangentialArcTo([16.14, -3.14], %)',
|
||||||
expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)',
|
expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)',
|
||||||
ang: -45,
|
ang: ang + 180,
|
||||||
steps: 6,
|
steps: 6,
|
||||||
})
|
})
|
||||||
console.log('tangentialArcTo2')
|
console.log('tangentialArcTo2')
|
||||||
await clickUnconstrained({
|
await clickUnconstrained({
|
||||||
hoverPos: { x: tangentialArcTo.x - 10, y: tangentialArcTo.y + 20 },
|
hoverPos: { x: tangentialArcTo.x, y: tangentialArcTo.y },
|
||||||
constraintType: 'yAbsolute',
|
constraintType: 'yAbsolute',
|
||||||
expectBeforeUnconstrained: 'tangentialArcTo([xAbs001, -3.14], %)',
|
expectBeforeUnconstrained: 'tangentialArcTo([xAbs001, -3.14], %)',
|
||||||
expectAfterUnconstrained: 'tangentialArcTo([xAbs001, yAbs001], %)',
|
expectAfterUnconstrained: 'tangentialArcTo([xAbs001, yAbs001], %)',
|
||||||
expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)',
|
expectFinal: 'tangentialArcTo([xAbs001, -3.14], %)',
|
||||||
ang: -135,
|
ang: ang + 180,
|
||||||
steps: 10,
|
steps: 10,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -3748,25 +3758,7 @@ const part001 = startSketchOn('XZ')
|
|||||||
steps: 6,
|
steps: 6,
|
||||||
})
|
})
|
||||||
|
|
||||||
segmentToDelete = await getOverlayByIndex(0)
|
segmentToDelete = await getOverlayByIndex(11)
|
||||||
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)
|
|
||||||
await deleteSegmentSequence({
|
await deleteSegmentSequence({
|
||||||
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
|
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
|
||||||
codeToBeDeleted: `angledLineThatIntersects({
|
codeToBeDeleted: `angledLineThatIntersects({
|
||||||
@ -3779,21 +3771,21 @@ const part001 = startSketchOn('XZ')
|
|||||||
steps: 7,
|
steps: 7,
|
||||||
})
|
})
|
||||||
|
|
||||||
segmentToDelete = await getOverlayByIndex(8)
|
segmentToDelete = await getOverlayByIndex(10)
|
||||||
await deleteSegmentSequence({
|
await deleteSegmentSequence({
|
||||||
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
|
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
|
||||||
codeToBeDeleted: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
|
codeToBeDeleted: 'angledLineToY({ angle: 89, to: 9.14 + 0 }, %)',
|
||||||
stdLibFnName: 'angledLineToY',
|
stdLibFnName: 'angledLineToY',
|
||||||
})
|
})
|
||||||
|
|
||||||
segmentToDelete = await getOverlayByIndex(7)
|
segmentToDelete = await getOverlayByIndex(9)
|
||||||
await deleteSegmentSequence({
|
await deleteSegmentSequence({
|
||||||
hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y },
|
hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y },
|
||||||
codeToBeDeleted: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)',
|
codeToBeDeleted: 'angledLineToX({ angle: 3 + 0, to: 26 }, %)',
|
||||||
stdLibFnName: 'angledLineToX',
|
stdLibFnName: 'angledLineToX',
|
||||||
})
|
})
|
||||||
|
|
||||||
segmentToDelete = await getOverlayByIndex(6)
|
segmentToDelete = await getOverlayByIndex(8)
|
||||||
await deleteSegmentSequence({
|
await deleteSegmentSequence({
|
||||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 },
|
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 },
|
||||||
codeToBeDeleted:
|
codeToBeDeleted:
|
||||||
@ -3801,7 +3793,7 @@ const part001 = startSketchOn('XZ')
|
|||||||
stdLibFnName: 'angledLineOfYLength',
|
stdLibFnName: 'angledLineOfYLength',
|
||||||
})
|
})
|
||||||
|
|
||||||
segmentToDelete = await getOverlayByIndex(5)
|
segmentToDelete = await getOverlayByIndex(7)
|
||||||
await deleteSegmentSequence({
|
await deleteSegmentSequence({
|
||||||
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
|
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
|
||||||
codeToBeDeleted:
|
codeToBeDeleted:
|
||||||
@ -3809,42 +3801,36 @@ const part001 = startSketchOn('XZ')
|
|||||||
stdLibFnName: 'angledLineOfXLength',
|
stdLibFnName: 'angledLineOfXLength',
|
||||||
})
|
})
|
||||||
|
|
||||||
segmentToDelete = await getOverlayByIndex(4)
|
segmentToDelete = await getOverlayByIndex(6)
|
||||||
await deleteSegmentSequence({
|
await deleteSegmentSequence({
|
||||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y + 10 },
|
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y + 10 },
|
||||||
codeToBeDeleted: 'yLine(21.14 + 0, %)',
|
codeToBeDeleted: 'yLine(21.14 + 0, %)',
|
||||||
stdLibFnName: 'yLine',
|
stdLibFnName: 'yLine',
|
||||||
})
|
})
|
||||||
|
|
||||||
segmentToDelete = await getOverlayByIndex(3)
|
segmentToDelete = await getOverlayByIndex(5)
|
||||||
await deleteSegmentSequence({
|
await deleteSegmentSequence({
|
||||||
hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y },
|
hoverPos: { x: segmentToDelete.x - 10, y: segmentToDelete.y },
|
||||||
codeToBeDeleted: 'xLine(26.04, %)',
|
codeToBeDeleted: 'xLine(26.04, %)',
|
||||||
stdLibFnName: 'xLine',
|
stdLibFnName: 'xLine',
|
||||||
})
|
})
|
||||||
|
|
||||||
segmentToDelete = await getOverlayByIndex(2)
|
segmentToDelete = await getOverlayByIndex(4)
|
||||||
await deleteSegmentSequence({
|
await deleteSegmentSequence({
|
||||||
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 },
|
hoverPos: { x: segmentToDelete.x, y: segmentToDelete.y - 10 },
|
||||||
codeToBeDeleted: "yLineTo(-10.77, %, 'a')",
|
codeToBeDeleted: "yLineTo(-10.77, %, 'a')",
|
||||||
stdLibFnName: 'yLineTo',
|
stdLibFnName: 'yLineTo',
|
||||||
})
|
})
|
||||||
|
|
||||||
segmentToDelete = await getOverlayByIndex(1)
|
segmentToDelete = await getOverlayByIndex(3)
|
||||||
await deleteSegmentSequence({
|
await deleteSegmentSequence({
|
||||||
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
|
hoverPos: { x: segmentToDelete.x + 10, y: segmentToDelete.y },
|
||||||
codeToBeDeleted: 'xLineTo(9 - 5, %)',
|
codeToBeDeleted: 'xLineTo(9 - 5, %)',
|
||||||
stdLibFnName: 'xLineTo',
|
stdLibFnName: 'xLineTo',
|
||||||
})
|
})
|
||||||
|
|
||||||
for (let i = 0; i < 15; i++) {
|
// Not sure why this is diff. from the others - Kurt, ideas?
|
||||||
await page.mouse.wheel(0, 100)
|
segmentToDelete = await getOverlayByIndex(2)
|
||||||
await page.waitForTimeout(25)
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForTimeout(200)
|
|
||||||
|
|
||||||
segmentToDelete = await getOverlayByIndex(0)
|
|
||||||
const hoverPos = { x: segmentToDelete.x - 10, y: segmentToDelete.y + 10 }
|
const hoverPos = { x: segmentToDelete.x - 10, y: segmentToDelete.y + 10 }
|
||||||
await expect(page.getByText('Added variable')).not.toBeVisible()
|
await expect(page.getByText('Added variable')).not.toBeVisible()
|
||||||
const [x, y] = [
|
const [x, y] = [
|
||||||
@ -3863,6 +3849,24 @@ const part001 = startSketchOn('XZ')
|
|||||||
await expect(page.locator('.cm-content')).not.toContainText(
|
await expect(page.locator('.cm-content')).not.toContainText(
|
||||||
codeToBeDeleted
|
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', () => {
|
test.describe('Testing delete with dependent segments', () => {
|
||||||
@ -4158,6 +4162,11 @@ test('simulate network down and network little widget', async ({ page }) => {
|
|||||||
await page.goto('/')
|
await page.goto('/')
|
||||||
await u.waitForAuthSkipAppStart()
|
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"]')
|
const networkWidget = page.locator('[data-testid="network-toggle"]')
|
||||||
await expect(networkWidget).toBeVisible()
|
await expect(networkWidget).toBeVisible()
|
||||||
await networkWidget.hover()
|
await networkWidget.hover()
|
||||||
@ -4165,7 +4174,7 @@ test('simulate network down and network little widget', async ({ page }) => {
|
|||||||
const networkPopover = page.locator('[data-testid="network-popover"]')
|
const networkPopover = page.locator('[data-testid="network-popover"]')
|
||||||
await expect(networkPopover).not.toBeVisible()
|
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()
|
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
|
||||||
|
|
||||||
// Click the network widget
|
// Click the network widget
|
||||||
@ -4209,7 +4218,11 @@ test('simulate network down and network little widget', async ({ page }) => {
|
|||||||
uploadThroughput: -1,
|
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()
|
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -4223,8 +4236,7 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Start Sketch' })
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
).not.toBeDisabled()
|
).not.toBeDisabled({ timeout: 15000 })
|
||||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
|
||||||
|
|
||||||
// click on "Start Sketch" button
|
// click on "Start Sketch" button
|
||||||
await u.clearCommandLogs()
|
await u.clearCommandLogs()
|
||||||
@ -4287,6 +4299,10 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Wait for the app to be ready for use
|
// 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
|
// Expect the network to be up
|
||||||
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
|
await expect(page.getByText('Network Health (Connected)')).toBeVisible()
|
||||||
|
|
||||||
@ -4314,15 +4330,15 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
|
|||||||
.toHaveText(`const part001 = startSketchOn('XZ')
|
.toHaveText(`const part001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([-11.59, 11.1], %)`)
|
|> line([-11.64, 11.11], %)`)
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||||
await expect(page.locator('.cm-content'))
|
await expect(page.locator('.cm-content'))
|
||||||
.toHaveText(`const part001 = startSketchOn('XZ')
|
.toHaveText(`const part001 = startSketchOn('XZ')
|
||||||
|> startProfileAt(${commonPoints.startAt}, %)
|
|> startProfileAt(${commonPoints.startAt}, %)
|
||||||
|> line([${commonPoints.num1}, 0], %)
|
|> line([${commonPoints.num1}, 0], %)
|
||||||
|> line([-11.59, 11.1], %)
|
|> line([-11.64, 11.11], %)
|
||||||
|> line([-6.61, 0], %)`)
|
|> line([-6.56, 0], %)`)
|
||||||
|
|
||||||
// Unequip line tool
|
// Unequip line tool
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 45 KiB |
@ -102,6 +102,29 @@ export async function getUtils(page: Page) {
|
|||||||
const cdpSession =
|
const cdpSession =
|
||||||
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
||||||
|
|
||||||
|
let click00rCenter = { x: 0, y: 0 }
|
||||||
|
const click00 = (x: number, y: number) =>
|
||||||
|
page.mouse.click(click00rCenter.x + x, click00rCenter.y + y)
|
||||||
|
let click00rLastPos = { x: 0, y: 0 }
|
||||||
|
|
||||||
|
// 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]))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
waitForAuthSkipAppStart: () => waitForPageLoad(page),
|
waitForAuthSkipAppStart: () => waitForPageLoad(page),
|
||||||
removeCurrentCode: () => removeCurrentCode(page),
|
removeCurrentCode: () => removeCurrentCode(page),
|
||||||
@ -145,11 +168,15 @@ export async function getUtils(page: Page) {
|
|||||||
y: bbox.y - angleYOffset,
|
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) =>
|
getBoundingBox: async (locator: string) =>
|
||||||
page
|
page
|
||||||
.locator(locator)
|
.locator(locator)
|
||||||
.boundingBox()
|
.boundingBox()
|
||||||
.then((box) => ({ x: box?.x || 0, y: box?.y || 0 })),
|
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
|
||||||
doAndWaitForCmd: async (
|
doAndWaitForCmd: async (
|
||||||
fn: () => Promise<void>,
|
fn: () => Promise<void>,
|
||||||
commandType: string,
|
commandType: string,
|
||||||
@ -217,6 +244,51 @@ export async function getUtils(page: Page) {
|
|||||||
|
|
||||||
cdpSession?.send('Network.emulateNetworkConditions', networkOptions)
|
cdpSession?.send('Network.emulateNetworkConditions', networkOptions)
|
||||||
},
|
},
|
||||||
|
expectCodeToBe: async (str: string) => {
|
||||||
|
await expect(page.locator('.cm-content')).toHaveText(str)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
},
|
||||||
|
click00rSetCenter: (x: number, y: number) => {
|
||||||
|
click00rCenter = { x, y }
|
||||||
|
},
|
||||||
|
click00r: (x?: number, y?: number) => {
|
||||||
|
// reset relative coordinates when anything is undefined
|
||||||
|
if (x === undefined || y === undefined) {
|
||||||
|
click00rLastPos.x = 0
|
||||||
|
click00rLastPos.y = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = click00(click00rLastPos.x + x, click00rLastPos.y + y)
|
||||||
|
click00rLastPos.x += x
|
||||||
|
click00rLastPos.y += y
|
||||||
|
|
||||||
|
// Returns the new absolute coordinate if you need it.
|
||||||
|
return ret.then(() => [click00rLastPos.x, click00rLastPos.y])
|
||||||
|
},
|
||||||
|
toSU,
|
||||||
|
wiggleMove: async (
|
||||||
|
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 y1 = 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 })
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@headlessui/react": "^1.7.19",
|
"@headlessui/react": "^1.7.19",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@kittycad/lib": "^0.0.63",
|
"@kittycad/lib": "^0.0.64",
|
||||||
"@lezer/javascript": "^1.4.9",
|
"@lezer/javascript": "^1.4.9",
|
||||||
"@open-rpc/client-js": "^1.8.1",
|
"@open-rpc/client-js": "^1.8.1",
|
||||||
"@react-hook/resize-observer": "^2.0.1",
|
"@react-hook/resize-observer": "^2.0.1",
|
||||||
@ -95,7 +95,8 @@
|
|||||||
"lint": "eslint --fix src",
|
"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",
|
"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",
|
"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": {
|
"prettier": {
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
|
@ -18,7 +18,7 @@ export default defineConfig({
|
|||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: process.env.CI ? 3 : 0,
|
retries: process.env.CI ? 3 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: process.env.CI ? 1 : 1,
|
workers: process.env.CI ? 2 : 1,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
@ -72,7 +72,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'yarn serve',
|
command: 'yarn start',
|
||||||
// url: 'http://127.0.0.1:3000',
|
// url: 'http://127.0.0.1:3000',
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
|
@ -12,6 +12,8 @@ import SignIn from './routes/SignIn'
|
|||||||
import { Auth } from './Auth'
|
import { Auth } from './Auth'
|
||||||
import { isTauri } from './lib/isTauri'
|
import { isTauri } from './lib/isTauri'
|
||||||
import Home from './routes/Home'
|
import Home from './routes/Home'
|
||||||
|
import { NetworkContext } from './hooks/useNetworkContext'
|
||||||
|
import { useNetworkStatus } from './hooks/useNetworkStatus'
|
||||||
import makeUrlPathRelative from './lib/makeUrlPathRelative'
|
import makeUrlPathRelative from './lib/makeUrlPathRelative'
|
||||||
import DownloadAppBanner from 'components/DownloadAppBanner'
|
import DownloadAppBanner from 'components/DownloadAppBanner'
|
||||||
import { WasmErrBanner } from 'components/WasmErrBanner'
|
import { WasmErrBanner } from 'components/WasmErrBanner'
|
||||||
@ -155,5 +157,11 @@ const router = createBrowserRouter([
|
|||||||
* @returns RouterProvider
|
* @returns RouterProvider
|
||||||
*/
|
*/
|
||||||
export const Router = () => {
|
export const Router = () => {
|
||||||
return <RouterProvider router={router} />
|
const networkStatus = useNetworkStatus()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NetworkContext.Provider value={networkStatus as any}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</NetworkContext.Provider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,11 @@ import { isCursorInSketchCommandRange } from 'lang/util'
|
|||||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { isSingleCursorInPipe } from 'lang/queryAst'
|
import { isSingleCursorInPipe } from 'lang/queryAst'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import {
|
|
||||||
NetworkHealthState,
|
|
||||||
useNetworkStatus,
|
|
||||||
} from 'components/NetworkHealthIndicator'
|
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
@ -38,14 +36,16 @@ export function Toolbar({
|
|||||||
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
||||||
|
|
||||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||||
|
const { overallState } = useNetworkContext()
|
||||||
const { overallState } = useNetworkStatus()
|
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useStore((s) => ({
|
const { isStreamReady } = useStore((s) => ({
|
||||||
isStreamReady: s.isStreamReady,
|
isStreamReady: s.isStreamReady,
|
||||||
}))
|
}))
|
||||||
const disableAllButtons =
|
const disableAllButtons =
|
||||||
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
|
(overallState !== NetworkHealthState.Ok &&
|
||||||
|
overallState !== NetworkHealthState.Weak) ||
|
||||||
|
isExecuting ||
|
||||||
|
!isStreamReady
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'l',
|
'l',
|
||||||
|
@ -1,14 +1,65 @@
|
|||||||
import { useEffect, useState } from 'react'
|
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 Loading = ({ children }: React.PropsWithChildren) => {
|
||||||
const [hasLongLoadTime, setHasLongLoadTime] = useState(false)
|
const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
|
||||||
|
|
||||||
useEffect(() => {
|
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(() => {
|
const timer = setTimeout(() => {
|
||||||
setHasLongLoadTime(true)
|
setError(ConnectionError.LongLoadingTime)
|
||||||
}, 4000)
|
}, 4000)
|
||||||
|
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}, [setHasLongLoadTime])
|
}, [error, setError])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="body-bg flex flex-col items-center justify-center h-screen"
|
className="body-bg flex flex-col items-center justify-center h-screen"
|
||||||
@ -29,10 +80,10 @@ const Loading = ({ children }: React.PropsWithChildren) => {
|
|||||||
<p
|
<p
|
||||||
className={
|
className={
|
||||||
'text-sm mt-4 text-primary/60 transition-opacity duration-500' +
|
'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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -13,7 +13,6 @@ import { LanguageSupport } from '@codemirror/language'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { paths } from 'lib/paths'
|
import { paths } from 'lib/paths'
|
||||||
import { FileEntry } from 'lib/types'
|
import { FileEntry } from 'lib/types'
|
||||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
|
||||||
import Worker from 'editor/plugins/lsp/worker.ts?worker'
|
import Worker from 'editor/plugins/lsp/worker.ts?worker'
|
||||||
import {
|
import {
|
||||||
LspWorkerEventType,
|
LspWorkerEventType,
|
||||||
@ -23,6 +22,8 @@ import {
|
|||||||
} from 'editor/plugins/lsp/types'
|
} from 'editor/plugins/lsp/types'
|
||||||
import { wasmUrl } from 'lang/wasm'
|
import { wasmUrl } from 'lang/wasm'
|
||||||
import { PROJECT_ENTRYPOINT } from 'lib/constants'
|
import { PROJECT_ENTRYPOINT } from 'lib/constants'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
|
|
||||||
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
||||||
return []
|
return []
|
||||||
@ -86,7 +87,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
const token = auth?.context.token
|
const token = auth?.context.token
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { overallState } = useNetworkStatus()
|
const { overallState } = useNetworkContext()
|
||||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||||
|
|
||||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
// 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 {
|
import {
|
||||||
NETWORK_HEALTH_TEXT,
|
NETWORK_HEALTH_TEXT,
|
||||||
NetworkHealthIndicator,
|
NetworkHealthIndicator,
|
||||||
NetworkHealthState,
|
|
||||||
} from './NetworkHealthIndicator'
|
} from './NetworkHealthIndicator'
|
||||||
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
|
|
||||||
function TestWrap({ children }: { children: React.ReactNode }) {
|
function TestWrap({ children }: { children: React.ReactNode }) {
|
||||||
// wrap in router and xState context
|
// 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', () => {
|
describe('NetworkHealthIndicator tests', () => {
|
||||||
test('Renders the network indicator', () => {
|
test('Renders the network indicator', () => {
|
||||||
render(
|
render(
|
||||||
@ -29,21 +30,7 @@ describe('NetworkHealthIndicator tests', () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
fireEvent.click(screen.getByTestId('network-toggle'))
|
||||||
|
|
||||||
expect(screen.getByTestId('network')).toHaveTextContent(
|
// Starts as disconnected
|
||||||
NETWORK_HEALTH_TEXT[NetworkHealthState.Ok]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Responds to network changes', () => {
|
|
||||||
render(
|
|
||||||
<TestWrap>
|
|
||||||
<NetworkHealthIndicator />
|
|
||||||
</TestWrap>
|
|
||||||
)
|
|
||||||
|
|
||||||
fireEvent.offline(window)
|
|
||||||
fireEvent.click(screen.getByTestId('network-toggle'))
|
|
||||||
|
|
||||||
expect(screen.getByTestId('network')).toHaveTextContent(
|
expect(screen.getByTestId('network')).toHaveTextContent(
|
||||||
NETWORK_HEALTH_TEXT[NetworkHealthState.Disconnected]
|
NETWORK_HEALTH_TEXT[NetworkHealthState.Disconnected]
|
||||||
)
|
)
|
||||||
|
@ -1,26 +1,13 @@
|
|||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
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'
|
import Tooltip from './Tooltip'
|
||||||
|
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
|
||||||
export enum NetworkHealthState {
|
import { useNetworkContext } from '../hooks/useNetworkContext'
|
||||||
Ok,
|
import { NetworkHealthState } from '../hooks/useNetworkStatus'
|
||||||
Issue,
|
|
||||||
Disconnected,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
||||||
[NetworkHealthState.Ok]: 'Connected',
|
[NetworkHealthState.Ok]: 'Connected',
|
||||||
|
[NetworkHealthState.Weak]: 'Weak',
|
||||||
[NetworkHealthState.Issue]: 'Problem',
|
[NetworkHealthState.Issue]: 'Problem',
|
||||||
[NetworkHealthState.Disconnected]: 'Offline',
|
[NetworkHealthState.Disconnected]: 'Offline',
|
||||||
}
|
}
|
||||||
@ -61,6 +48,10 @@ const overallConnectionStateColor: Record<NetworkHealthState, IconColorConfig> =
|
|||||||
icon: 'text-succeed-80 dark:text-succeed-10',
|
icon: 'text-succeed-80 dark:text-succeed-10',
|
||||||
bg: 'bg-succeed-10/30 dark:bg-succeed-80/50',
|
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]: {
|
[NetworkHealthState.Issue]: {
|
||||||
icon: 'text-destroy-80 dark:text-destroy-10',
|
icon: 'text-destroy-80 dark:text-destroy-10',
|
||||||
bg: 'bg-destroy-10 dark:bg-destroy-80/80',
|
bg: 'bg-destroy-10 dark:bg-destroy-80/80',
|
||||||
@ -76,125 +67,11 @@ const overallConnectionStateIcon: Record<
|
|||||||
ActionIconProps['icon']
|
ActionIconProps['icon']
|
||||||
> = {
|
> = {
|
||||||
[NetworkHealthState.Ok]: 'network',
|
[NetworkHealthState.Ok]: 'network',
|
||||||
|
[NetworkHealthState.Weak]: 'network',
|
||||||
[NetworkHealthState.Issue]: 'networkCrossedOut',
|
[NetworkHealthState.Issue]: 'networkCrossedOut',
|
||||||
[NetworkHealthState.Disconnected]: '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 = () => {
|
export const NetworkHealthIndicator = () => {
|
||||||
const {
|
const {
|
||||||
hasIssues,
|
hasIssues,
|
||||||
@ -205,7 +82,7 @@ export const NetworkHealthIndicator = () => {
|
|||||||
error,
|
error,
|
||||||
setHasCopied,
|
setHasCopied,
|
||||||
hasCopied,
|
hasCopied,
|
||||||
} = useNetworkStatus()
|
} = useNetworkContext()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
@ -259,18 +136,18 @@ export const NetworkHealthIndicator = () => {
|
|||||||
size="lg"
|
size="lg"
|
||||||
icon={
|
icon={
|
||||||
hasIssueToIcon[
|
hasIssueToIcon[
|
||||||
issues[name as ConnectingTypeGroup].toString()
|
String(issues[name as ConnectingTypeGroup])
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
iconClassName={
|
iconClassName={
|
||||||
hasIssueToIconColors[
|
hasIssueToIconColors[
|
||||||
issues[name as ConnectingTypeGroup].toString()
|
String(issues[name as ConnectingTypeGroup])
|
||||||
].icon
|
].icon
|
||||||
}
|
}
|
||||||
bgClassName={
|
bgClassName={
|
||||||
'rounded-sm ' +
|
'rounded-sm ' +
|
||||||
hasIssueToIconColors[
|
hasIssueToIconColors[
|
||||||
issues[name as ConnectingTypeGroup].toString()
|
String(issues[name as ConnectingTypeGroup])
|
||||||
].bg
|
].bg
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -4,8 +4,9 @@ import { getNormalisedCoordinates } from '../lib/utils'
|
|||||||
import Loading from './Loading'
|
import Loading from './Loading'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
||||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
|
||||||
import { butName } from 'lib/cameraControls'
|
import { butName } from 'lib/cameraControls'
|
||||||
import { sendSelectEventToEngine } from 'lib/selections'
|
import { sendSelectEventToEngine } from 'lib/selections'
|
||||||
|
|
||||||
@ -28,8 +29,11 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
}))
|
}))
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const { state } = useModelingContext()
|
const { state } = useModelingContext()
|
||||||
const { overallState } = useNetworkStatus()
|
const { overallState } = useNetworkContext()
|
||||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
|
||||||
|
const isNetworkOkay =
|
||||||
|
overallState === NetworkHealthState.Ok ||
|
||||||
|
overallState === NetworkHealthState.Weak
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@ -43,6 +47,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
}, [mediaStream])
|
}, [mediaStream])
|
||||||
|
|
||||||
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
|
const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
|
if (!isNetworkOkay) return
|
||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
if (state.matches('Sketch')) return
|
if (state.matches('Sketch')) return
|
||||||
if (state.matches('Sketch no face')) return
|
if (state.matches('Sketch no face')) return
|
||||||
@ -58,6 +63,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
const handleMouseUp: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
|
if (!isNetworkOkay) return
|
||||||
if (!videoRef.current) return
|
if (!videoRef.current) return
|
||||||
setButtonDownInStream(undefined)
|
setButtonDownInStream(undefined)
|
||||||
if (state.matches('Sketch')) return
|
if (state.matches('Sketch')) return
|
||||||
@ -72,6 +78,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
|
const handleMouseMove: MouseEventHandler<HTMLVideoElement> = (e) => {
|
||||||
|
if (!isNetworkOkay) return
|
||||||
if (state.matches('Sketch')) return
|
if (state.matches('Sketch')) return
|
||||||
if (state.matches('Sketch no face')) return
|
if (state.matches('Sketch no face')) return
|
||||||
if (!clickCoords) return
|
if (!clickCoords) return
|
||||||
@ -112,7 +119,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
{!isNetworkOkay && !isLoading && (
|
{!isNetworkOkay && !isLoading && (
|
||||||
<div className="text-center absolute inset-0">
|
<div className="text-center absolute inset-0">
|
||||||
<Loading>
|
<Loading>
|
||||||
<span data-testid="loading-stream">Stream disconnected</span>
|
<span data-testid="loading-stream">Stream disconnected...</span>
|
||||||
</Loading>
|
</Loading>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -474,19 +474,13 @@ const completionRequester = (client: LanguageServerClient) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
|
export const copilotPlugin = (options: LanguageServerOptions): Extension => {
|
||||||
let plugin: LanguageServerPlugin | null = null
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
documentUri.of(options.documentUri),
|
documentUri.of(options.documentUri),
|
||||||
languageId.of('kcl'),
|
languageId.of('kcl'),
|
||||||
workspaceFolders.of(options.workspaceFolders),
|
workspaceFolders.of(options.workspaceFolders),
|
||||||
ViewPlugin.define(
|
ViewPlugin.define(
|
||||||
(view) =>
|
(view) =>
|
||||||
(plugin = new LanguageServerPlugin(
|
new LanguageServerPlugin(options.client, view, options.allowHTMLContent)
|
||||||
options.client,
|
|
||||||
view,
|
|
||||||
options.allowHTMLContent
|
|
||||||
))
|
|
||||||
),
|
),
|
||||||
completionDecoration,
|
completionDecoration,
|
||||||
Prec.highest(completionPlugin(options.client)),
|
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 { NetworkHealthState } from './useNetworkStatus'
|
||||||
|
|
||||||
|
export const NetworkContext = createContext({
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
export const useNetworkContext = () => {
|
||||||
|
return useContext(NetworkContext)
|
||||||
|
}
|
213
src/hooks/useNetworkStatus.tsx
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
engineCommandManager.pool = settings.pool
|
||||||
}
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
const startEngineInstance = () => {
|
||||||
// Load the engine command manager once with the initial width and height,
|
// Load the engine command manager once with the initial width and height,
|
||||||
// then we do not want to reload it.
|
// then we do not want to reload it.
|
||||||
const { width: quadWidth, height: quadHeight } = getDimensions(
|
const { width: quadWidth, height: quadHeight } = getDimensions(
|
||||||
@ -73,7 +73,12 @@ export function useSetupEngineManager(
|
|||||||
})
|
})
|
||||||
hasSetNonZeroDimensions.current = true
|
hasSetNonZeroDimensions.current = true
|
||||||
}
|
}
|
||||||
}, [streamRef?.current?.offsetWidth, streamRef?.current?.offsetHeight])
|
}
|
||||||
|
|
||||||
|
useLayoutEffect(startEngineInstance, [
|
||||||
|
streamRef?.current?.offsetWidth,
|
||||||
|
streamRef?.current?.offsetHeight,
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleResize = deferExecution(() => {
|
const handleResize = deferExecution(() => {
|
||||||
@ -96,8 +101,20 @@ export function useSetupEngineManager(
|
|||||||
}
|
}
|
||||||
}, 500)
|
}, 500)
|
||||||
|
|
||||||
|
const onOnline = () => {
|
||||||
|
startEngineInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onOffline = () => {
|
||||||
|
engineCommandManager.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('online', onOnline)
|
||||||
|
window.addEventListener('offline', onOffline)
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
return () => {
|
return () => {
|
||||||
|
window.removeEventListener('online', onOnline)
|
||||||
|
window.removeEventListener('offline', onOffline)
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
@ -7,12 +7,10 @@ import { authMachine } from 'machines/authMachine'
|
|||||||
import { settingsMachine } from 'machines/settingsMachine'
|
import { settingsMachine } from 'machines/settingsMachine'
|
||||||
import { homeMachine } from 'machines/homeMachine'
|
import { homeMachine } from 'machines/homeMachine'
|
||||||
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
|
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
|
||||||
import {
|
|
||||||
NetworkHealthState,
|
|
||||||
useNetworkStatus,
|
|
||||||
} from 'components/NetworkHealthIndicator'
|
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
|
|
||||||
// This might not be necessary, AnyStateMachine from xstate is working
|
// This might not be necessary, AnyStateMachine from xstate is working
|
||||||
export type AllMachines =
|
export type AllMachines =
|
||||||
@ -47,7 +45,7 @@ export default function useStateMachineCommands<
|
|||||||
onCancel,
|
onCancel,
|
||||||
}: UseStateMachineCommandsArgs<T, S>) {
|
}: UseStateMachineCommandsArgs<T, S>) {
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { overallState } = useNetworkStatus()
|
const { overallState } = useNetworkContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useStore((s) => ({
|
const { isStreamReady } = useStore((s) => ({
|
||||||
isStreamReady: s.isStreamReady,
|
isStreamReady: s.isStreamReady,
|
||||||
@ -55,7 +53,10 @@ export default function useStateMachineCommands<
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const disableAllButtons =
|
const disableAllButtons =
|
||||||
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
|
(overallState !== NetworkHealthState.Ok &&
|
||||||
|
overallState !== NetworkHealthState.Weak) ||
|
||||||
|
isExecuting ||
|
||||||
|
!isStreamReady
|
||||||
const newCommands = state.nextEvents
|
const newCommands = state.nextEvents
|
||||||
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
|
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
|
||||||
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { PathToNode, Program, SourceRange } from 'lang/wasm'
|
import { PathToNode, Program, SourceRange } from 'lang/wasm'
|
||||||
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env'
|
import { VITE_KC_API_WS_MODELING_URL } from 'env'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import { exportSave } from 'lib/exportSave'
|
import { exportSave } from 'lib/exportSave'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
@ -9,6 +9,9 @@ import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
|||||||
|
|
||||||
let lastMessage = ''
|
let lastMessage = ''
|
||||||
|
|
||||||
|
// TODO(paultag): This ought to be tweakable.
|
||||||
|
const pingIntervalMs = 10000
|
||||||
|
|
||||||
interface CommandInfo {
|
interface CommandInfo {
|
||||||
commandType: CommandTypes
|
commandType: CommandTypes
|
||||||
range: SourceRange
|
range: SourceRange
|
||||||
@ -26,6 +29,12 @@ interface CommandInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isHighlightSetEntity_type(
|
||||||
|
data: any
|
||||||
|
): data is Models['HighlightSetEntity_type'] {
|
||||||
|
return data.entity_id && data.sequence
|
||||||
|
}
|
||||||
|
|
||||||
type WebSocketResponse = Models['WebSocketResponse_type']
|
type WebSocketResponse = Models['WebSocketResponse_type']
|
||||||
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
|
type OkWebSocketResponseData = Models['OkWebSocketResponseData_type']
|
||||||
|
|
||||||
@ -110,9 +119,54 @@ export enum DisconnectingType {
|
|||||||
Quit = 'quit',
|
Quit = 'quit',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sorted by severity
|
||||||
|
export enum ConnectionError {
|
||||||
|
Unset = 0,
|
||||||
|
LongLoadingTime,
|
||||||
|
|
||||||
|
LostVideoStream,
|
||||||
|
ICENegotiate,
|
||||||
|
DataChannelError,
|
||||||
|
WebSocketError,
|
||||||
|
LocalDescriptionInvalid,
|
||||||
|
|
||||||
|
// These are more severe than protocol errors because they don't even allow
|
||||||
|
// the program to do any protocol messages in the first place if they occur.
|
||||||
|
MissingAuthToken,
|
||||||
|
BadAuthToken,
|
||||||
|
TooManyConnections,
|
||||||
|
|
||||||
|
// An unknown error is the most severe because it has not been classified
|
||||||
|
// or encountered before.
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CONNECTION_ERROR_TEXT: Record<ConnectionError, string> = {
|
||||||
|
[ConnectionError.Unset]: '',
|
||||||
|
[ConnectionError.LongLoadingTime]:
|
||||||
|
'Loading is taking longer than expected...',
|
||||||
|
[ConnectionError.LostVideoStream]:
|
||||||
|
'Lost connection to video stream... Reconnecting...',
|
||||||
|
[ConnectionError.ICENegotiate]: 'ICE negotiation failed.',
|
||||||
|
[ConnectionError.DataChannelError]: 'The data channel signaled an error.',
|
||||||
|
[ConnectionError.WebSocketError]: 'The websocket signaled an error.',
|
||||||
|
[ConnectionError.LocalDescriptionInvalid]:
|
||||||
|
'The local description is invalid.',
|
||||||
|
[ConnectionError.MissingAuthToken]:
|
||||||
|
'Your authorization token is missing; please login again.',
|
||||||
|
[ConnectionError.BadAuthToken]:
|
||||||
|
'Your authorization token is invalid; please login again.',
|
||||||
|
[ConnectionError.TooManyConnections]: 'There are too many connections.',
|
||||||
|
[ConnectionError.Unknown]:
|
||||||
|
'An unexpected error occurred. Please report this to us.',
|
||||||
|
}
|
||||||
|
|
||||||
export interface ErrorType {
|
export interface ErrorType {
|
||||||
// We may not necessary have an error to assign.
|
// The error we've encountered.
|
||||||
error?: Error
|
error: ConnectionError
|
||||||
|
|
||||||
|
// Additional context.
|
||||||
|
context?: any
|
||||||
|
|
||||||
// We assign this in the state setter because we may have not failed at
|
// We assign this in the state setter because we may have not failed at
|
||||||
// a Connecting state, which we check for there.
|
// a Connecting state, which we check for there.
|
||||||
@ -127,7 +181,7 @@ export type DisconnectingValue =
|
|||||||
// These are ordered by the expected sequence.
|
// These are ordered by the expected sequence.
|
||||||
export enum ConnectingType {
|
export enum ConnectingType {
|
||||||
WebSocketConnecting = 'websocket-connecting',
|
WebSocketConnecting = 'websocket-connecting',
|
||||||
WebSocketEstablished = 'websocket-established',
|
WebSocketOpen = 'websocket-open',
|
||||||
PeerConnectionCreated = 'peer-connection-created',
|
PeerConnectionCreated = 'peer-connection-created',
|
||||||
ICEServersSet = 'ice-servers-set',
|
ICEServersSet = 'ice-servers-set',
|
||||||
SetLocalDescription = 'set-local-description',
|
SetLocalDescription = 'set-local-description',
|
||||||
@ -154,7 +208,7 @@ export const initialConnectingTypeGroupState: Record<
|
|||||||
> = {
|
> = {
|
||||||
[ConnectingTypeGroup.WebSocket]: [
|
[ConnectingTypeGroup.WebSocket]: [
|
||||||
[ConnectingType.WebSocketConnecting, undefined],
|
[ConnectingType.WebSocketConnecting, undefined],
|
||||||
[ConnectingType.WebSocketEstablished, undefined],
|
[ConnectingType.WebSocketOpen, undefined],
|
||||||
],
|
],
|
||||||
[ConnectingTypeGroup.ICE]: [
|
[ConnectingTypeGroup.ICE]: [
|
||||||
[ConnectingType.PeerConnectionCreated, undefined],
|
[ConnectingType.PeerConnectionCreated, undefined],
|
||||||
@ -176,7 +230,7 @@ export const initialConnectingTypeGroupState: Record<
|
|||||||
|
|
||||||
export type ConnectingValue =
|
export type ConnectingValue =
|
||||||
| State<ConnectingType.WebSocketConnecting, void>
|
| State<ConnectingType.WebSocketConnecting, void>
|
||||||
| State<ConnectingType.WebSocketEstablished, void>
|
| State<ConnectingType.WebSocketOpen, void>
|
||||||
| State<ConnectingType.PeerConnectionCreated, void>
|
| State<ConnectingType.PeerConnectionCreated, void>
|
||||||
| State<ConnectingType.ICEServersSet, void>
|
| State<ConnectingType.ICEServersSet, void>
|
||||||
| State<ConnectingType.SetLocalDescription, void>
|
| State<ConnectingType.SetLocalDescription, void>
|
||||||
@ -197,12 +251,28 @@ export type EngineConnectionState =
|
|||||||
| State<EngineConnectionStateType.Disconnecting, DisconnectingValue>
|
| State<EngineConnectionStateType.Disconnecting, DisconnectingValue>
|
||||||
| State<EngineConnectionStateType.Disconnected, void>
|
| State<EngineConnectionStateType.Disconnected, void>
|
||||||
|
|
||||||
/**
|
export type PingPongState = 'OK' | 'TIMEOUT'
|
||||||
* EngineConnection encapsulates the connection(s) to the Engine
|
|
||||||
* for the EngineCommandManager; namely, the underlying WebSocket
|
export enum EngineConnectionEvents {
|
||||||
* and WebRTC connections.
|
// Fires for each ping-pong success or failure.
|
||||||
*/
|
PingPongChanged = 'ping-pong-changed', // (state: PingPongState) => void
|
||||||
class EngineConnection {
|
|
||||||
|
// For now, this is only used by the NetworkHealthIndicator.
|
||||||
|
// We can eventually use it for more, but one step at a time.
|
||||||
|
ConnectionStateChanged = 'connection-state-changed', // (state: EngineConnectionState) => void
|
||||||
|
|
||||||
|
// These are used for the EngineCommandManager and were created
|
||||||
|
// before onConnectionStateChange existed.
|
||||||
|
ConnectionStarted = 'connection-started', // (engineConnection: EngineConnection) => void
|
||||||
|
Opened = 'opened', // (engineConnection: EngineConnection) => void
|
||||||
|
Closed = 'closed', // (engineConnection: EngineConnection) => void
|
||||||
|
NewTrack = 'new-track', // (track: NewTrackArgs) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// EngineConnection encapsulates the connection(s) to the Engine
|
||||||
|
// for the EngineCommandManager; namely, the underlying WebSocket
|
||||||
|
// and WebRTC connections.
|
||||||
|
class EngineConnection extends EventTarget {
|
||||||
websocket?: WebSocket
|
websocket?: WebSocket
|
||||||
pc?: RTCPeerConnection
|
pc?: RTCPeerConnection
|
||||||
unreliableDataChannel?: RTCDataChannel
|
unreliableDataChannel?: RTCDataChannel
|
||||||
@ -222,12 +292,13 @@ class EngineConnection {
|
|||||||
if (next.type === EngineConnectionStateType.Disconnecting) {
|
if (next.type === EngineConnectionStateType.Disconnecting) {
|
||||||
const sub = next.value
|
const sub = next.value
|
||||||
if (sub.type === DisconnectingType.Error) {
|
if (sub.type === DisconnectingType.Error) {
|
||||||
|
console.log(sub)
|
||||||
|
|
||||||
// Record the last step we failed at.
|
// Record the last step we failed at.
|
||||||
// (Check the current state that we're about to override that
|
// (Check the current state that we're about to override that
|
||||||
// it was a Connecting state.)
|
// it was a Connecting state.)
|
||||||
console.log(sub)
|
|
||||||
if (this._state.type === EngineConnectionStateType.Connecting) {
|
if (this._state.type === EngineConnectionStateType.Connecting) {
|
||||||
if (!sub.value) sub.value = {}
|
if (!sub.value) sub.value = { error: ConnectionError.Unknown }
|
||||||
sub.value.lastConnectingValue = this._state.value
|
sub.value.lastConnectingValue = this._state.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +306,12 @@ class EngineConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._state = next
|
this._state = next
|
||||||
this.onConnectionStateChange(this._state)
|
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EngineConnectionEvents.ConnectionStateChanged, {
|
||||||
|
detail: this._state,
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private failedConnTimeout: IsomorphicTimeout | null
|
private failedConnTimeout: IsomorphicTimeout | null
|
||||||
@ -243,95 +319,80 @@ class EngineConnection {
|
|||||||
readonly url: string
|
readonly url: string
|
||||||
private readonly token?: string
|
private readonly token?: string
|
||||||
|
|
||||||
/**For now, this is only used by the NetworkHealthIndicator.
|
// TODO: actual type is ClientMetrics
|
||||||
* We can eventually use it for more, but one step at a time.
|
|
||||||
*/
|
|
||||||
private onConnectionStateChange: (state: EngineConnectionState) => void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used for the EngineCommandManager, created before
|
|
||||||
* onConnectionStateChange existed.
|
|
||||||
*/
|
|
||||||
private onEngineConnectionOpen: (engineConnection: EngineConnection) => void
|
|
||||||
/**
|
|
||||||
* Used for the EngineCommandManager, created before
|
|
||||||
* onConnectionStateChange existed.
|
|
||||||
*/
|
|
||||||
private onConnectionStarted: (engineConnection: EngineConnection) => void
|
|
||||||
/**
|
|
||||||
* Used for the EngineCommandManager, created before
|
|
||||||
* onConnectionStateChange existed.
|
|
||||||
*/
|
|
||||||
private onClose: (engineConnection: EngineConnection) => void
|
|
||||||
/**
|
|
||||||
* Used for the EngineCommandManager, created before
|
|
||||||
* onConnectionStateChange existed.
|
|
||||||
*/
|
|
||||||
private onNewTrack: (track: NewTrackArgs) => void
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @todo actual type is `ClientMetrics`
|
|
||||||
*/
|
|
||||||
public webrtcStatsCollector?: () => Promise<WebRTCClientMetrics>
|
public webrtcStatsCollector?: () => Promise<WebRTCClientMetrics>
|
||||||
private engineCommandManager: EngineCommandManager
|
private engineCommandManager: EngineCommandManager
|
||||||
|
|
||||||
|
private pingPongSpan: { ping?: Date; pong?: Date }
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
onConnectionStateChange = () => {},
|
|
||||||
onNewTrack = () => {},
|
|
||||||
onEngineConnectionOpen = () => {},
|
|
||||||
onConnectionStarted = () => {},
|
|
||||||
onClose = () => {},
|
|
||||||
}: {
|
}: {
|
||||||
engineCommandManager: EngineCommandManager
|
engineCommandManager: EngineCommandManager
|
||||||
url: string
|
url: string
|
||||||
token?: string
|
token?: string
|
||||||
onConnectionStateChange?: (state: EngineConnectionState) => void
|
|
||||||
onEngineConnectionOpen?: (engineConnection: EngineConnection) => void
|
|
||||||
onConnectionStarted?: (engineConnection: EngineConnection) => void
|
|
||||||
onClose?: (engineConnection: EngineConnection) => void
|
|
||||||
onNewTrack?: (track: NewTrackArgs) => void
|
|
||||||
}) {
|
}) {
|
||||||
|
super()
|
||||||
|
|
||||||
this.engineCommandManager = engineCommandManager
|
this.engineCommandManager = engineCommandManager
|
||||||
this.url = url
|
this.url = url
|
||||||
this.token = token
|
this.token = token
|
||||||
this.failedConnTimeout = null
|
this.failedConnTimeout = null
|
||||||
this.onConnectionStateChange = onConnectionStateChange
|
|
||||||
this.onEngineConnectionOpen = onEngineConnectionOpen
|
|
||||||
this.onConnectionStarted = onConnectionStarted
|
|
||||||
|
|
||||||
this.onClose = onClose
|
this.pingPongSpan = { ping: undefined, pong: undefined }
|
||||||
this.onNewTrack = onNewTrack
|
|
||||||
|
|
||||||
// TODO(paultag): This ought to be tweakable.
|
|
||||||
const pingIntervalMs = 10000
|
|
||||||
|
|
||||||
// Without an interval ping, our connection will timeout.
|
// Without an interval ping, our connection will timeout.
|
||||||
let pingInterval = setInterval(() => {
|
setInterval(() => {
|
||||||
switch (this.state.type as EngineConnectionStateType) {
|
switch (this.state.type as EngineConnectionStateType) {
|
||||||
case EngineConnectionStateType.ConnectionEstablished:
|
case EngineConnectionStateType.ConnectionEstablished:
|
||||||
|
// If there was no reply to the last ping, report a timeout.
|
||||||
|
if (this.pingPongSpan.ping && !this.pingPongSpan.pong) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EngineConnectionEvents.PingPongChanged, {
|
||||||
|
detail: 'TIMEOUT',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// Otherwise check the time between was >= pingIntervalMs,
|
||||||
|
// and if it was, then it's bad network health.
|
||||||
|
} else if (this.pingPongSpan.ping && this.pingPongSpan.pong) {
|
||||||
|
if (
|
||||||
|
Math.abs(
|
||||||
|
this.pingPongSpan.pong.valueOf() -
|
||||||
|
this.pingPongSpan.ping.valueOf()
|
||||||
|
) >= pingIntervalMs
|
||||||
|
) {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EngineConnectionEvents.PingPongChanged, {
|
||||||
|
detail: 'TIMEOUT',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EngineConnectionEvents.PingPongChanged, {
|
||||||
|
detail: 'OK',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.send({ type: 'ping' })
|
this.send({ type: 'ping' })
|
||||||
|
this.pingPongSpan.ping = new Date()
|
||||||
|
this.pingPongSpan.pong = undefined
|
||||||
break
|
break
|
||||||
case EngineConnectionStateType.Disconnecting:
|
case EngineConnectionStateType.Disconnecting:
|
||||||
case EngineConnectionStateType.Disconnected:
|
case EngineConnectionStateType.Disconnected:
|
||||||
clearInterval(pingInterval)
|
// Reconnect if we have disconnected.
|
||||||
|
if (!this.isConnecting()) this.connect()
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
|
if (this.isConnecting()) break
|
||||||
|
// Means we never could do an initial connection. Reconnect everything.
|
||||||
|
if (!this.pingPongSpan.ping) this.connect()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, pingIntervalMs)
|
}, pingIntervalMs)
|
||||||
|
|
||||||
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
|
||||||
let connectRetryInterval = setInterval(() => {
|
|
||||||
if (this.state.type !== EngineConnectionStateType.Disconnected) return
|
|
||||||
|
|
||||||
// Only try reconnecting when completely disconnected.
|
|
||||||
clearInterval(connectRetryInterval)
|
|
||||||
console.log('Trying to reconnect')
|
|
||||||
this.connect()
|
|
||||||
}, connectionTimeoutMs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isConnecting() {
|
isConnecting() {
|
||||||
@ -362,13 +423,18 @@ class EngineConnection {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Information on the connect transaction
|
|
||||||
|
|
||||||
const createPeerConnection = () => {
|
const createPeerConnection = () => {
|
||||||
this.pc = new RTCPeerConnection({
|
this.pc = new RTCPeerConnection({
|
||||||
bundlePolicy: 'max-bundle',
|
bundlePolicy: 'max-bundle',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Other parts of the application expect pc to be initialized when firing.
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EngineConnectionEvents.ConnectionStarted, {
|
||||||
|
detail: this,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
// Data channels MUST BE specified before SDP offers because requesting
|
// Data channels MUST BE specified before SDP offers because requesting
|
||||||
// them affects what our needs are!
|
// them affects what our needs are!
|
||||||
const DATACHANNEL_NAME_UMC = 'unreliable_modeling_cmds'
|
const DATACHANNEL_NAME_UMC = 'unreliable_modeling_cmds'
|
||||||
@ -422,7 +488,11 @@ class EngineConnection {
|
|||||||
// dance is it safest to connect the video tracks / stream
|
// dance is it safest to connect the video tracks / stream
|
||||||
case 'connected':
|
case 'connected':
|
||||||
// Let the browser attach to the video stream now
|
// Let the browser attach to the video stream now
|
||||||
this.onNewTrack({ conn: this, mediaStream: this.mediaStream! })
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EngineConnectionEvents.NewTrack, {
|
||||||
|
detail: { conn: this, mediaStream: this.mediaStream! },
|
||||||
|
})
|
||||||
|
)
|
||||||
break
|
break
|
||||||
case 'failed':
|
case 'failed':
|
||||||
this.disconnectAll()
|
this.disconnectAll()
|
||||||
@ -431,9 +501,8 @@ class EngineConnection {
|
|||||||
value: {
|
value: {
|
||||||
type: DisconnectingType.Error,
|
type: DisconnectingType.Error,
|
||||||
value: {
|
value: {
|
||||||
error: new Error(
|
error: ConnectionError.ICENegotiate,
|
||||||
'failed to negotiate ice connection; restarting'
|
context: event,
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -552,7 +621,10 @@ class EngineConnection {
|
|||||||
this.state = { type: EngineConnectionStateType.ConnectionEstablished }
|
this.state = { type: EngineConnectionStateType.ConnectionEstablished }
|
||||||
|
|
||||||
this.engineCommandManager.inSequence = 1
|
this.engineCommandManager.inSequence = 1
|
||||||
this.onEngineConnectionOpen(this)
|
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent(EngineConnectionEvents.Opened, { detail: this })
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.unreliableDataChannel.addEventListener('close', (event) => {
|
this.unreliableDataChannel.addEventListener('close', (event) => {
|
||||||
@ -568,7 +640,8 @@ class EngineConnection {
|
|||||||
value: {
|
value: {
|
||||||
type: DisconnectingType.Error,
|
type: DisconnectingType.Error,
|
||||||
value: {
|
value: {
|
||||||
error: new Error(event.toString()),
|
error: ConnectionError.DataChannelError,
|
||||||
|
context: event,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -606,6 +679,7 @@ class EngineConnection {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createWebSocketConnection = () => {
|
||||||
this.websocket = new WebSocket(this.url, [])
|
this.websocket = new WebSocket(this.url, [])
|
||||||
this.websocket.binaryType = 'arraybuffer'
|
this.websocket.binaryType = 'arraybuffer'
|
||||||
|
|
||||||
@ -613,7 +687,7 @@ class EngineConnection {
|
|||||||
this.state = {
|
this.state = {
|
||||||
type: EngineConnectionStateType.Connecting,
|
type: EngineConnectionStateType.Connecting,
|
||||||
value: {
|
value: {
|
||||||
type: ConnectingType.WebSocketEstablished,
|
type: ConnectingType.WebSocketOpen,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -626,6 +700,10 @@ class EngineConnection {
|
|||||||
headers: { Authorization: `Bearer ${this.token}` },
|
headers: { Authorization: `Bearer ${this.token}` },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send an initial ping
|
||||||
|
this.send({ type: 'ping' })
|
||||||
|
this.pingPongSpan.ping = new Date()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.websocket.addEventListener('close', (event) => {
|
this.websocket.addEventListener('close', (event) => {
|
||||||
@ -641,7 +719,8 @@ class EngineConnection {
|
|||||||
value: {
|
value: {
|
||||||
type: DisconnectingType.Error,
|
type: DisconnectingType.Error,
|
||||||
value: {
|
value: {
|
||||||
error: new Error(event.toString()),
|
error: ConnectionError.WebSocketError,
|
||||||
|
context: event,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -655,6 +734,8 @@ class EngineConnection {
|
|||||||
// when assuming we're the only consumer or that all messages will
|
// when assuming we're the only consumer or that all messages will
|
||||||
// be carefully formatted here.
|
// be carefully formatted here.
|
||||||
|
|
||||||
|
console.log(event)
|
||||||
|
|
||||||
if (typeof event.data !== 'string') {
|
if (typeof event.data !== 'string') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -675,21 +756,39 @@ class EngineConnection {
|
|||||||
`Error in response to request ${message.request_id}:\n${errorsString}
|
`Error in response to request ${message.request_id}:\n${errorsString}
|
||||||
failed cmd type was ${artifactThatFailed?.commandType}`
|
failed cmd type was ${artifactThatFailed?.commandType}`
|
||||||
)
|
)
|
||||||
|
console.log(artifactThatFailed)
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error from server:\n${errorsString}`)
|
console.error(`Error from server:\n${errorsString}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const firstError = message?.errors[0]
|
||||||
|
if (firstError.error_code === 'auth_token_invalid') {
|
||||||
|
this.state = {
|
||||||
|
type: EngineConnectionStateType.Disconnecting,
|
||||||
|
value: {
|
||||||
|
type: DisconnectingType.Error,
|
||||||
|
value: {
|
||||||
|
error: ConnectionError.BadAuthToken,
|
||||||
|
context: firstError.message,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
this.disconnectAll()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp = message.resp
|
let resp = message.resp
|
||||||
|
|
||||||
// If there's no body to the response, we can bail here.
|
// If there's no body to the response, we can bail here.
|
||||||
// !resp.type is usually "pong" response for our "ping"
|
|
||||||
if (!resp || !resp.type) {
|
if (!resp || !resp.type) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (resp.type) {
|
switch (resp.type) {
|
||||||
|
case 'pong':
|
||||||
|
this.pingPongSpan.pong = new Date()
|
||||||
|
break
|
||||||
case 'ice_server_info':
|
case 'ice_server_info':
|
||||||
let ice_servers = resp.data?.ice_servers
|
let ice_servers = resp.data?.ice_servers
|
||||||
|
|
||||||
@ -758,10 +857,7 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
|||||||
return this.pc?.setLocalDescription(offer).then(() => {
|
return this.pc?.setLocalDescription(offer).then(() => {
|
||||||
this.send({
|
this.send({
|
||||||
type: 'sdp_offer',
|
type: 'sdp_offer',
|
||||||
offer: {
|
offer: offer as Models['RtcSessionDescription_type'],
|
||||||
sdp: offer.sdp || '',
|
|
||||||
type: offer.type,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
this.state = {
|
this.state = {
|
||||||
type: EngineConnectionStateType.Connecting,
|
type: EngineConnectionStateType.Connecting,
|
||||||
@ -771,8 +867,7 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((err: Error) => {
|
||||||
console.error(error)
|
|
||||||
// The local description is invalid, so there's no point continuing.
|
// The local description is invalid, so there's no point continuing.
|
||||||
this.disconnectAll()
|
this.disconnectAll()
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -780,7 +875,8 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
|||||||
value: {
|
value: {
|
||||||
type: DisconnectingType.Error,
|
type: DisconnectingType.Error,
|
||||||
value: {
|
value: {
|
||||||
error,
|
error: ConnectionError.LocalDescriptionInvalid,
|
||||||
|
context: err,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -841,28 +937,9 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
|
createWebSocketConnection()
|
||||||
if (this.failedConnTimeout) {
|
|
||||||
clearTimeout(this.failedConnTimeout)
|
|
||||||
this.failedConnTimeout = null
|
|
||||||
}
|
|
||||||
this.failedConnTimeout = setTimeout(() => {
|
|
||||||
if (this.isReady()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.failedConnTimeout = null
|
|
||||||
this.state = {
|
|
||||||
type: EngineConnectionStateType.Disconnecting,
|
|
||||||
value: {
|
|
||||||
type: DisconnectingType.Timeout,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
this.disconnectAll()
|
|
||||||
this.finalizeIfAllConnectionsClosed()
|
|
||||||
}, connectionTimeoutMs)
|
|
||||||
|
|
||||||
this.onConnectionStarted(this)
|
|
||||||
}
|
}
|
||||||
// Do not change this back to an object or any, we should only be sending the
|
// Do not change this back to an object or any, we should only be sending the
|
||||||
// WebSocketRequest type!
|
// WebSocketRequest type!
|
||||||
@ -914,6 +991,8 @@ export interface UnreliableSubscription<T extends UnreliableResponses['type']> {
|
|||||||
callback: (data: Extract<UnreliableResponses, { type: T }>) => void
|
callback: (data: Extract<UnreliableResponses, { type: T }>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Should eventually be replaced with native EventTarget event system,
|
||||||
|
// as it manages events in a more familiar way to other developers.
|
||||||
export interface Subscription<T extends ModelTypes> {
|
export interface Subscription<T extends ModelTypes> {
|
||||||
event: T
|
event: T
|
||||||
callback: (
|
callback: (
|
||||||
@ -941,6 +1020,10 @@ export type CommandLog =
|
|||||||
data: null
|
data: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum EngineCommandManagerEvents {
|
||||||
|
EngineAvailable = 'engine-available',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The EngineCommandManager is the main interface to the Engine for Modeling App.
|
* The EngineCommandManager is the main interface to the Engine for Modeling App.
|
||||||
*
|
*
|
||||||
@ -951,7 +1034,7 @@ export type CommandLog =
|
|||||||
* It also maintains an {@link artifactMap} that keeps track of the state of each
|
* It also maintains an {@link artifactMap} that keeps track of the state of each
|
||||||
* command, and the artifacts that have been generated by those commands.
|
* command, and the artifacts that have been generated by those commands.
|
||||||
*/
|
*/
|
||||||
export class EngineCommandManager {
|
export class EngineCommandManager extends EventTarget {
|
||||||
/**
|
/**
|
||||||
* The artifactMap is a client-side representation of the commands that have been sent
|
* The artifactMap is a client-side representation of the commands that have been sent
|
||||||
* to the server-side geometry engine, and the state of their resulting artifacts.
|
* to the server-side geometry engine, and the state of their resulting artifacts.
|
||||||
@ -1020,10 +1103,9 @@ export class EngineCommandManager {
|
|||||||
}
|
}
|
||||||
} = {} as any
|
} = {} as any
|
||||||
|
|
||||||
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
|
|
||||||
[]
|
|
||||||
|
|
||||||
constructor(pool?: string) {
|
constructor(pool?: string) {
|
||||||
|
super()
|
||||||
|
|
||||||
this.engineConnection = undefined
|
this.engineConnection = undefined
|
||||||
this.pool = pool
|
this.pool = pool
|
||||||
}
|
}
|
||||||
@ -1088,14 +1170,22 @@ export class EngineCommandManager {
|
|||||||
engineCommandManager: this,
|
engineCommandManager: this,
|
||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
onConnectionStateChange: (state: EngineConnectionState) => {
|
})
|
||||||
for (let cb of this.callbacksEngineStateConnection) {
|
|
||||||
cb(state)
|
this.dispatchEvent(
|
||||||
}
|
new CustomEvent(EngineCommandManagerEvents.EngineAvailable, {
|
||||||
},
|
detail: this.engineConnection,
|
||||||
onEngineConnectionOpen: () => {
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
this.engineConnection.addEventListener(
|
||||||
|
EngineConnectionEvents.Opened,
|
||||||
|
() => {
|
||||||
// Set the stream background color
|
// Set the stream background color
|
||||||
this.sendSceneCommand({
|
// This takes RGBA values from 0-1
|
||||||
|
// So we convert from the conventional 0-255 found in Figma
|
||||||
|
|
||||||
|
void this.sendSceneCommand({
|
||||||
type: 'modeling_cmd_req',
|
type: 'modeling_cmd_req',
|
||||||
cmd_id: uuidv4(),
|
cmd_id: uuidv4(),
|
||||||
cmd: {
|
cmd: {
|
||||||
@ -1125,6 +1215,24 @@ export class EngineCommandManager {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Make the axis gizmo.
|
||||||
|
// We do this after the connection opened to avoid a race condition.
|
||||||
|
// Connected opened is the last thing that happens when the stream
|
||||||
|
// is ready.
|
||||||
|
// We also do this here because we want to ensure we create the gizmo
|
||||||
|
// and execute the code everytime the stream is restarted.
|
||||||
|
const gizmoId = uuidv4()
|
||||||
|
void this.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd_id: gizmoId,
|
||||||
|
cmd: {
|
||||||
|
type: 'make_axes_gizmo',
|
||||||
|
clobber: false,
|
||||||
|
// If true, axes gizmo will be placed in the corner of the screen.
|
||||||
|
// If false, it will be placed at the origin of the scene.
|
||||||
|
gizmo_mode: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
this._camControlsCameraChange()
|
this._camControlsCameraChange()
|
||||||
this.sendSceneCommand({
|
this.sendSceneCommand({
|
||||||
// CameraControls subscribes to default_camera_get_settings response events
|
// CameraControls subscribes to default_camera_get_settings response events
|
||||||
@ -1141,14 +1249,58 @@ export class EngineCommandManager {
|
|||||||
setIsStreamReady(true)
|
setIsStreamReady(true)
|
||||||
await executeCode()
|
await executeCode()
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
onClose: () => {
|
)
|
||||||
|
|
||||||
|
this.engineConnection.addEventListener(
|
||||||
|
EngineConnectionEvents.Closed,
|
||||||
|
() => {
|
||||||
setIsStreamReady(false)
|
setIsStreamReady(false)
|
||||||
},
|
}
|
||||||
onConnectionStarted: (engineConnection) => {
|
)
|
||||||
|
|
||||||
|
this.engineConnection.addEventListener(
|
||||||
|
EngineConnectionEvents.ConnectionStarted,
|
||||||
|
({ detail: engineConnection }: any) => {
|
||||||
|
engineConnection?.pc?.addEventListener(
|
||||||
|
'datachannel',
|
||||||
|
(event: RTCDataChannelEvent) => {
|
||||||
|
let unreliableDataChannel = event.channel
|
||||||
|
|
||||||
|
unreliableDataChannel.addEventListener(
|
||||||
|
'message',
|
||||||
|
(event: MessageEvent) => {
|
||||||
|
const result: UnreliableResponses = JSON.parse(event.data)
|
||||||
|
Object.values(
|
||||||
|
this.unreliableSubscriptions[result.type] || {}
|
||||||
|
).forEach(
|
||||||
|
// TODO: There is only one response that uses the unreliable channel atm,
|
||||||
|
// highlight_set_entity, if there are more it's likely they will all have the same
|
||||||
|
// sequence logic, but I'm not sure if we use a single global sequence or a sequence
|
||||||
|
// per unreliable subscription.
|
||||||
|
(callback) => {
|
||||||
|
let data = result?.data
|
||||||
|
if (isHighlightSetEntity_type(data)) {
|
||||||
|
if (
|
||||||
|
data.sequence !== undefined &&
|
||||||
|
data.sequence > this.inSequence
|
||||||
|
) {
|
||||||
|
this.inSequence = data.sequence
|
||||||
|
callback(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// When the EngineConnection starts a connection, we want to register
|
// When the EngineConnection starts a connection, we want to register
|
||||||
// callbacks into the WebSocket/PeerConnection.
|
// callbacks into the WebSocket/PeerConnection.
|
||||||
engineConnection.websocket?.addEventListener('message', (event) => {
|
engineConnection.websocket?.addEventListener('message', ((
|
||||||
|
event: MessageEvent
|
||||||
|
) => {
|
||||||
if (event.data instanceof ArrayBuffer) {
|
if (event.data instanceof ArrayBuffer) {
|
||||||
// If the data is an ArrayBuffer, it's the result of an export command,
|
// If the data is an ArrayBuffer, it's the result of an export command,
|
||||||
// because in all other cases we send JSON strings. But in the case of
|
// because in all other cases we send JSON strings. But in the case of
|
||||||
@ -1179,23 +1331,36 @@ export class EngineCommandManager {
|
|||||||
this.handleFailedModelingCommand(message.request_id, message)
|
this.handleFailedModelingCommand(message.request_id, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}) as EventListener)
|
||||||
},
|
|
||||||
onNewTrack: ({ mediaStream }) => {
|
this.engineConnection?.addEventListener(
|
||||||
|
EngineConnectionEvents.NewTrack,
|
||||||
|
(({ detail: { mediaStream } }: CustomEvent<NewTrackArgs>) => {
|
||||||
console.log('received track', mediaStream)
|
console.log('received track', mediaStream)
|
||||||
|
|
||||||
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
|
mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
|
||||||
console.log('peer is not sending video to us')
|
if (this.engineConnection) {
|
||||||
// this.engineConnection?.close()
|
this.engineConnection.state = {
|
||||||
// this.engineConnection?.connect()
|
type: EngineConnectionStateType.Disconnecting,
|
||||||
|
value: {
|
||||||
|
type: DisconnectingType.Error,
|
||||||
|
value: {
|
||||||
|
error: ConnectionError.LostVideoStream,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setMediaStream(mediaStream)
|
setMediaStream(mediaStream)
|
||||||
},
|
}) as EventListener
|
||||||
})
|
)
|
||||||
|
|
||||||
this.engineConnection?.connect()
|
this.engineConnection?.connect()
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
handleResize({
|
handleResize({
|
||||||
streamWidth,
|
streamWidth,
|
||||||
streamHeight,
|
streamHeight,
|
||||||
@ -1443,9 +1608,6 @@ export class EngineCommandManager {
|
|||||||
) {
|
) {
|
||||||
delete this.unreliableSubscriptions[event][id]
|
delete this.unreliableSubscriptions[event][id]
|
||||||
}
|
}
|
||||||
onConnectionStateChange(callback: (state: EngineConnectionState) => void) {
|
|
||||||
this.callbacksEngineStateConnection.push(callback)
|
|
||||||
}
|
|
||||||
// We make this a separate function so we can call it from wasm.
|
// We make this a separate function so we can call it from wasm.
|
||||||
clearDefaultPlanes() {
|
clearDefaultPlanes() {
|
||||||
this.defaultPlanes = null
|
this.defaultPlanes = null
|
||||||
|
@ -318,7 +318,6 @@ function resetAndSetEngineEntitySelectionCmds(
|
|||||||
selections: SelectionToEngine[]
|
selections: SelectionToEngine[]
|
||||||
): Models['WebSocketRequest_type'][] {
|
): Models['WebSocketRequest_type'][] {
|
||||||
if (!engineCommandManager.engineConnection?.isReady()) {
|
if (!engineCommandManager.engineConnection?.isReady()) {
|
||||||
console.log('engine connection is not ready')
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
|
39
yarn.lock
@ -1880,10 +1880,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
|
||||||
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
|
||||||
|
|
||||||
"@kittycad/lib@^0.0.63":
|
"@kittycad/lib@^0.0.64":
|
||||||
version "0.0.63"
|
version "0.0.64"
|
||||||
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.63.tgz#cc70cf1c0780543bbca6f55aae40d0904cfd45d7"
|
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.64.tgz#0cea0788cd8af4a8964ddbf7152028affadcb17f"
|
||||||
integrity sha512-fDpGnycumT1xI/tSubRZzU9809/7s+m06w2EuJzxowgFrdIlvThnIHVf3EYvSujdFb0bHR/LZjodAw2ocXkXZw==
|
integrity sha512-qHyvNYKbhsfR5aXLFrdKrBQ4JI+0G0v096oROD3HatJ+AIzg5H0THmI+rMnQ9L4zx4U6n1A9gLi7ZQjSsZsleg==
|
||||||
dependencies:
|
dependencies:
|
||||||
node-fetch "3.3.2"
|
node-fetch "3.3.2"
|
||||||
openapi-types "^12.0.0"
|
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"
|
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0":
|
"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==
|
|
||||||
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:
|
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@ -8316,14 +8307,7 @@ string_decoder@~1.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "~5.1.0"
|
safe-buffer "~5.1.0"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm: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==
|
|
||||||
dependencies:
|
|
||||||
ansi-regex "^5.0.1"
|
|
||||||
|
|
||||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
@ -9305,7 +9289,7 @@ workerpool@6.2.1:
|
|||||||
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
|
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
|
||||||
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
|
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"
|
version "7.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||||
@ -9323,15 +9307,6 @@ wrap-ansi@^6.2.0:
|
|||||||
string-width "^4.1.0"
|
string-width "^4.1.0"
|
||||||
strip-ansi "^6.0.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:
|
wrap-ansi@^8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||||
|