Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
f441998f1a | |||
531496420e | |||
13bb482904 | |||
562959ee22 | |||
b044f6faef | |||
d916c79874 | |||
4fd5e26abe | |||
8f9bef922f | |||
545e610bbc | |||
55a3e2a4ed | |||
591f17b182 | |||
a7a88bd762 | |||
0916f990cb | |||
75ae4b4a4a | |||
4a490d5900 | |||
4d9cdc6b40 | |||
0d3880233c | |||
8a029605bd | |||
f26adee360 | |||
0f2a01b6c8 |
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@ -4,7 +4,7 @@ on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- tauri
|
||||
release:
|
||||
types: [published]
|
||||
schedule:
|
||||
@ -123,7 +123,7 @@ jobs:
|
||||
git commit -am "Look at this (photo)Graph *in the voice of Nickelback*" || true
|
||||
git push
|
||||
git push origin ${{ github.head_ref }}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -412,17 +412,6 @@ jobs:
|
||||
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-app"
|
||||
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
|
||||
- name: Run e2e tests (windows only)
|
||||
if: ${{ matrix.os == 'windows-latest' && github.event_name != 'release' && github.event_name != 'schedule' }}
|
||||
run: |
|
||||
cargo install tauri-driver --force
|
||||
yarn wdio run wdio.conf.ts
|
||||
env:
|
||||
E2E_APPLICATION: ".\\src-tauri\\target\\${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}\\Zoo Modeling App.exe"
|
||||
KITTYCAD_API_TOKEN: ${{ env.BUILD_RELEASE == 'true' && secrets.KITTYCAD_API_TOKEN || secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
VITE_KC_API_BASE_URL: ${{ env.BUILD_RELEASE == 'true' && 'https://api.zoo.dev' || 'https://api.dev.zoo.dev' }}
|
||||
E2E_TAURI_ENABLED: true
|
||||
TS_NODE_COMPILER_OPTIONS: '{"module": "commonjs"}'
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
if: ${{ env.CUT_RELEASE_PR == 'true' }}
|
||||
|
2
.github/workflows/create-release.yml
vendored
2
.github/workflows/create-release.yml
vendored
@ -3,7 +3,7 @@ name: Create Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- tauri
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
|
@ -117,6 +117,7 @@ Which commands from setup are one off vs need to be run every time?
|
||||
The following will need to be run when checking out a new commit and guarantees the build is not stale:
|
||||
```bash
|
||||
yarn install
|
||||
yarn wasm-prep
|
||||
yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build
|
||||
yarn start # or yarn build:local && yarn serve for slower but more production-like build
|
||||
```
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -3,6 +3,10 @@ import { test, expect, Page } from '@playwright/test'
|
||||
import { getUtils, setup, tearDown } from './test-utils'
|
||||
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
import {
|
||||
PLAYWRIGHT_MOCK_EXPORT_DURATION,
|
||||
PLAYWRIGHT_TOAST_DURATION,
|
||||
} from 'lib/constants'
|
||||
|
||||
test.beforeEach(async ({ context, page }) => {
|
||||
await setup(context, page)
|
||||
@ -250,7 +254,7 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
await expect(exportButton).toBeVisible()
|
||||
|
||||
// Click the export button
|
||||
exportButton.click()
|
||||
await exportButton.click()
|
||||
|
||||
// Click the stl.
|
||||
const stlOption = page.getByText('glTF')
|
||||
@ -279,7 +283,7 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
|
||||
// Click the code editor
|
||||
page.locator('.cm-content').click()
|
||||
await page.locator('.cm-content').click()
|
||||
|
||||
await page.waitForTimeout(2000)
|
||||
|
||||
@ -288,8 +292,7 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
|
||||
// Now add in code that works.
|
||||
page.locator('.cm-content').fill(bracket)
|
||||
page.locator('.cm-content').click()
|
||||
await page.locator('.cm-content').fill(bracket)
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
@ -302,7 +305,7 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
// Now try exporting
|
||||
|
||||
// Click the export button
|
||||
exportButton.click()
|
||||
await exportButton.click()
|
||||
|
||||
// Click the stl.
|
||||
await expect(stlOption).toBeVisible()
|
||||
@ -330,84 +333,108 @@ const sketch001 = startSketchAt([-0, -0])
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async (code) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
}, bracket)
|
||||
await test.step('Set up the code and durations', async () => {
|
||||
await page.addInitScript(
|
||||
async ({ code, toastDurationKey, exportDurationKey }) => {
|
||||
localStorage.setItem('persistCode', code)
|
||||
// Normally we make these durations short to speed up PW tests
|
||||
// to superhuman speeds. But in this case we want to make sure
|
||||
// the export toast is visible for a while, and the export
|
||||
// duration is long enough to make sure the export toast is visible
|
||||
localStorage.setItem(toastDurationKey, '1500')
|
||||
localStorage.setItem(exportDurationKey, '750')
|
||||
},
|
||||
{
|
||||
code: bracket,
|
||||
toastDurationKey: PLAYWRIGHT_TOAST_DURATION,
|
||||
exportDurationKey: PLAYWRIGHT_MOCK_EXPORT_DURATION,
|
||||
}
|
||||
)
|
||||
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// wait for execution done
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
// wait for execution done
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// expect zero errors in guter
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
// expect zero errors in guter
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
})
|
||||
|
||||
const errorToastMessage = page.getByText(`Error while exporting`)
|
||||
const exportingToastMessage = page.getByText(`Exporting...`)
|
||||
const engineErrorToastMessage = page.getByText(`Nothing to export`)
|
||||
const alreadyExportingToastMessage = page.getByText(`Already exporting`)
|
||||
|
||||
await clickExportButton(page)
|
||||
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
|
||||
await clickExportButton(page)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
await expect(alreadyExportingToastMessage).toBeVisible()
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Expect it to succeed.
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
await expect(errorToastMessage).not.toBeVisible()
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
|
||||
const successToastMessage = page.getByText(`Exported successfully`)
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
|
||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
||||
await test.step('Blocked second export', async () => {
|
||||
await clickExportButton(page)
|
||||
|
||||
// Try exporting again.
|
||||
await clickExportButton(page)
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
await clickExportButton(page)
|
||||
|
||||
// Expect it to succeed.
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
await expect(errorToastMessage).not.toBeVisible()
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
||||
await test.step('The second export is blocked', async () => {
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
await expect(alreadyExportingToastMessage).toBeVisible()
|
||||
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
await page.waitForTimeout(1000)
|
||||
})
|
||||
|
||||
await test.step('The first export still succeeds', async () => {
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
await expect(errorToastMessage).not.toBeVisible()
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
|
||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Successful, unblocked export', async () => {
|
||||
// Try exporting again.
|
||||
await clickExportButton(page)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
await expect(exportingToastMessage).toBeVisible()
|
||||
|
||||
// Expect it to succeed.
|
||||
await expect(exportingToastMessage).not.toBeVisible()
|
||||
await expect(errorToastMessage).not.toBeVisible()
|
||||
await expect(engineErrorToastMessage).not.toBeVisible()
|
||||
await expect(alreadyExportingToastMessage).not.toBeVisible()
|
||||
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function clickExportButton(page: Page) {
|
||||
// export the model
|
||||
const exportButton = page.getByTestId('export-pane-button')
|
||||
await expect(exportButton).toBeVisible()
|
||||
await test.step('Running export flow', async () => {
|
||||
// export the model
|
||||
const exportButton = page.getByTestId('export-pane-button')
|
||||
await expect(exportButton).toBeEnabled()
|
||||
|
||||
// Click the export button
|
||||
exportButton.click()
|
||||
// Click the export button
|
||||
await exportButton.click()
|
||||
|
||||
// Click the stl.
|
||||
const gltfOption = page.getByText('glTF')
|
||||
await expect(gltfOption).toBeVisible()
|
||||
// Click the stl.
|
||||
const gltfOption = page.getByRole('option', { name: 'glTF' })
|
||||
await expect(gltfOption).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Click the checkbox
|
||||
const submitButton = page.getByText('Confirm Export')
|
||||
await expect(submitButton).toBeVisible()
|
||||
// Click the checkbox
|
||||
const submitButton = page.getByText('Confirm Export')
|
||||
await expect(submitButton).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.press('Enter')
|
||||
})
|
||||
}
|
||||
|
@ -29,15 +29,13 @@ test.describe('Text-to-CAD tests', () => {
|
||||
)
|
||||
await expect(submittingToastMessage).toBeVisible()
|
||||
|
||||
await page.waitForTimeout(5000)
|
||||
|
||||
const generatingToastMessage = page.getByText(
|
||||
`Generating parametric model...`
|
||||
)
|
||||
await expect(generatingToastMessage).toBeVisible()
|
||||
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||
|
||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||
|
||||
await expect(page.getByText('Copied')).not.toBeVisible()
|
||||
|
||||
@ -78,6 +76,52 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await expect(successToastMessage).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('success model, then ignore success toast, user can create new prompt from command bar', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await sendPromptFromCommandBar(page, 'a 2x6 lego')
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
const submittingToastMessage = page.getByText(
|
||||
`Submitting to Text-to-CAD API...`
|
||||
)
|
||||
await expect(submittingToastMessage).toBeVisible()
|
||||
|
||||
const generatingToastMessage = page.getByText(
|
||||
`Generating parametric model...`
|
||||
)
|
||||
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||
|
||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
||||
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||
|
||||
await expect(page.getByText('Copied')).not.toBeVisible()
|
||||
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
|
||||
// Can send a new prompt from the command bar.
|
||||
await sendPromptFromCommandBar(page, 'a 2x4 lego')
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
await expect(submittingToastMessage).toBeVisible()
|
||||
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||
|
||||
// Expect 2 success toasts.
|
||||
await expect(successToastMessage).toHaveCount(2, {
|
||||
timeout: 15000,
|
||||
})
|
||||
await expect(page.getByText('a 2x4 lego')).toBeVisible()
|
||||
await expect(page.getByText('a 2x6 lego')).toBeVisible()
|
||||
})
|
||||
|
||||
test('you can reject text-to-cad output and it does nothing', async ({
|
||||
page,
|
||||
}) => {
|
||||
@ -96,15 +140,13 @@ test.describe('Text-to-CAD tests', () => {
|
||||
)
|
||||
await expect(submittingToastMessage).toBeVisible()
|
||||
|
||||
await page.waitForTimeout(5000)
|
||||
|
||||
const generatingToastMessage = page.getByText(
|
||||
`Generating parametric model...`
|
||||
)
|
||||
await expect(generatingToastMessage).toBeVisible()
|
||||
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||
|
||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||
|
||||
// Hit copy to clipboard.
|
||||
const rejectButton = page.getByRole('button', { name: 'Reject' })
|
||||
@ -184,7 +226,9 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await expect(failureToastMessage).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('sending a bad prompt fails, can start over', async ({ page }) => {
|
||||
test('sending a bad prompt fails, can start over from toast', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
@ -266,11 +310,86 @@ test.describe('Text-to-CAD tests', () => {
|
||||
// Look out for the toast message
|
||||
await expect(submittingToastMessage).toBeVisible()
|
||||
|
||||
await page.waitForTimeout(5000)
|
||||
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||
|
||||
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||
})
|
||||
|
||||
test('sending a bad prompt fails, can ignore toast, can start over from command bar', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
const commandBarButton = page.getByRole('button', { name: 'Commands' })
|
||||
await expect(commandBarButton).toBeVisible()
|
||||
// Click the command bar button
|
||||
await commandBarButton.click()
|
||||
|
||||
// Wait for the command bar to appear
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
|
||||
const textToCadCommand = page.getByText('Text-to-CAD')
|
||||
await expect(textToCadCommand.first()).toBeVisible()
|
||||
// Click the Text-to-CAD command
|
||||
await textToCadCommand.first().click()
|
||||
|
||||
// Enter the prompt.
|
||||
const prompt = page.getByText('Prompt')
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
|
||||
const badPrompt =
|
||||
'akjsndladf lajbhflauweyfa;wieufjn;wieJNUF;.wjdfn weh Fwhefb'
|
||||
|
||||
// Type the prompt.
|
||||
await page.keyboard.type(badPrompt)
|
||||
await page.waitForTimeout(1000)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
const submittingToastMessage = page.getByText(
|
||||
`Submitting to Text-to-CAD API...`
|
||||
)
|
||||
await expect(submittingToastMessage).toBeVisible()
|
||||
|
||||
const generatingToastMessage = page.getByText(
|
||||
`Generating parametric model...`
|
||||
)
|
||||
await expect(generatingToastMessage).toBeVisible()
|
||||
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
const failureToastMessage = page.getByText(
|
||||
`The prompt must clearly describe a CAD model`
|
||||
)
|
||||
await expect(failureToastMessage).toBeVisible()
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Make sure the toast did not say it was successful.
|
||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
||||
await expect(successToastMessage).not.toBeVisible()
|
||||
await expect(page.getByText(`Text-to-CAD failed`)).toBeVisible()
|
||||
|
||||
// They should be able to try again from the command bar.
|
||||
await sendPromptFromCommandBar(page, 'a 2x4 lego')
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
await expect(submittingToastMessage).toBeVisible()
|
||||
|
||||
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||
|
||||
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||
|
||||
await expect(page.getByText('Copied')).not.toBeVisible()
|
||||
|
||||
// old failure toast should stick around.
|
||||
await expect(failureToastMessage).toBeVisible()
|
||||
await expect(page.getByText(`Text-to-CAD failed`)).toBeVisible()
|
||||
})
|
||||
|
||||
test('ensure you can shift+enter in the prompt box', async ({ page }) => {
|
||||
@ -317,18 +436,210 @@ test.describe('Text-to-CAD tests', () => {
|
||||
)
|
||||
await expect(submittingToastMessage).toBeVisible()
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
const generatingToastMessage = page.getByText(
|
||||
`Generating parametric model...`
|
||||
)
|
||||
await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
|
||||
|
||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
||||
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||
|
||||
await expect(page.getByText(promptWithNewline)).toBeVisible()
|
||||
})
|
||||
|
||||
test('can do many at once and get many prompts back, and interact with many', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await sendPromptFromCommandBar(page, 'a 2x4 lego')
|
||||
|
||||
await sendPromptFromCommandBar(page, 'a 2x8 lego')
|
||||
|
||||
await sendPromptFromCommandBar(page, 'a 2x10 lego')
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
const submittingToastMessage = page.getByText(
|
||||
`Submitting to Text-to-CAD API...`
|
||||
)
|
||||
await expect(submittingToastMessage.first()).toBeVisible()
|
||||
|
||||
const generatingToastMessage = page.getByText(
|
||||
`Generating parametric model...`
|
||||
)
|
||||
await expect(generatingToastMessage).toBeVisible()
|
||||
await page.waitForTimeout(5000)
|
||||
await expect(generatingToastMessage.first()).toBeVisible({ timeout: 10000 })
|
||||
|
||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
||||
await expect(successToastMessage).toBeVisible()
|
||||
// We should have three success toasts.
|
||||
await expect(successToastMessage).toHaveCount(3, { timeout: 15000 })
|
||||
|
||||
await expect(page.getByText(promptWithNewline)).toBeVisible()
|
||||
await expect(page.getByText('Copied')).not.toBeVisible()
|
||||
|
||||
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
|
||||
await expect(page.getByText(`a 2x8 lego`)).toBeVisible()
|
||||
await expect(page.getByText(`a 2x10 lego`)).toBeVisible()
|
||||
|
||||
// Ensure if you reject one, the others stay.
|
||||
const rejectButton = page.getByRole('button', { name: 'Reject' })
|
||||
await expect(rejectButton.first()).toBeVisible()
|
||||
// Click the reject button on the first toast.
|
||||
await rejectButton.first().click()
|
||||
|
||||
// The first toast should disappear, but not the others.
|
||||
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
|
||||
await expect(page.getByText(`a 2x8 lego`)).toBeVisible()
|
||||
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
|
||||
|
||||
// Ensure you can copy the code for one of the models remaining.
|
||||
const copyToClipboardButton = page.getByRole('button', {
|
||||
name: 'Copy to clipboard',
|
||||
})
|
||||
await expect(copyToClipboardButton.first()).toBeVisible()
|
||||
// Click the button.
|
||||
await copyToClipboardButton.first().click()
|
||||
|
||||
// Expect the code to be copied.
|
||||
await expect(page.getByText('Copied')).toBeVisible()
|
||||
|
||||
// Click in the code editor.
|
||||
await page.locator('.cm-content').click({ position: { x: 10, y: 10 } })
|
||||
|
||||
// Paste the code.
|
||||
await page.keyboard.down(CtrlKey)
|
||||
await page.keyboard.press('KeyV')
|
||||
await page.keyboard.up(CtrlKey)
|
||||
|
||||
// Expect the code to be pasted.
|
||||
await expect(page.locator('.cm-content')).toContainText(`2x8`)
|
||||
|
||||
// Find the toast close button.
|
||||
const closeButton = page.getByRole('button', { name: 'Close' })
|
||||
await expect(closeButton).toBeVisible()
|
||||
await closeButton.click()
|
||||
|
||||
// Ensure the final toast remains.
|
||||
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
|
||||
await expect(page.getByText(`a 2x8 lego`)).not.toBeVisible()
|
||||
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
|
||||
|
||||
// Ensure you can copy the code for the final model.
|
||||
await expect(copyToClipboardButton).toBeVisible()
|
||||
// Click the button.
|
||||
await copyToClipboardButton.click()
|
||||
|
||||
// Expect the code to be copied.
|
||||
await expect(page.getByText('Copied')).toBeVisible()
|
||||
|
||||
// Click in the code editor.
|
||||
await page.locator('.cm-content').click({ position: { x: 10, y: 10 } })
|
||||
|
||||
// Paste the code.
|
||||
await page.keyboard.press('ControlOrMeta+a')
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('ControlOrMeta+v')
|
||||
|
||||
// Expect the code to be pasted.
|
||||
await expect(page.locator('.cm-content')).toContainText(`2x4`)
|
||||
|
||||
// Expect the toast to disappear.
|
||||
// Find the toast close button.
|
||||
await expect(closeButton).toBeVisible()
|
||||
await closeButton.click()
|
||||
await expect(successToastMessage).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('can do many at once with errors, clicking dismiss error does not dismiss all', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await sendPromptFromCommandBar(page, 'a 2x4 lego')
|
||||
|
||||
await sendPromptFromCommandBar(
|
||||
page,
|
||||
'alkjsdnlajshdbfjlhsbdf a;skjdnf;askjdnf'
|
||||
)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
const submittingToastMessage = page.getByText(
|
||||
`Submitting to Text-to-CAD API...`
|
||||
)
|
||||
await expect(submittingToastMessage.first()).toBeVisible()
|
||||
|
||||
const generatingToastMessage = page.getByText(
|
||||
`Generating parametric model...`
|
||||
)
|
||||
await expect(generatingToastMessage.first()).toBeVisible({ timeout: 10000 })
|
||||
|
||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
||||
// We should have three success toasts.
|
||||
await expect(successToastMessage).toHaveCount(1, { timeout: 15000 })
|
||||
|
||||
await expect(page.getByText('Copied')).not.toBeVisible()
|
||||
|
||||
const failureToastMessage = page.getByText(
|
||||
`The prompt must clearly describe a CAD model`
|
||||
)
|
||||
await expect(failureToastMessage).toBeVisible()
|
||||
|
||||
// Make sure the toast did not say it was successful.
|
||||
await expect(page.getByText(`Text-to-CAD failed`)).toBeVisible()
|
||||
|
||||
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
|
||||
|
||||
// Ensure if you dismiss the error the others stay.
|
||||
const dismissButton = page.getByRole('button', { name: 'Dismiss' })
|
||||
await expect(dismissButton).toBeVisible()
|
||||
// Click the dismiss button on the first toast.
|
||||
await dismissButton.first().click()
|
||||
|
||||
// Make sure the failure toast disappears.
|
||||
await expect(failureToastMessage).not.toBeVisible()
|
||||
await expect(page.getByText(`Text-to-CAD failed`)).not.toBeVisible()
|
||||
|
||||
// The first toast should disappear, but not the others.
|
||||
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
|
||||
|
||||
// Ensure you can copy the code for one of the models remaining.
|
||||
const copyToClipboardButton = page.getByRole('button', {
|
||||
name: 'Copy to clipboard',
|
||||
})
|
||||
await expect(copyToClipboardButton.first()).toBeVisible()
|
||||
// Click the button.
|
||||
await copyToClipboardButton.first().click()
|
||||
|
||||
// Expect the code to be copied.
|
||||
await expect(page.getByText('Copied')).toBeVisible()
|
||||
|
||||
// Click in the code editor.
|
||||
await page.locator('.cm-content').click({ position: { x: 10, y: 10 } })
|
||||
|
||||
// Paste the code.
|
||||
await page.keyboard.down(CtrlKey)
|
||||
await page.keyboard.press('KeyV')
|
||||
await page.keyboard.up(CtrlKey)
|
||||
|
||||
// Expect the code to be pasted.
|
||||
await expect(page.locator('.cm-content')).toContainText(`2x4`)
|
||||
|
||||
// Find the toast close button.
|
||||
const closeButton = page.getByRole('button', { name: 'Close' })
|
||||
await expect(closeButton).toBeVisible()
|
||||
await closeButton.click()
|
||||
|
||||
// Expect the toast to disappear.
|
||||
await expect(page.getByText('Copied')).not.toBeVisible()
|
||||
await expect(successToastMessage).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@ -342,7 +653,7 @@ async function sendPromptFromCommandBar(page: Page, promptStr: string) {
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
|
||||
const textToCadCommand = page.getByText('Text-to-CAD')
|
||||
const textToCadCommand = page.getByText('Use the Zoo Text-to-CAD API ')
|
||||
await expect(textToCadCommand.first()).toBeVisible()
|
||||
// Click the Text-to-CAD command
|
||||
await textToCadCommand.first().click()
|
||||
|
@ -264,6 +264,8 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
|
||||
})
|
||||
|
||||
test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
// This test can run long if it takes a little too long to load
|
||||
// the engine.
|
||||
test.setTimeout(90000)
|
||||
@ -273,131 +275,162 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
|
||||
'weird playwright bug on ubuntu https://github.com/KittyCAD/modeling-app/issues/2444'
|
||||
)
|
||||
// Load the app with the code pane open
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'store',
|
||||
JSON.stringify({
|
||||
state: {
|
||||
openPanes: ['code'],
|
||||
},
|
||||
version: 0,
|
||||
})
|
||||
)
|
||||
|
||||
await test.step(`Set up test`, async () => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'store',
|
||||
JSON.stringify({
|
||||
state: {
|
||||
openPanes: ['code'],
|
||||
},
|
||||
version: 0,
|
||||
})
|
||||
)
|
||||
})
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
})
|
||||
|
||||
// Wait for the app to be ready for use
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const codePane = page.getByRole('textbox').locator('div')
|
||||
const codePaneButton = page.getByTestId('code-pane-button')
|
||||
const codePane = page.locator('.cm-content')
|
||||
const lineButton = page.getByRole('button', { name: 'Line', exact: true })
|
||||
const arcButton = page.getByRole('button', {
|
||||
name: 'Tangential Arc',
|
||||
exact: true,
|
||||
})
|
||||
const extrudeButton = page.getByRole('button', { name: 'Extrude' })
|
||||
const commandBarComboBox = page.getByPlaceholder('Search commands')
|
||||
const exitSketchButton = page.getByRole('button', { name: 'Exit Sketch' })
|
||||
|
||||
// Test that the hotkeys do nothing when
|
||||
// focus is on the code pane
|
||||
await codePane.click()
|
||||
await page.keyboard.press('/')
|
||||
await page.keyboard.press('/')
|
||||
await page.keyboard.press('s')
|
||||
await page.keyboard.press('l')
|
||||
await page.keyboard.press('a')
|
||||
await page.keyboard.press('e')
|
||||
await expect(page.locator('.cm-content')).toHaveText('//slae')
|
||||
await page.keyboard.press('Meta+/')
|
||||
await page.waitForTimeout(1000)
|
||||
// Test these hotkeys perform actions when
|
||||
// focus is on the canvas
|
||||
await page.mouse.move(600, 250)
|
||||
await page.mouse.click(600, 250)
|
||||
|
||||
// work-around: to stop "keyboard.press('s')" from typing in the editor even when it should be blurred
|
||||
await page.getByRole('button', { name: 'Commands' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(100)
|
||||
// end work-around
|
||||
|
||||
// Start a sketch
|
||||
await page.keyboard.press('s')
|
||||
await page.waitForTimeout(1000)
|
||||
await page.mouse.move(800, 300, { steps: 5 })
|
||||
await page.mouse.click(800, 300)
|
||||
await page.waitForTimeout(1000)
|
||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'true', {
|
||||
timeout: 15_000,
|
||||
await test.step(`Type code with modeling hotkeys, shouldn't fire`, async () => {
|
||||
await codePane.click()
|
||||
await page.keyboard.type('//')
|
||||
await page.keyboard.press('s')
|
||||
await expect(commandBarComboBox).not.toBeVisible()
|
||||
await page.keyboard.press('e')
|
||||
await expect(commandBarComboBox).not.toBeVisible()
|
||||
await expect(codePane).toHaveText('//se')
|
||||
})
|
||||
|
||||
// Blur focus from the code editor, use the s command to sketch
|
||||
await test.step(`Blur editor focus, enter sketch`, async () => {
|
||||
/**
|
||||
* TODO: There is a bug somewhere that causes this test to fail
|
||||
* if you toggle the codePane closed before your trigger the
|
||||
* start of the sketch.
|
||||
* and a separate Safari-only bug that causes the test to fail
|
||||
* if the pane is open the entire test. The maintainer of CodeMirror
|
||||
* has pinpointed this to the unusual browser behavior:
|
||||
* https://discuss.codemirror.net/t/how-to-force-unfocus-of-the-codemirror-element-in-safari/8095/3
|
||||
*/
|
||||
await blurCodeEditor()
|
||||
await page.waitForTimeout(1000)
|
||||
await page.keyboard.press('s')
|
||||
await page.waitForTimeout(1000)
|
||||
await page.mouse.move(800, 300, { steps: 5 })
|
||||
await page.mouse.click(800, 300)
|
||||
await page.waitForTimeout(1000)
|
||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'true', {
|
||||
timeout: 15_000,
|
||||
})
|
||||
})
|
||||
|
||||
// Use some sketch hotkeys to create a sketch (l and a for now)
|
||||
await test.step(`Incomplete sketch with hotkeys`, async () => {
|
||||
await test.step(`Draw a line`, async () => {
|
||||
await page.mouse.move(700, 200, { steps: 5 })
|
||||
await page.mouse.click(700, 200)
|
||||
await page.mouse.move(800, 250, { steps: 5 })
|
||||
await page.mouse.click(800, 250)
|
||||
})
|
||||
|
||||
await test.step(`Unequip line tool`, async () => {
|
||||
await page.keyboard.press('l')
|
||||
await expect(lineButton).not.toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
|
||||
await test.step(`Draw a tangential arc`, async () => {
|
||||
await page.keyboard.press('a')
|
||||
await expect(arcButton).toHaveAttribute('aria-pressed', 'true', {
|
||||
timeout: 10_000,
|
||||
})
|
||||
await page.mouse.move(1000, 100, { steps: 5 })
|
||||
await page.mouse.click(1000, 100)
|
||||
})
|
||||
|
||||
await test.step(`Unequip with escape, equip line tool`, async () => {
|
||||
await page.keyboard.press('Escape')
|
||||
await page.keyboard.press('l')
|
||||
await page.waitForTimeout(50)
|
||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
await test.step(`Type code with sketch hotkeys, shouldn't fire`, async () => {
|
||||
// Since there's code now, we have to get to the end of the line
|
||||
await page.locator('.cm-line').last().click()
|
||||
await page.keyboard.press('ControlOrMeta+ArrowRight')
|
||||
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type('//')
|
||||
await page.keyboard.press('l')
|
||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
|
||||
await page.keyboard.press('a')
|
||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
|
||||
await expect(codePane).toContainText('//la')
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.keyboard.press('Backspace')
|
||||
})
|
||||
|
||||
await test.step(`Close profile and exit sketch`, async () => {
|
||||
await blurCodeEditor()
|
||||
await page.mouse.move(700, 200, { steps: 5 })
|
||||
await page.mouse.click(700, 200)
|
||||
// On close it will unequip the line tool.
|
||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'false')
|
||||
await expect(exitSketchButton).toBeEnabled()
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Exit Sketch' })
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
// Extrude with e
|
||||
await test.step(`Extrude the sketch`, async () => {
|
||||
await page.mouse.click(750, 150)
|
||||
await blurCodeEditor()
|
||||
await expect(extrudeButton).toBeEnabled()
|
||||
await page.keyboard.press('e')
|
||||
await page.waitForTimeout(500)
|
||||
await page.mouse.move(800, 200, { steps: 5 })
|
||||
await page.mouse.click(800, 200)
|
||||
await page.waitForTimeout(500)
|
||||
await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Continue' }).click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Submit command' })
|
||||
).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Submit command' }).click()
|
||||
await expect(page.locator('.cm-content')).toContainText('extrude(')
|
||||
})
|
||||
|
||||
// await codePaneButton.click()
|
||||
// await expect(u.codeLocator).not.toBeVisible()
|
||||
|
||||
/**
|
||||
* TODO: There is a bug somewhere that causes this test to fail
|
||||
* if you toggle the codePane closed before your trigger the
|
||||
* start of the sketch.
|
||||
* and a separate Safari-only bug that causes the test to fail
|
||||
* if the pane is open the entire test. The maintainer of CodeMirror
|
||||
* has pinpointed this to the unusual browser behavior:
|
||||
* https://discuss.codemirror.net/t/how-to-force-unfocus-of-the-codemirror-element-in-safari/8095/3
|
||||
* work-around: to stop `keyboard.press()` from typing in the editor even when it should be blurred
|
||||
*/
|
||||
await codePaneButton.click()
|
||||
await expect(u.codeLocator).not.toBeVisible()
|
||||
await page.waitForTimeout(300)
|
||||
|
||||
// Draw a line
|
||||
await page.mouse.move(700, 200, { steps: 5 })
|
||||
await page.mouse.click(700, 200)
|
||||
await page.waitForTimeout(300)
|
||||
await page.mouse.move(800, 250, { steps: 5 })
|
||||
await page.mouse.click(800, 250)
|
||||
// Unequip line tool
|
||||
await page.keyboard.press('l')
|
||||
await expect(lineButton).not.toHaveAttribute('aria-pressed', 'true')
|
||||
// Equip arc tool
|
||||
await page.keyboard.press('a')
|
||||
await expect(arcButton).toHaveAttribute('aria-pressed', 'true', {
|
||||
timeout: 10_000,
|
||||
})
|
||||
await page.mouse.move(1000, 100, { steps: 5 })
|
||||
await page.mouse.click(1000, 100)
|
||||
await page.keyboard.press('Escape')
|
||||
await page.keyboard.press('l')
|
||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
|
||||
// Close profile
|
||||
await page.mouse.move(700, 200, { steps: 5 })
|
||||
await page.mouse.click(700, 200)
|
||||
// On close it will unequip the line tool.
|
||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'false')
|
||||
// Exit sketch
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Exit Sketch' })
|
||||
).not.toBeVisible()
|
||||
await page.waitForTimeout(400)
|
||||
|
||||
// Extrude
|
||||
await page.mouse.click(750, 150)
|
||||
await expect(extrudeButton).not.toBeDisabled()
|
||||
await page.keyboard.press('e')
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.move(800, 200, { steps: 5 })
|
||||
await page.mouse.click(800, 200)
|
||||
await page.waitForTimeout(300)
|
||||
await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Continue' }).click()
|
||||
await page.waitForTimeout(300)
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Submit command' })
|
||||
).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Submit command' }).click()
|
||||
|
||||
await codePaneButton.click()
|
||||
await expect(page.locator('.cm-content')).toContainText('extrude(')
|
||||
async function blurCodeEditor() {
|
||||
await page.getByRole('button', { name: 'Commands' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(100)
|
||||
}
|
||||
})
|
||||
|
||||
test('Delete key does not navigate back', async ({ page }) => {
|
||||
|
@ -618,6 +618,67 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"description": "Ams change filament.",
|
||||
"properties": {
|
||||
"command": {
|
||||
"enum": [
|
||||
"ams_change_filament"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"errorno": {
|
||||
"description": "The error number.",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"reason": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Reason"
|
||||
}
|
||||
],
|
||||
"description": "The reason for the message.",
|
||||
"nullable": true
|
||||
},
|
||||
"result": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Result"
|
||||
}
|
||||
],
|
||||
"description": "The result of the command."
|
||||
},
|
||||
"sequence_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SequenceId"
|
||||
}
|
||||
],
|
||||
"description": "The sequence id."
|
||||
},
|
||||
"tar_temp": {
|
||||
"description": "The target temperature.",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"target": {
|
||||
"description": "The target.",
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"errorno",
|
||||
"result",
|
||||
"sequence_id",
|
||||
"tar_temp",
|
||||
"target"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"description": "Calibration.",
|
||||
@ -1192,6 +1253,54 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"description": "Print speed.",
|
||||
"properties": {
|
||||
"command": {
|
||||
"enum": [
|
||||
"print_speed"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"param": {
|
||||
"description": "The param.",
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Reason"
|
||||
}
|
||||
],
|
||||
"description": "The reason for the message.",
|
||||
"nullable": true
|
||||
},
|
||||
"result": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Result"
|
||||
}
|
||||
],
|
||||
"description": "The result of the command."
|
||||
},
|
||||
"sequence_id": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/SequenceId"
|
||||
}
|
||||
],
|
||||
"description": "The sequence id."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"param",
|
||||
"result",
|
||||
"sequence_id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"additionalProperties": true,
|
||||
"description": "Resume the print.",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "untitled-app",
|
||||
"version": "0.24.11",
|
||||
"version": "0.24.13",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.17.0",
|
||||
@ -37,7 +37,7 @@
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html2canvas-pro": "^1.5.5",
|
||||
"html2canvas-pro": "^1.5.8",
|
||||
"json-rpc-2.0": "^1.6.0",
|
||||
"jszip": "^3.10.1",
|
||||
"re-resizable": "^6.9.11",
|
||||
|
@ -80,5 +80,5 @@
|
||||
}
|
||||
},
|
||||
"productName": "Zoo Modeling App",
|
||||
"version": "0.24.11"
|
||||
"version": "0.24.13"
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import { PATHS } from 'lib/paths'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
@ -50,7 +50,7 @@ export function App() {
|
||||
const token = auth?.context?.token
|
||||
|
||||
const coreDumpManager = useMemo(
|
||||
() => new CoreDumpManager(engineCommandManager, token),
|
||||
() => new CoreDumpManager(engineCommandManager, codeManager, token),
|
||||
[]
|
||||
)
|
||||
|
||||
|
@ -34,7 +34,7 @@ import { KclContextProvider } from 'lang/KclProvider'
|
||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
import { getState, setState } from 'lib/tauri'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import toast from 'react-hot-toast'
|
||||
@ -181,7 +181,7 @@ function CoreDump() {
|
||||
const { auth } = useSettingsAuthContext()
|
||||
const token = auth?.context?.token
|
||||
const coreDumpManager = useMemo(
|
||||
() => new CoreDumpManager(engineCommandManager, token),
|
||||
() => new CoreDumpManager(engineCommandManager, codeManager, token),
|
||||
[]
|
||||
)
|
||||
useHotkeyWrapper(['meta + shift + .'], () => {
|
||||
|
@ -21,11 +21,13 @@ import {
|
||||
rename,
|
||||
create,
|
||||
writeTextFile,
|
||||
exists,
|
||||
} from '@tauri-apps/plugin-fs'
|
||||
import { isTauri } from 'lib/isTauri'
|
||||
import { join, sep } from '@tauri-apps/api/path'
|
||||
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
|
||||
import { getProjectInfo } from 'lib/tauri'
|
||||
import { getNextDirName, getNextFileName } from 'lib/tauriFS'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -105,14 +107,20 @@ export const FileMachineProvider = ({
|
||||
let createdPath: string
|
||||
|
||||
if (event.data.makeDir) {
|
||||
createdPath = await join(context.selectedDirectory.path, createdName)
|
||||
let { name, path } = await getNextDirName({
|
||||
entryName: createdName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await mkdir(createdPath)
|
||||
} else {
|
||||
createdPath =
|
||||
context.selectedDirectory.path +
|
||||
sep() +
|
||||
createdName +
|
||||
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT)
|
||||
const { name, path } = await getNextFileName({
|
||||
entryName: createdName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await create(createdPath)
|
||||
if (event.data.content) {
|
||||
await writeTextFile(createdPath, event.data.content)
|
||||
@ -129,14 +137,20 @@ export const FileMachineProvider = ({
|
||||
let createdPath: string
|
||||
|
||||
if (event.data.makeDir) {
|
||||
createdPath = await join(context.selectedDirectory.path, createdName)
|
||||
let { name, path } = await getNextDirName({
|
||||
entryName: createdName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await mkdir(createdPath)
|
||||
} else {
|
||||
createdPath =
|
||||
context.selectedDirectory.path +
|
||||
sep() +
|
||||
createdName +
|
||||
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT)
|
||||
const { name, path } = await getNextFileName({
|
||||
entryName: createdName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await create(createdPath)
|
||||
if (event.data.content) {
|
||||
await writeTextFile(createdPath, event.data.content)
|
||||
|
@ -382,11 +382,17 @@ export const FileTreeMenu = () => {
|
||||
const { send } = useFileContext()
|
||||
|
||||
async function createFile() {
|
||||
send({ type: 'Create file', data: { name: '', makeDir: false } })
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: { name: '', makeDir: false },
|
||||
})
|
||||
}
|
||||
|
||||
async function createFolder() {
|
||||
send({ type: 'Create file', data: { name: '', makeDir: true } })
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: { name: '', makeDir: true },
|
||||
})
|
||||
}
|
||||
|
||||
useHotkeyWrapper(['meta + n'], createFile)
|
||||
|
@ -85,6 +85,7 @@ import {
|
||||
} from 'lang/std/engineConnection'
|
||||
import { submitAndAwaitTextToKcl } from 'lib/textToCad'
|
||||
import { useFileContext } from 'hooks/useFileContext'
|
||||
import { PLAYWRIGHT_MOCK_EXPORT_DURATION } from 'lib/constants'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -393,10 +394,24 @@ export const ModelingMachineProvider = ({
|
||||
selection: { type: 'default_scene' },
|
||||
}
|
||||
|
||||
const mockExportDuration = window.localStorage.getItem(
|
||||
PLAYWRIGHT_MOCK_EXPORT_DURATION
|
||||
)
|
||||
|
||||
console.log('mockExportDuration', mockExportDuration)
|
||||
|
||||
// Artificially delay the export in playwright tests
|
||||
toast.promise(
|
||||
exportFromEngine({
|
||||
format: format,
|
||||
}),
|
||||
Promise.all([
|
||||
exportFromEngine({
|
||||
format: format,
|
||||
}),
|
||||
mockExportDuration
|
||||
? new Promise((resolve) =>
|
||||
setTimeout(resolve, Number(mockExportDuration))
|
||||
)
|
||||
: Promise.resolve(),
|
||||
]),
|
||||
{
|
||||
loading: 'Starting print...',
|
||||
success: 'Started print successfully',
|
||||
@ -467,12 +482,6 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
)
|
||||
},
|
||||
'Add pending text-to-cad prompt': assign({
|
||||
pendingTextToCad: (_, event) => event.data.prompt.trim(),
|
||||
}),
|
||||
'Remove pending text-to-cad prompt': assign({
|
||||
pendingTextToCad: undefined,
|
||||
}),
|
||||
'Submit to Text-to-CAD API': async (_, { data }) => {
|
||||
const trimmedPrompt = data.prompt.trim()
|
||||
if (!trimmedPrompt) return
|
||||
@ -549,8 +558,6 @@ export const ModelingMachineProvider = ({
|
||||
return false
|
||||
}
|
||||
},
|
||||
'Has no pending text-to-cad submissions': ({ pendingTextToCad }) =>
|
||||
!pendingTextToCad,
|
||||
},
|
||||
services: {
|
||||
'AST-undo-startSketchOn': async ({ sketchDetails }) => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { coreDump } from 'lang/wasm'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||
import React, { useMemo } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import Tooltip from './Tooltip'
|
||||
@ -11,7 +11,7 @@ export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
||||
const { auth } = useSettingsAuthContext()
|
||||
const token = auth?.context?.token
|
||||
const coreDumpManager = useMemo(
|
||||
() => new CoreDumpManager(engineCommandManager, token),
|
||||
() => new CoreDumpManager(engineCommandManager, codeManager, token),
|
||||
[]
|
||||
)
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { PATHS } from 'lib/paths'
|
||||
import toast from 'react-hot-toast'
|
||||
import { sep } from '@tauri-apps/api/path'
|
||||
import { TextToCad_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
Box3,
|
||||
Color,
|
||||
@ -36,10 +36,12 @@ const FRUSTUM_SIZE = 0.5
|
||||
const OUTPUT_KEY = 'source.glb'
|
||||
|
||||
export function ToastTextToCadError({
|
||||
toastId,
|
||||
message,
|
||||
prompt,
|
||||
commandBarSend,
|
||||
}: {
|
||||
toastId: string
|
||||
message: string
|
||||
prompt: string
|
||||
commandBarSend: (
|
||||
@ -63,7 +65,7 @@ export function ToastTextToCadError({
|
||||
}}
|
||||
name="Dismiss"
|
||||
onClick={() => {
|
||||
toast.dismiss()
|
||||
toast.dismiss(toastId)
|
||||
}}
|
||||
>
|
||||
Dismiss
|
||||
@ -85,7 +87,7 @@ export function ToastTextToCadError({
|
||||
},
|
||||
},
|
||||
})
|
||||
toast.dismiss()
|
||||
toast.dismiss(toastId)
|
||||
}}
|
||||
>
|
||||
Edit prompt
|
||||
@ -96,6 +98,7 @@ export function ToastTextToCadError({
|
||||
}
|
||||
|
||||
export function ToastTextToCadSuccess({
|
||||
toastId,
|
||||
data,
|
||||
navigate,
|
||||
context,
|
||||
@ -103,7 +106,7 @@ export function ToastTextToCadSuccess({
|
||||
fileMachineSend,
|
||||
settings,
|
||||
}: {
|
||||
// TODO: update this type to match the actual data when API is done
|
||||
toastId: string
|
||||
data: TextToCad_type & { fileName: string }
|
||||
navigate: (to: string) => void
|
||||
context: ReturnType<typeof useFileContext>['context']
|
||||
@ -119,10 +122,40 @@ export function ToastTextToCadSuccess({
|
||||
}) {
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||
const animationRequestRef = useRef<number>()
|
||||
const [hasCopied, setHasCopied] = useState(false)
|
||||
const [showCopiedUi, setShowCopiedUi] = useState(false)
|
||||
const modelId = data.id
|
||||
|
||||
const animate = useCallback(
|
||||
({
|
||||
renderer,
|
||||
scene,
|
||||
camera,
|
||||
controls,
|
||||
isFirstRender = false,
|
||||
}: {
|
||||
renderer: WebGLRenderer
|
||||
scene: Scene
|
||||
camera: OrthographicCamera
|
||||
controls: OrbitControls
|
||||
isFirstRender?: boolean
|
||||
}) => {
|
||||
if (
|
||||
!wrapperRef.current ||
|
||||
!(isFirstRender || animationRequestRef.current)
|
||||
)
|
||||
return
|
||||
animationRequestRef.current = requestAnimationFrame(() =>
|
||||
animate({ renderer, scene, camera, controls })
|
||||
)
|
||||
// required if controls.enableDamping or controls.autoRotate are set to true
|
||||
controls.update()
|
||||
renderer.render(scene, camera)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return
|
||||
|
||||
@ -130,7 +163,6 @@ export function ToastTextToCadSuccess({
|
||||
const renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true })
|
||||
renderer.setSize(CANVAS_SIZE, CANVAS_SIZE)
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
|
||||
renderer.setAnimationLoop(animate)
|
||||
|
||||
const scene = new Scene()
|
||||
const ambientLight = new DirectionalLight(new Color('white'), 8.0)
|
||||
@ -153,13 +185,6 @@ export function ToastTextToCadSuccess({
|
||||
return
|
||||
}
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate)
|
||||
// required if controls.enableDamping or controls.autoRotate are set to true
|
||||
controls.update()
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
|
||||
loader.parse(
|
||||
buffer,
|
||||
'',
|
||||
@ -210,6 +235,8 @@ export function ToastTextToCadSuccess({
|
||||
|
||||
camera.updateProjectionMatrix()
|
||||
controls.update()
|
||||
// render the scene once...
|
||||
renderer.render(scene, camera)
|
||||
},
|
||||
// called when loading has errors
|
||||
function (error) {
|
||||
@ -219,8 +246,26 @@ export function ToastTextToCadSuccess({
|
||||
}
|
||||
)
|
||||
|
||||
// ...and set a mouseover listener on the canvas to enable the orbit controls
|
||||
canvasRef.current.addEventListener('mouseover', () => {
|
||||
renderer.setAnimationLoop(() =>
|
||||
animate({ renderer, scene, camera, controls, isFirstRender: true })
|
||||
)
|
||||
})
|
||||
canvasRef.current.addEventListener('mouseout', () => {
|
||||
renderer.setAnimationLoop(null)
|
||||
if (animationRequestRef.current) {
|
||||
cancelAnimationFrame(animationRequestRef.current)
|
||||
animationRequestRef.current = undefined
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
renderer.dispose()
|
||||
if (animationRequestRef.current) {
|
||||
cancelAnimationFrame(animationRequestRef.current)
|
||||
animationRequestRef.current = undefined
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -265,7 +310,7 @@ export function ToastTextToCadSuccess({
|
||||
},
|
||||
})
|
||||
}
|
||||
toast.dismiss()
|
||||
toast.dismiss(toastId)
|
||||
}}
|
||||
>
|
||||
{hasCopied ? 'Close' : 'Reject'}
|
||||
@ -284,7 +329,7 @@ export function ToastTextToCadSuccess({
|
||||
`${context.project.path}${sep()}${data.fileName}`
|
||||
)}`
|
||||
)
|
||||
toast.dismiss()
|
||||
toast.dismiss(toastId)
|
||||
}}
|
||||
>
|
||||
Accept
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
createUpdaterRestartModal,
|
||||
} from 'components/UpdaterRestartModal'
|
||||
import { AppStreamProvider } from 'AppState'
|
||||
import { PLAYWRIGHT_KEY, PLAYWRIGHT_TOAST_DURATION } from 'lib/constants'
|
||||
|
||||
// uncomment for xstate inspector
|
||||
// import { DEV } from 'env'
|
||||
@ -24,6 +25,9 @@ import { AppStreamProvider } from 'AppState'
|
||||
// })
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||
const maybePlaywrightToastDuration = Number(
|
||||
window?.localStorage.getItem(PLAYWRIGHT_TOAST_DURATION)
|
||||
)
|
||||
|
||||
root.render(
|
||||
<HotkeysProvider>
|
||||
@ -44,8 +48,10 @@ root.render(
|
||||
secondary: 'oklch(48.62% 0.1654 142.5deg)',
|
||||
},
|
||||
duration:
|
||||
window?.localStorage.getItem('playwright') === 'true'
|
||||
? 10 // speed up e2e tests
|
||||
window?.localStorage.getItem(PLAYWRIGHT_KEY) === 'true'
|
||||
? maybePlaywrightToastDuration > 0
|
||||
? maybePlaywrightToastDuration
|
||||
: 10 // optionally speed up e2e tests
|
||||
: 1500,
|
||||
},
|
||||
}}
|
||||
|
@ -56,3 +56,10 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
|
||||
/** The default KCL length expression */
|
||||
export const KCL_DEFAULT_LENGTH = `5`
|
||||
export const COOKIE_NAME = '__Secure-next-auth.session-token'
|
||||
|
||||
/** localStorage key to determine if we're in Playwright tests */
|
||||
export const PLAYWRIGHT_KEY = 'playwright'
|
||||
/** localStorage key to set toast duration in Playwright tests */
|
||||
export const PLAYWRIGHT_TOAST_DURATION = 'playwright-toast-duration'
|
||||
/** localStorage key to set mock export pause duration in Playwright tests */
|
||||
export const PLAYWRIGHT_MOCK_EXPORT_DURATION = 'playwright-mock-export-duration'
|
||||
|
@ -11,6 +11,7 @@ import { APP_VERSION } from 'routes/Settings'
|
||||
import { UAParser } from 'ua-parser-js'
|
||||
import screenshot from 'lib/screenshot'
|
||||
import { VITE_KC_API_BASE_URL } from 'env'
|
||||
import CodeManager from 'lang/codeManager'
|
||||
|
||||
/* eslint-disable suggest-no-throw/suggest-no-throw --
|
||||
* All the throws in CoreDumpManager are intentional and should be caught and handled properly
|
||||
@ -32,14 +33,17 @@ import { VITE_KC_API_BASE_URL } from 'env'
|
||||
// TODO: Throw more
|
||||
export class CoreDumpManager {
|
||||
engineCommandManager: EngineCommandManager
|
||||
codeManager: CodeManager
|
||||
token: string | undefined
|
||||
baseUrl: string = VITE_KC_API_BASE_URL
|
||||
|
||||
constructor(
|
||||
engineCommandManager: EngineCommandManager,
|
||||
codeManager: CodeManager,
|
||||
token: string | undefined
|
||||
) {
|
||||
this.engineCommandManager = engineCommandManager
|
||||
this.codeManager = codeManager
|
||||
this.token = token
|
||||
}
|
||||
|
||||
@ -61,6 +65,10 @@ export class CoreDumpManager {
|
||||
return APP_VERSION
|
||||
}
|
||||
|
||||
kclCode(): string {
|
||||
return this.codeManager.code
|
||||
}
|
||||
|
||||
// Get the backend pool we've requested.
|
||||
pool(): string {
|
||||
return this.engineCommandManager.settings.pool || ''
|
||||
|
41
src/lib/machine-api.d.ts
vendored
41
src/lib/machine-api.d.ts
vendored
@ -264,6 +264,33 @@ export interface components {
|
||||
} & {
|
||||
[key: string]: unknown
|
||||
})
|
||||
| ({
|
||||
/** @enum {string} */
|
||||
command: 'ams_change_filament'
|
||||
/**
|
||||
* Format: int64
|
||||
* @description The error number.
|
||||
*/
|
||||
errorno: number
|
||||
/** @description The reason for the message. */
|
||||
reason?: components['schemas']['Reason'] | null
|
||||
/** @description The result of the command. */
|
||||
result: components['schemas']['Result']
|
||||
/** @description The sequence id. */
|
||||
sequence_id: components['schemas']['SequenceId']
|
||||
/**
|
||||
* Format: int64
|
||||
* @description The target temperature.
|
||||
*/
|
||||
tar_temp: number
|
||||
/**
|
||||
* Format: int64
|
||||
* @description The target.
|
||||
*/
|
||||
target: number
|
||||
} & {
|
||||
[key: string]: unknown
|
||||
})
|
||||
| ({
|
||||
/** @enum {string} */
|
||||
command: 'calibration'
|
||||
@ -528,6 +555,20 @@ export interface components {
|
||||
} & {
|
||||
[key: string]: unknown
|
||||
})
|
||||
| ({
|
||||
/** @enum {string} */
|
||||
command: 'print_speed'
|
||||
/** @description The param. */
|
||||
param: string
|
||||
/** @description The reason for the message. */
|
||||
reason?: components['schemas']['Reason'] | null
|
||||
/** @description The result of the command. */
|
||||
result: components['schemas']['Result']
|
||||
/** @description The sequence id. */
|
||||
sequence_id: components['schemas']['SequenceId']
|
||||
} & {
|
||||
[key: string]: unknown
|
||||
})
|
||||
| ({
|
||||
/** @enum {string} */
|
||||
command: 'resume'
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { appConfigDir } from '@tauri-apps/api/path'
|
||||
import { appConfigDir, join } from '@tauri-apps/api/path'
|
||||
import { isTauri } from './isTauri'
|
||||
import type { FileEntry } from 'lib/types'
|
||||
import {
|
||||
FILE_EXT,
|
||||
INDEX_IDENTIFIER,
|
||||
MAX_PADDING,
|
||||
ONBOARDING_PROJECT_NAME,
|
||||
@ -15,6 +16,7 @@ import {
|
||||
readAppSettingsFile,
|
||||
} from './tauri'
|
||||
import { engineCommandManager } from './singletons'
|
||||
import { exists } from '@tauri-apps/plugin-fs'
|
||||
|
||||
export const isHidden = (fileOrDir: FileEntry) =>
|
||||
!!fileOrDir.name?.startsWith('.')
|
||||
@ -162,3 +164,56 @@ export async function createAndOpenNewProject({
|
||||
)
|
||||
return newProject
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next available file name by appending a hyphen and number to the end of the name
|
||||
* @todo move this to the equivalent of tauriFS.ts for Electron migration
|
||||
*/
|
||||
export async function getNextFileName({
|
||||
entryName,
|
||||
baseDir,
|
||||
}: {
|
||||
entryName: string
|
||||
baseDir: string
|
||||
}) {
|
||||
// Remove any existing index from the name before adding a new one
|
||||
let createdName = entryName.replace(FILE_EXT, '') + FILE_EXT
|
||||
let createdPath = await join(baseDir, createdName)
|
||||
let i = 1
|
||||
while (await exists(createdPath)) {
|
||||
const matchOnIndexAndExtension = new RegExp(`(-\\d+)?(${FILE_EXT})?$`)
|
||||
createdName =
|
||||
entryName.replace(matchOnIndexAndExtension, '') + `-${i}` + FILE_EXT
|
||||
createdPath = await join(baseDir, createdName)
|
||||
i++
|
||||
}
|
||||
return {
|
||||
name: createdName,
|
||||
path: createdPath,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next available directory name by appending a hyphen and number to the end of the name
|
||||
* @todo move this to the equivalent of tauriFS.ts for Electron migration
|
||||
*/
|
||||
export async function getNextDirName({
|
||||
entryName,
|
||||
baseDir,
|
||||
}: {
|
||||
entryName: string
|
||||
baseDir: string
|
||||
}) {
|
||||
let createdName = entryName
|
||||
let createdPath = await join(baseDir, createdName)
|
||||
let i = 1
|
||||
while (await exists(createdPath)) {
|
||||
createdName = entryName.replace(/-\d+$/, '') + `-${i}`
|
||||
createdPath = await join(baseDir, createdName)
|
||||
i++
|
||||
}
|
||||
return {
|
||||
name: createdName,
|
||||
path: createdPath,
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import crossPlatformFetch from './crossPlatformFetch'
|
||||
import { isTauri } from './isTauri'
|
||||
import { Themes } from './theme'
|
||||
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||
import { getNextFileName } from './tauriFS'
|
||||
|
||||
export async function submitTextToCadPrompt(
|
||||
prompt: string,
|
||||
@ -91,6 +92,7 @@ export async function submitAndAwaitTextToKcl({
|
||||
toast.error(
|
||||
() =>
|
||||
ToastTextToCadError({
|
||||
toastId,
|
||||
message,
|
||||
commandBarSend,
|
||||
prompt: trimmedPrompt,
|
||||
@ -165,7 +167,7 @@ export async function submitAndAwaitTextToKcl({
|
||||
showFailureToast('Failed to generate parametric model')
|
||||
return e
|
||||
})
|
||||
.then((value) => {
|
||||
.then(async (value) => {
|
||||
if (value.code === undefined || !value.code || value.code.length === 0) {
|
||||
// We want to show the real error message to the user.
|
||||
if (value.error && value.error.length > 0) {
|
||||
@ -179,13 +181,23 @@ export async function submitAndAwaitTextToKcl({
|
||||
}
|
||||
|
||||
const TRUNCATED_PROMPT_LENGTH = 24
|
||||
const newFileName = `${value.prompt
|
||||
let newFileName = `${value.prompt
|
||||
.slice(0, TRUNCATED_PROMPT_LENGTH)
|
||||
.replace(/\s/gi, '-')
|
||||
.replace(/\W/gi, '-')
|
||||
.toLowerCase()}`
|
||||
.toLowerCase()}${FILE_EXT}`
|
||||
|
||||
if (isTauri()) {
|
||||
// We have to pre-emptively run our unique file name logic,
|
||||
// so that we can pass the unique file name to the toast,
|
||||
// and by extension the file-deletion-on-reject logic.
|
||||
newFileName = (
|
||||
await getNextFileName({
|
||||
entryName: newFileName,
|
||||
baseDir: context.selectedDirectory.path,
|
||||
})
|
||||
).name
|
||||
|
||||
fileMachineSend({
|
||||
type: 'Create file',
|
||||
data: {
|
||||
@ -193,14 +205,13 @@ export async function submitAndAwaitTextToKcl({
|
||||
makeDir: false,
|
||||
content: value.code,
|
||||
silent: true,
|
||||
makeUnique: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...value,
|
||||
fileName: newFileName + FILE_EXT,
|
||||
fileName: newFileName,
|
||||
}
|
||||
})
|
||||
|
||||
@ -214,6 +225,7 @@ export async function submitAndAwaitTextToKcl({
|
||||
toast.success(
|
||||
() =>
|
||||
ToastTextToCadSuccess({
|
||||
toastId,
|
||||
data: textToCadOutputCreated,
|
||||
token,
|
||||
navigate,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user