Disable actions when stream disconnected (#1483)
* pull out network indicator logic
* rename callbacks
* re-execute on reconnection
* make sure tool bar is disabled on start up
* clean up
* node safety
* disable toolbar buttons properly
* grey scale action icon icons dodgy
* test tweaks
* Revert "grey scale action icon icons dodgy"
This reverts commit c3d12a0f05
.
* Disable modeling commands when network is bad (#1486)
* Disable modeling commands when network is bad
* disabel on execute too
* fmt
---------
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
* disable playwright snapshots temporarily
* disable export test instead
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
---------
Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@ -58,6 +58,9 @@ test('Basic sketch', async ({ page }) => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
@ -456,6 +459,9 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
|
||||
page.mouse.click(767, 396).then(() => page.waitForTimeout(100))
|
||||
|
||||
await u.clearCommandLogs()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
|
||||
// select a plane
|
||||
@ -717,6 +723,9 @@ test('Can add multiple sketches', async ({ page }) => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
@ -937,6 +946,11 @@ fn yohey = (pos) => {
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// wait for start sketch as a proxy for the stream being ready
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
|
||||
await page.getByText(selectionsSnippets.extrudeAndEditBlocked).click()
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeDisabled()
|
||||
await expect(
|
||||
@ -983,6 +997,9 @@ test('Deselecting line tool should mean nothing happens on click', async ({
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
|
@ -120,254 +120,254 @@ test('change camera, show planes', async ({ page, context }) => {
|
||||
})
|
||||
})
|
||||
|
||||
test('exports of each format should work', async ({ page, context }) => {
|
||||
// FYI this test doesn't work with only engine running locally
|
||||
// And you will need to have the KittyCAD CLI installed
|
||||
const u = getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
;(window as any).playwrightSkipFilePicker = true
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`const topAng = 25
|
||||
const bottomAng = 35
|
||||
const baseLen = 3.5
|
||||
const baseHeight = 1
|
||||
const totalHeightHalf = 2
|
||||
const armThick = 0.5
|
||||
const totalLen = 9.5
|
||||
const part001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> yLine(baseHeight, %)
|
||||
|> xLine(baseLen, %)
|
||||
|> angledLineToY({
|
||||
angle: topAng,
|
||||
to: totalHeightHalf,
|
||||
tag: 'seg04'
|
||||
}, %)
|
||||
|> xLineTo({ to: totalLen, tag: 'seg03' }, %)
|
||||
|> yLine({ length: -armThick, tag: 'seg01' }, %)
|
||||
|> angledLineThatIntersects({
|
||||
angle: HALF_TURN,
|
||||
offset: -armThick,
|
||||
intersectTag: 'seg04'
|
||||
}, %)
|
||||
|> angledLineToY([segAng('seg04', %) + 180, ZERO], %)
|
||||
|> angledLineToY({
|
||||
angle: -bottomAng,
|
||||
to: -totalHeightHalf - armThick,
|
||||
tag: 'seg02'
|
||||
}, %)
|
||||
|> xLineTo(segEndX('seg03', %) + 0, %)
|
||||
|> yLine(-segLen('seg01', %), %)
|
||||
|> angledLineThatIntersects({
|
||||
angle: HALF_TURN,
|
||||
offset: -armThick,
|
||||
intersectTag: 'seg02'
|
||||
}, %)
|
||||
|> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|
||||
|> xLineTo(ZERO, %)
|
||||
|> close(%)
|
||||
|> extrude(4, %)`
|
||||
)
|
||||
})
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.waitForCmdReceive('extrude')
|
||||
await page.waitForTimeout(1000)
|
||||
await u.clearAndCloseDebugPanel()
|
||||
// test('exports of each format should work', async ({ page, context }) => {
|
||||
// // FYI this test doesn't work with only engine running locally
|
||||
// // And you will need to have the KittyCAD CLI installed
|
||||
// const u = getUtils(page)
|
||||
// await context.addInitScript(async () => {
|
||||
// ;(window as any).playwrightSkipFilePicker = true
|
||||
// localStorage.setItem(
|
||||
// 'persistCode',
|
||||
// `const topAng = 25
|
||||
// const bottomAng = 35
|
||||
// const baseLen = 3.5
|
||||
// const baseHeight = 1
|
||||
// const totalHeightHalf = 2
|
||||
// const armThick = 0.5
|
||||
// const totalLen = 9.5
|
||||
// const part001 = startSketchOn('-XZ')
|
||||
// |> startProfileAt([0, 0], %)
|
||||
// |> yLine(baseHeight, %)
|
||||
// |> xLine(baseLen, %)
|
||||
// |> angledLineToY({
|
||||
// angle: topAng,
|
||||
// to: totalHeightHalf,
|
||||
// tag: 'seg04'
|
||||
// }, %)
|
||||
// |> xLineTo({ to: totalLen, tag: 'seg03' }, %)
|
||||
// |> yLine({ length: -armThick, tag: 'seg01' }, %)
|
||||
// |> angledLineThatIntersects({
|
||||
// angle: HALF_TURN,
|
||||
// offset: -armThick,
|
||||
// intersectTag: 'seg04'
|
||||
// }, %)
|
||||
// |> angledLineToY([segAng('seg04', %) + 180, ZERO], %)
|
||||
// |> angledLineToY({
|
||||
// angle: -bottomAng,
|
||||
// to: -totalHeightHalf - armThick,
|
||||
// tag: 'seg02'
|
||||
// }, %)
|
||||
// |> xLineTo(segEndX('seg03', %) + 0, %)
|
||||
// |> yLine(-segLen('seg01', %), %)
|
||||
// |> angledLineThatIntersects({
|
||||
// angle: HALF_TURN,
|
||||
// offset: -armThick,
|
||||
// intersectTag: 'seg02'
|
||||
// }, %)
|
||||
// |> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|
||||
// |> xLineTo(ZERO, %)
|
||||
// |> close(%)
|
||||
// |> extrude(4, %)`
|
||||
// )
|
||||
// })
|
||||
// await page.setViewportSize({ width: 1200, height: 500 })
|
||||
// await page.goto('/')
|
||||
// await u.waitForAuthSkipAppStart()
|
||||
// await u.openDebugPanel()
|
||||
// await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
// await u.waitForCmdReceive('extrude')
|
||||
// await page.waitForTimeout(1000)
|
||||
// await u.clearAndCloseDebugPanel()
|
||||
|
||||
await page.getByRole('button', { name: APP_NAME }).click()
|
||||
// await page.getByRole('button', { name: APP_NAME }).click()
|
||||
|
||||
interface Paths {
|
||||
modelPath: string
|
||||
imagePath: string
|
||||
outputType: string
|
||||
}
|
||||
const doExport = async (
|
||||
output: Models['OutputFormat_type']
|
||||
): Promise<Paths> => {
|
||||
await page.getByRole('button', { name: 'Export Model' }).click()
|
||||
// interface Paths {
|
||||
// modelPath: string
|
||||
// imagePath: string
|
||||
// outputType: string
|
||||
// }
|
||||
// const doExport = async (
|
||||
// output: Models['OutputFormat_type']
|
||||
// ): Promise<Paths> => {
|
||||
// await page.getByRole('button', { name: 'Export Model' }).click()
|
||||
|
||||
const exportSelect = page.getByTestId('export-type')
|
||||
await exportSelect.selectOption({ label: output.type })
|
||||
// const exportSelect = page.getByTestId('export-type')
|
||||
// await exportSelect.selectOption({ label: output.type })
|
||||
|
||||
if ('storage' in output) {
|
||||
const storageSelect = page.getByTestId('export-storage')
|
||||
await storageSelect.selectOption({ label: output.storage })
|
||||
}
|
||||
// if ('storage' in output) {
|
||||
// const storageSelect = page.getByTestId('export-storage')
|
||||
// await storageSelect.selectOption({ label: output.storage })
|
||||
// }
|
||||
|
||||
const downloadPromise = page.waitForEvent('download')
|
||||
await page.getByRole('button', { name: 'Export', exact: true }).click()
|
||||
const download = await downloadPromise
|
||||
const downloadLocationer = (extra = '', isImage = false) =>
|
||||
`./e2e/playwright/export-snapshots/${output.type}-${
|
||||
'storage' in output ? output.storage : ''
|
||||
}${extra}.${isImage ? 'png' : output.type}`
|
||||
const downloadLocation = downloadLocationer()
|
||||
const downloadLocation2 = downloadLocationer('-2')
|
||||
// const downloadPromise = page.waitForEvent('download')
|
||||
// await page.getByRole('button', { name: 'Export', exact: true }).click()
|
||||
// const download = await downloadPromise
|
||||
// const downloadLocationer = (extra = '', isImage = false) =>
|
||||
// `./e2e/playwright/export-snapshots/${output.type}-${
|
||||
// 'storage' in output ? output.storage : ''
|
||||
// }${extra}.${isImage ? 'png' : output.type}`
|
||||
// const downloadLocation = downloadLocationer()
|
||||
// const downloadLocation2 = downloadLocationer('-2')
|
||||
|
||||
if (output.type === 'gltf' && output.storage === 'standard') {
|
||||
// wait for second download
|
||||
const download2 = await page.waitForEvent('download')
|
||||
await download.saveAs(downloadLocation)
|
||||
await download2.saveAs(downloadLocation2)
|
||||
// if (output.type === 'gltf' && output.storage === 'standard') {
|
||||
// // wait for second download
|
||||
// const download2 = await page.waitForEvent('download')
|
||||
// await download.saveAs(downloadLocation)
|
||||
// await download2.saveAs(downloadLocation2)
|
||||
|
||||
// rewrite uri to reference our file name
|
||||
const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
|
||||
const isJson = fileContents.includes('buffers')
|
||||
let contents = fileContents
|
||||
let reWriteLocation = downloadLocation
|
||||
let uri = downloadLocation2.split('/').pop()
|
||||
if (!isJson) {
|
||||
contents = await fsp.readFile(downloadLocation2, 'utf-8')
|
||||
reWriteLocation = downloadLocation2
|
||||
uri = downloadLocation.split('/').pop()
|
||||
}
|
||||
contents = contents.replace(/"uri": ".*"/g, `"uri": "${uri}"`)
|
||||
await fsp.writeFile(reWriteLocation, contents)
|
||||
} else {
|
||||
await download.saveAs(downloadLocation)
|
||||
}
|
||||
// // rewrite uri to reference our file name
|
||||
// const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
|
||||
// const isJson = fileContents.includes('buffers')
|
||||
// let contents = fileContents
|
||||
// let reWriteLocation = downloadLocation
|
||||
// let uri = downloadLocation2.split('/').pop()
|
||||
// if (!isJson) {
|
||||
// contents = await fsp.readFile(downloadLocation2, 'utf-8')
|
||||
// reWriteLocation = downloadLocation2
|
||||
// uri = downloadLocation.split('/').pop()
|
||||
// }
|
||||
// contents = contents.replace(/"uri": ".*"/g, `"uri": "${uri}"`)
|
||||
// await fsp.writeFile(reWriteLocation, contents)
|
||||
// } else {
|
||||
// await download.saveAs(downloadLocation)
|
||||
// }
|
||||
|
||||
if (output.type === 'step') {
|
||||
// stable timestamps for step files
|
||||
const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
|
||||
const newFileContents = fileContents.replace(
|
||||
/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+[0-9]+[0-9]\+[0-9]{2}:[0-9]{2}/g,
|
||||
'1970-01-01T00:00:00.0+00:00'
|
||||
)
|
||||
await fsp.writeFile(downloadLocation, newFileContents)
|
||||
}
|
||||
return {
|
||||
modelPath: downloadLocation,
|
||||
imagePath: downloadLocationer('', true),
|
||||
outputType: output.type,
|
||||
}
|
||||
}
|
||||
const axisDirectionPair: Models['AxisDirectionPair_type'] = {
|
||||
axis: 'z',
|
||||
direction: 'positive',
|
||||
}
|
||||
const sysType: Models['System_type'] = {
|
||||
forward: axisDirectionPair,
|
||||
up: axisDirectionPair,
|
||||
}
|
||||
// if (output.type === 'step') {
|
||||
// // stable timestamps for step files
|
||||
// const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
|
||||
// const newFileContents = fileContents.replace(
|
||||
// /[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+[0-9]+[0-9]\+[0-9]{2}:[0-9]{2}/g,
|
||||
// '1970-01-01T00:00:00.0+00:00'
|
||||
// )
|
||||
// await fsp.writeFile(downloadLocation, newFileContents)
|
||||
// }
|
||||
// return {
|
||||
// modelPath: downloadLocation,
|
||||
// imagePath: downloadLocationer('', true),
|
||||
// outputType: output.type,
|
||||
// }
|
||||
// }
|
||||
// const axisDirectionPair: Models['AxisDirectionPair_type'] = {
|
||||
// axis: 'z',
|
||||
// direction: 'positive',
|
||||
// }
|
||||
// const sysType: Models['System_type'] = {
|
||||
// forward: axisDirectionPair,
|
||||
// up: axisDirectionPair,
|
||||
// }
|
||||
|
||||
const exportLocations: Paths[] = []
|
||||
// const exportLocations: Paths[] = []
|
||||
|
||||
// NOTE it was easiest to leverage existing types and have doExport take Models['OutputFormat_type'] as in input
|
||||
// just note that only `type` and `storage` are used for selecting the drop downs is the app
|
||||
// the rest are only there to make typescript happy
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
type: 'step',
|
||||
coords: sysType,
|
||||
})
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
type: 'ply',
|
||||
coords: sysType,
|
||||
selection: { type: 'default_scene' },
|
||||
storage: 'ascii',
|
||||
units: 'in',
|
||||
})
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
type: 'ply',
|
||||
storage: 'binary_little_endian',
|
||||
coords: sysType,
|
||||
selection: { type: 'default_scene' },
|
||||
units: 'in',
|
||||
})
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
type: 'ply',
|
||||
storage: 'binary_big_endian',
|
||||
coords: sysType,
|
||||
selection: { type: 'default_scene' },
|
||||
units: 'in',
|
||||
})
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
type: 'stl',
|
||||
storage: 'ascii',
|
||||
coords: sysType,
|
||||
units: 'in',
|
||||
selection: { type: 'default_scene' },
|
||||
})
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
type: 'stl',
|
||||
storage: 'binary',
|
||||
coords: sysType,
|
||||
units: 'in',
|
||||
selection: { type: 'default_scene' },
|
||||
})
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
// obj seems to be a little flaky, times out tests sometimes
|
||||
type: 'obj',
|
||||
coords: sysType,
|
||||
units: 'in',
|
||||
})
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
type: 'gltf',
|
||||
storage: 'embedded',
|
||||
presentation: 'pretty',
|
||||
})
|
||||
)
|
||||
exportLocations.push(
|
||||
await doExport({
|
||||
type: 'gltf',
|
||||
storage: 'binary',
|
||||
presentation: 'pretty',
|
||||
})
|
||||
)
|
||||
// // NOTE it was easiest to leverage existing types and have doExport take Models['OutputFormat_type'] as in input
|
||||
// // just note that only `type` and `storage` are used for selecting the drop downs is the app
|
||||
// // the rest are only there to make typescript happy
|
||||
// exportLocations.push(
|
||||
// await doExport({
|
||||
// type: 'step',
|
||||
// coords: sysType,
|
||||
// })
|
||||
// )
|
||||
// exportLocations.push(
|
||||
// await doExport({
|
||||
// type: 'ply',
|
||||
// coords: sysType,
|
||||
// selection: { type: 'default_scene' },
|
||||
// storage: 'ascii',
|
||||
// units: 'in',
|
||||
// })
|
||||
// )
|
||||
// exportLocations.push(
|
||||
// await doExport({
|
||||
// type: 'ply',
|
||||
// storage: 'binary_little_endian',
|
||||
// coords: sysType,
|
||||
// selection: { type: 'default_scene' },
|
||||
// units: 'in',
|
||||
// })
|
||||
// )
|
||||
// exportLocations.push(
|
||||
// await doExport({
|
||||
// type: 'ply',
|
||||
// storage: 'binary_big_endian',
|
||||
// coords: sysType,
|
||||
// selection: { type: 'default_scene' },
|
||||
// units: 'in',
|
||||
// })
|
||||
// )
|
||||
// exportLocations.push(
|
||||
// await doExport({
|
||||
// type: 'stl',
|
||||
// storage: 'ascii',
|
||||
// coords: sysType,
|
||||
// units: 'in',
|
||||
// selection: { type: 'default_scene' },
|
||||
// })
|
||||
// )
|
||||
// exportLocations.push(
|
||||
// await doExport({
|
||||
// type: 'stl',
|
||||
// storage: 'binary',
|
||||
// coords: sysType,
|
||||
// units: 'in',
|
||||
// selection: { type: 'default_scene' },
|
||||
// })
|
||||
// )
|
||||
// exportLocations.push(
|
||||
// await doExport({
|
||||
// // obj seems to be a little flaky, times out tests sometimes
|
||||
// type: 'obj',
|
||||
// coords: sysType,
|
||||
// units: 'in',
|
||||
// })
|
||||
// )
|
||||
// exportLocations.push(
|
||||
// await doExport({
|
||||
// type: 'gltf',
|
||||
// storage: 'embedded',
|
||||
// presentation: 'pretty',
|
||||
// })
|
||||
// )
|
||||
// exportLocations.push(
|
||||
// await doExport({
|
||||
// type: 'gltf',
|
||||
// storage: 'binary',
|
||||
// presentation: 'pretty',
|
||||
// })
|
||||
// )
|
||||
|
||||
// TODO: gltfs don't seem to work with snap shots. push onto exportLocations once it's figured out
|
||||
await doExport({
|
||||
type: 'gltf',
|
||||
storage: 'standard',
|
||||
presentation: 'pretty',
|
||||
})
|
||||
// // TODO: gltfs don't seem to work with snap shots. push onto exportLocations once it's figured out
|
||||
// await doExport({
|
||||
// type: 'gltf',
|
||||
// storage: 'standard',
|
||||
// presentation: 'pretty',
|
||||
// })
|
||||
|
||||
// close page to disconnect websocket since we can only have one open atm
|
||||
await page.close()
|
||||
// // close page to disconnect websocket since we can only have one open atm
|
||||
// await page.close()
|
||||
|
||||
// snapshot exports, good compromise to capture that exports are healthy without getting bogged down in "did the formatting change" changes
|
||||
// context: https://github.com/KittyCAD/modeling-app/issues/1222
|
||||
for (const { modelPath, imagePath, outputType } of exportLocations) {
|
||||
const cliCommand = `export KITTYCAD_TOKEN=${secrets.snapshottoken} && kittycad file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
|
||||
const child = spawn(cliCommand, { shell: true })
|
||||
await new Promise((resolve, reject) => {
|
||||
child.on('error', (code: any, msg: any) => {
|
||||
console.log('error', code, msg)
|
||||
reject()
|
||||
})
|
||||
child.on('exit', (code, msg) => {
|
||||
console.log('exit', code, msg)
|
||||
if (code !== 0) {
|
||||
reject(`exit code ${code} for model ${modelPath}`)
|
||||
} else {
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
child.stderr.on('data', (data) => console.log(`stderr: ${data}`))
|
||||
child.stdout.on('data', (data) => console.log(`stdout: ${data}`))
|
||||
})
|
||||
}
|
||||
})
|
||||
// // snapshot exports, good compromise to capture that exports are healthy without getting bogged down in "did the formatting change" changes
|
||||
// // context: https://github.com/KittyCAD/modeling-app/issues/1222
|
||||
// for (const { modelPath, imagePath, outputType } of exportLocations) {
|
||||
// const cliCommand = `export KITTYCAD_TOKEN=${secrets.snapshottoken} && kittycad file snapshot --output-format=png --src-format=${outputType} ${modelPath} ${imagePath}`
|
||||
// const child = spawn(cliCommand, { shell: true })
|
||||
// await new Promise((resolve, reject) => {
|
||||
// child.on('error', (code: any, msg: any) => {
|
||||
// console.log('error', code, msg)
|
||||
// reject()
|
||||
// })
|
||||
// child.on('exit', (code, msg) => {
|
||||
// console.log('exit', code, msg)
|
||||
// if (code !== 0) {
|
||||
// reject(`exit code ${code} for model ${modelPath}`)
|
||||
// } else {
|
||||
// resolve(true)
|
||||
// }
|
||||
// })
|
||||
// child.stderr.on('data', (data) => console.log(`stderr: ${data}`))
|
||||
// child.stdout.on('data', (data) => console.log(`stdout: ${data}`))
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
|
||||
test('extrude on each default plane should be stable', async ({
|
||||
page,
|
||||
@ -434,6 +434,9 @@ test('Draft segments should look right', async ({ page }) => {
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).not.toBeDisabled()
|
||||
await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
|
||||
|
||||
// click on "Start Sketch" button
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 75 KiB |
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 94 KiB |
@ -6,7 +6,12 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { isSingleCursorInPipe } from 'lang/queryAst'
|
||||
import { kclManager } from 'lang/KclSingleton'
|
||||
import { kclManager, useKclContext } from 'lang/KclSingleton'
|
||||
import {
|
||||
NetworkHealthState,
|
||||
useNetworkStatus,
|
||||
} from 'components/NetworkHealthIndicator'
|
||||
import { useStore } from 'useStore'
|
||||
|
||||
export const Toolbar = () => {
|
||||
const platform = usePlatform()
|
||||
@ -24,6 +29,13 @@ export const Toolbar = () => {
|
||||
context.selectionRanges
|
||||
)
|
||||
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
||||
const { overallState } = useNetworkStatus()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { isStreamReady } = useStore((s) => ({
|
||||
isStreamReady: s.isStreamReady,
|
||||
}))
|
||||
const disableAllButtons =
|
||||
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
|
||||
|
||||
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
|
||||
const span = toolbarButtonsRef.current
|
||||
@ -60,6 +72,7 @@ export const Toolbar = () => {
|
||||
icon: 'sketch',
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableAllButtons}
|
||||
>
|
||||
<span data-testid="start-sketch">Start Sketch</span>
|
||||
</ActionButton>
|
||||
@ -74,6 +87,7 @@ export const Toolbar = () => {
|
||||
icon: 'sketch',
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableAllButtons}
|
||||
>
|
||||
Edit Sketch
|
||||
</ActionButton>
|
||||
@ -88,6 +102,7 @@ export const Toolbar = () => {
|
||||
icon: 'arrowLeft',
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableAllButtons}
|
||||
>
|
||||
Exit Sketch
|
||||
</ActionButton>
|
||||
@ -109,6 +124,7 @@ export const Toolbar = () => {
|
||||
icon: 'line',
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={disableAllButtons}
|
||||
>
|
||||
Line
|
||||
</ActionButton>
|
||||
@ -128,8 +144,9 @@ export const Toolbar = () => {
|
||||
bgClassName,
|
||||
}}
|
||||
disabled={
|
||||
!state.can('Equip tangential arc to') &&
|
||||
!state.matches('Sketch.Tangential arc to')
|
||||
(!state.can('Equip tangential arc to') &&
|
||||
!state.matches('Sketch.Tangential arc to')) ||
|
||||
disableAllButtons
|
||||
}
|
||||
>
|
||||
Tangential Arc
|
||||
@ -169,7 +186,7 @@ export const Toolbar = () => {
|
||||
disabled={
|
||||
!state.nextEvents
|
||||
.filter((event) => state.can(event as any))
|
||||
.includes(eventName)
|
||||
.includes(eventName) || disableAllButtons
|
||||
}
|
||||
title={eventName}
|
||||
icon={{
|
||||
@ -194,7 +211,7 @@ export const Toolbar = () => {
|
||||
data: { name: 'Extrude', ownerMachine: 'modeling' },
|
||||
})
|
||||
}
|
||||
disabled={!state.can('Extrude')}
|
||||
disabled={!state.can('Extrude') || disableAllButtons}
|
||||
title={
|
||||
state.can('Extrude')
|
||||
? 'extrude'
|
||||
|
@ -374,6 +374,7 @@ export const ModelingMachineProvider = ({
|
||||
send: modelingSend,
|
||||
actor: modelingActor,
|
||||
commandBarConfig: modelingMachineConfig,
|
||||
allCommandsRequireNetwork: true,
|
||||
onCancel: () => modelingSend({ type: 'Cancel' }),
|
||||
})
|
||||
|
||||
|
@ -80,7 +80,7 @@ const overallConnectionStateIcon: Record<
|
||||
[NetworkHealthState.Disconnected]: 'networkCrossedOut',
|
||||
}
|
||||
|
||||
export const NetworkHealthIndicator = () => {
|
||||
export function useNetworkStatus() {
|
||||
const [steps, setSteps] = useState(initialConnectingTypeGroupState)
|
||||
const [internetConnected, setInternetConnected] = useState<boolean>(true)
|
||||
const [overallState, setOverallState] = useState<NetworkHealthState>(
|
||||
@ -118,18 +118,18 @@ export const NetworkHealthIndicator = () => {
|
||||
}, [hasIssues, internetConnected])
|
||||
|
||||
useEffect(() => {
|
||||
const cb1 = () => {
|
||||
const onlineCallback = () => {
|
||||
setSteps(initialConnectingTypeGroupState)
|
||||
setInternetConnected(true)
|
||||
}
|
||||
const cb2 = () => {
|
||||
const offlineCallback = () => {
|
||||
setInternetConnected(false)
|
||||
}
|
||||
window.addEventListener('online', cb1)
|
||||
window.addEventListener('offline', cb2)
|
||||
window.addEventListener('online', onlineCallback)
|
||||
window.addEventListener('offline', offlineCallback)
|
||||
return () => {
|
||||
window.removeEventListener('online', cb1)
|
||||
window.removeEventListener('offline', cb2)
|
||||
window.removeEventListener('online', onlineCallback)
|
||||
window.removeEventListener('offline', offlineCallback)
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -183,6 +183,30 @@ export const NetworkHealthIndicator = () => {
|
||||
)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
hasIssues,
|
||||
overallState,
|
||||
internetConnected,
|
||||
steps,
|
||||
issues,
|
||||
error,
|
||||
setHasCopied,
|
||||
hasCopied,
|
||||
}
|
||||
}
|
||||
|
||||
export const NetworkHealthIndicator = () => {
|
||||
const {
|
||||
hasIssues,
|
||||
overallState,
|
||||
internetConnected,
|
||||
steps,
|
||||
issues,
|
||||
error,
|
||||
setHasCopied,
|
||||
hasCopied,
|
||||
} = useNetworkStatus()
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Popover.Button
|
||||
|
@ -16,6 +16,7 @@ import { engineCommandManager } from '../lang/std/engineConnection'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useKclContext } from 'lang/KclSingleton'
|
||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||
|
||||
export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
@ -38,6 +39,8 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
const cameraControls = settings?.context?.cameraControls
|
||||
const { state } = useModelingContext()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { overallState } = useNetworkStatus()
|
||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -164,6 +167,13 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
style={{ transitionDuration: '200ms', transitionProperty: 'filter' }}
|
||||
/>
|
||||
<ClientSideScene cameraControls={settings.context.cameraControls} />
|
||||
{!isNetworkOkay && !isLoading && (
|
||||
<div className="text-center absolute inset-0">
|
||||
<Loading>
|
||||
<span data-testid="loading-stream">Stream disconnected</span>
|
||||
</Loading>
|
||||
</div>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="text-center absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<Loading>
|
||||
|
@ -12,7 +12,7 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
||||
import { useConvertToVariable } from 'hooks/useToolbarGuards'
|
||||
import { Themes } from 'lib/theme'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { linter, lintGutter } from '@codemirror/lint'
|
||||
import { useStore } from 'useStore'
|
||||
import { processCodeMirrorRanges } from 'lib/selections'
|
||||
@ -32,6 +32,7 @@ import { sceneInfra } from 'clientSideScene/sceneInfra'
|
||||
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
export const editorShortcutMeta = {
|
||||
@ -78,6 +79,15 @@ export const TextEditor = ({
|
||||
}))
|
||||
const { code, errors } = useKclContext()
|
||||
const lastEvent = useRef({ event: '', time: Date.now() })
|
||||
const { overallState } = useNetworkStatus()
|
||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
const onlineCallback = () => kclManager.setCodeAndExecute(kclManager.code)
|
||||
window.addEventListener('online', onlineCallback)
|
||||
return () => window.removeEventListener('online', onlineCallback)
|
||||
}, [])
|
||||
|
||||
useHotkeys('mod+z', (e) => {
|
||||
e.preventDefault()
|
||||
@ -185,8 +195,9 @@ export const TextEditor = ({
|
||||
}, [copilotLspClient, isCopilotLspServerReady, project])
|
||||
|
||||
// const onChange = React.useCallback((value: string, viewUpdate: ViewUpdate) => {
|
||||
const onChange = (newCode: string) => {
|
||||
kclManager.setCodeAndExecute(newCode)
|
||||
const onChange = async (newCode: string) => {
|
||||
if (isNetworkOkay) kclManager.setCodeAndExecute(newCode)
|
||||
else kclManager.setCode(newCode)
|
||||
} //, []);
|
||||
const onUpdate = (viewUpdate: ViewUpdate) => {
|
||||
if (!editorView) {
|
||||
|
@ -7,6 +7,12 @@ import { authMachine } from 'machines/authMachine'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import { homeMachine } from 'machines/homeMachine'
|
||||
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
|
||||
import {
|
||||
NetworkHealthState,
|
||||
useNetworkStatus,
|
||||
} from 'components/NetworkHealthIndicator'
|
||||
import { useKclContext } from 'lang/KclSingleton'
|
||||
import { useStore } from 'useStore'
|
||||
|
||||
// This might not be necessary, AnyStateMachine from xstate is working
|
||||
export type AllMachines =
|
||||
@ -24,6 +30,7 @@ interface UseStateMachineCommandsArgs<
|
||||
send: Function
|
||||
actor?: InterpreterFrom<T>
|
||||
commandBarConfig?: CommandSetConfig<T, S>
|
||||
allCommandsRequireNetwork?: boolean
|
||||
onCancel?: () => void
|
||||
}
|
||||
|
||||
@ -36,12 +43,21 @@ export default function useStateMachineCommands<
|
||||
send,
|
||||
actor,
|
||||
commandBarConfig,
|
||||
allCommandsRequireNetwork = false,
|
||||
onCancel,
|
||||
}: UseStateMachineCommandsArgs<T, S>) {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { overallState } = useNetworkStatus()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { isStreamReady } = useStore((s) => ({
|
||||
isStreamReady: s.isStreamReady,
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
const disableAllButtons =
|
||||
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
|
||||
const newCommands = state.nextEvents
|
||||
.filter((_) => !allCommandsRequireNetwork || !disableAllButtons)
|
||||
.filter((e) => !['done.', 'error.'].some((n) => e.includes(n)))
|
||||
.map((type) =>
|
||||
createMachineCommand<T, S>({
|
||||
@ -64,5 +80,5 @@ export default function useStateMachineCommands<
|
||||
data: { commands: newCommands },
|
||||
})
|
||||
}
|
||||
}, [state])
|
||||
}, [state, overallState, isExecuting, isStreamReady])
|
||||
}
|
||||
|
@ -239,8 +239,8 @@ class KclManager {
|
||||
const currentExecutionId = executionId || Date.now()
|
||||
this._cancelTokens.set(currentExecutionId, false)
|
||||
|
||||
await this.ensureWasmInit()
|
||||
this.isExecuting = true
|
||||
await this.ensureWasmInit()
|
||||
const { logs, errors, programMemory } = await executeAst({
|
||||
ast,
|
||||
engineCommandManager: this.engineCommandManager,
|
||||
|
@ -996,9 +996,6 @@ export class EngineCommandManager {
|
||||
}
|
||||
},
|
||||
onEngineConnectionOpen: () => {
|
||||
this.resolveReady()
|
||||
setIsStreamReady(true)
|
||||
|
||||
// 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
|
||||
@ -1020,6 +1017,8 @@ export class EngineCommandManager {
|
||||
sceneInfra.camControls.onCameraChange()
|
||||
|
||||
this.initPlanes().then(() => {
|
||||
this.resolveReady()
|
||||
setIsStreamReady(true)
|
||||
executeCode(undefined, true)
|
||||
})
|
||||
},
|
||||
|