Compare commits

...

20 Commits

Author SHA1 Message Date
f441998f1a updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-20 11:59:01 -07:00
531496420e Cut release v0.24.13 (#3571)
* bump version;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* actual veriosn

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-20 11:57:18 -07:00
13bb482904 updates
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-20 11:25:00 -07:00
562959ee22 skip windows shit
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-20 11:19:56 -07:00
b044f6faef Fix CPU-driven churn once Text-to-CAD toast appears in the app (#3523)
* Dispose of requestAnimationFrame loop when component unmounts

* Only run requestAnimationFrame loop when mouse is on canvas

* Better animation loop disposal on canvas mouseout

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Text-to-cad test flakiness

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)

* Re-run CI

* Remove arbitrary timeout which may cause us to miss the toast on a fast-running test

* Remove a couple more arbitrary timeouts in text-to-cad tests

* Remove all the arbitrary 5s awaits from these tests

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-20 11:03:42 -07:00
d916c79874 Cut release v0.24.12 (#3458)
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-08-15 18:30:12 -04:00
4fd5e26abe Move KCL from Rust strings into files (#3467) 2024-08-15 16:37:42 -05:00
8f9bef922f Put back the position on the .cm-content click 2024-08-15 21:17:51 +02:00
545e610bbc Always give new files and dirs a new index if their names are taken (#3460)
* Add actual support for makeUnique parameter

* Add uniqueness logic to dirs, make text-to-cad receive unique filename

* No longer need makeUnique flag, it's always on

* fmt

* Don't show toast when name hasn't changed during a rename

* fmt

* Get "interact with many" text-to-cad test passing again

* Get "engine fails export" back to reliably green

* Maybe more stable click target for text-to-cad test

* Make "export is already going" test moderately more reliable

* Mark "export is already going" as fixme

* Undo that fixme thing I take it back
2024-08-15 14:24:27 -04:00
55a3e2a4ed add lite to benchmarks as well (#3464)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-15 10:32:55 -07:00
591f17b182 Make Basic default modeling and sketch hotkeys work E2E test more reliable (#3461)
* Make hotkeys E2E test more reliable

* Fixes
2024-08-15 09:40:28 -04:00
a7a88bd762 remove flakey has no pending logic, let them do whatever they want (#3457)
* remove flakey has no pending logic

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add test for many at once w dismiss bug

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* toastid

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixup more tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-14 23:08:37 -07:00
0916f990cb Fix sketch groups and extrude groups when used inside objects (#3439)
* Fix SketchGroups and ExtrudeGroups to work with user objects

* Fix to never clone more than once

* Fix error messages to be more helpful

* Add test
2024-08-14 22:37:33 -07:00
75ae4b4a4a Fix bisect steps to clean out old generated files (#3428) 2024-08-14 22:28:33 -07:00
4a490d5900 Bump html2canvas-pro from 1.5.5 to 1.5.8 (#3453)
Bumps [html2canvas-pro](https://github.com/yorickshan/html2canvas-pro) from 1.5.5 to 1.5.8.
- [Release notes](https://github.com/yorickshan/html2canvas-pro/releases)
- [Changelog](https://github.com/yorickshan/html2canvas-pro/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yorickshan/html2canvas-pro/compare/v1.5.5...v1.5.8)

---
updated-dependencies:
- dependency-name: html2canvas-pro
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-14 21:59:05 -07:00
4d9cdc6b40 tests for weird text-to-cad toast (#3448)
* tests for weird text-to-cad toast

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* test the inverse

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-14 21:39:55 -07:00
0d3880233c Bump serde_tokenstream from 0.2.1 to 0.2.2 in /src/wasm-lib (#3449)
Bumps [serde_tokenstream](https://github.com/oxidecomputer/serde_tokenstream) from 0.2.1 to 0.2.2.
- [Release notes](https://github.com/oxidecomputer/serde_tokenstream/releases)
- [Commits](https://github.com/oxidecomputer/serde_tokenstream/compare/v0.2.1...v0.2.2)

---
updated-dependencies:
- dependency-name: serde_tokenstream
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-14 21:15:25 -07:00
8a029605bd Kcl in coredump (#3434)
* add kcl code to coredump

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix ts

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* new images

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-14 17:56:28 -07:00
f26adee360 Update machine-api spec (#3441)
* YOYO NEW API SPEC!

* New machine-api types

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-14 17:26:41 -07:00
0f2a01b6c8 Update machine-api spec (#3438)
* YOYO NEW API SPEC!

* New machine-api types

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-14 16:59:39 -07:00
322 changed files with 2515 additions and 454 deletions

View File

@ -4,7 +4,7 @@ on:
pull_request: pull_request:
push: push:
branches: branches:
- main - tauri
release: release:
types: [published] types: [published]
schedule: schedule:
@ -412,17 +412,6 @@ jobs:
E2E_APPLICATION: "./src-tauri/target/${{ env.BUILD_RELEASE == 'true' && 'release' || 'debug' }}/zoo-modeling-app" 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 }} 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 - uses: actions/download-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }} if: ${{ env.CUT_RELEASE_PR == 'true' }}

View File

@ -3,7 +3,7 @@ name: Create Release
on: on:
push: push:
branches: branches:
- main - tauri
jobs: jobs:
create-release: create-release:

View File

@ -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: The following will need to be run when checking out a new commit and guarantees the build is not stale:
```bash ```bash
yarn install yarn install
yarn wasm-prep
yarn build:wasm-dev # or yarn build:wasm for slower but more production-like build 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 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

View File

@ -3,6 +3,10 @@ import { test, expect, Page } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils' import { getUtils, setup, tearDown } from './test-utils'
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates' import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import {
PLAYWRIGHT_MOCK_EXPORT_DURATION,
PLAYWRIGHT_TOAST_DURATION,
} from 'lib/constants'
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
await setup(context, page) await setup(context, page)
@ -250,7 +254,7 @@ const sketch001 = startSketchAt([-0, -0])
await expect(exportButton).toBeVisible() await expect(exportButton).toBeVisible()
// Click the export button // Click the export button
exportButton.click() await exportButton.click()
// Click the stl. // Click the stl.
const stlOption = page.getByText('glTF') const stlOption = page.getByText('glTF')
@ -279,7 +283,7 @@ const sketch001 = startSketchAt([-0, -0])
await expect(exportingToastMessage).not.toBeVisible() await expect(exportingToastMessage).not.toBeVisible()
// Click the code editor // Click the code editor
page.locator('.cm-content').click() await page.locator('.cm-content').click()
await page.waitForTimeout(2000) await page.waitForTimeout(2000)
@ -288,8 +292,7 @@ const sketch001 = startSketchAt([-0, -0])
await expect(engineErrorToastMessage).not.toBeVisible() await expect(engineErrorToastMessage).not.toBeVisible()
// Now add in code that works. // Now add in code that works.
page.locator('.cm-content').fill(bracket) await page.locator('.cm-content').fill(bracket)
page.locator('.cm-content').click()
await page.keyboard.press('End') await page.keyboard.press('End')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
@ -302,7 +305,7 @@ const sketch001 = startSketchAt([-0, -0])
// Now try exporting // Now try exporting
// Click the export button // Click the export button
exportButton.click() await exportButton.click()
// Click the stl. // Click the stl.
await expect(stlOption).toBeVisible() await expect(stlOption).toBeVisible()
@ -330,9 +333,23 @@ const sketch001 = startSketchAt([-0, -0])
page, page,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async (code) => { await test.step('Set up the code and durations', async () => {
await page.addInitScript(
async ({ code, toastDurationKey, exportDurationKey }) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
}, bracket) // 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 })
@ -345,35 +362,42 @@ const sketch001 = startSketchAt([-0, -0])
// expect zero errors in guter // expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
})
const errorToastMessage = page.getByText(`Error while exporting`) const errorToastMessage = page.getByText(`Error while exporting`)
const exportingToastMessage = page.getByText(`Exporting...`) const exportingToastMessage = page.getByText(`Exporting...`)
const engineErrorToastMessage = page.getByText(`Nothing to export`) const engineErrorToastMessage = page.getByText(`Nothing to export`)
const alreadyExportingToastMessage = page.getByText(`Already exporting`) const alreadyExportingToastMessage = page.getByText(`Already exporting`)
const successToastMessage = page.getByText(`Exported successfully`)
await test.step('Blocked second export', async () => {
await clickExportButton(page) await clickExportButton(page)
await expect(exportingToastMessage).toBeVisible() await expect(exportingToastMessage).toBeVisible()
await clickExportButton(page) await clickExportButton(page)
await test.step('The second export is blocked', async () => {
// Find the toast. // Find the toast.
// Look out for the toast message // Look out for the toast message
await expect(exportingToastMessage).toBeVisible() await expect(exportingToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).toBeVisible() await expect(alreadyExportingToastMessage).toBeVisible()
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
})
// Expect it to succeed. await test.step('The first export still succeeds', async () => {
await expect(exportingToastMessage).not.toBeVisible() await expect(exportingToastMessage).not.toBeVisible()
await expect(errorToastMessage).not.toBeVisible() await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible() await expect(engineErrorToastMessage).not.toBeVisible()
const successToastMessage = page.getByText(`Exported successfully`)
await expect(successToastMessage).toBeVisible() await expect(successToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).not.toBeVisible() await expect(alreadyExportingToastMessage).not.toBeVisible()
})
})
await test.step('Successful, unblocked export', async () => {
// Try exporting again. // Try exporting again.
await clickExportButton(page) await clickExportButton(page)
@ -389,18 +413,20 @@ const sketch001 = startSketchAt([-0, -0])
await expect(successToastMessage).toBeVisible() await expect(successToastMessage).toBeVisible()
}) })
})
}) })
async function clickExportButton(page: Page) { async function clickExportButton(page: Page) {
await test.step('Running export flow', async () => {
// export the model // export the model
const exportButton = page.getByTestId('export-pane-button') const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible() await expect(exportButton).toBeEnabled()
// Click the export button // Click the export button
exportButton.click() await exportButton.click()
// Click the stl. // Click the stl.
const gltfOption = page.getByText('glTF') const gltfOption = page.getByRole('option', { name: 'glTF' })
await expect(gltfOption).toBeVisible() await expect(gltfOption).toBeVisible()
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
@ -410,4 +436,5 @@ async function clickExportButton(page: Page) {
await expect(submitButton).toBeVisible() await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
})
} }

View File

@ -29,15 +29,13 @@ test.describe('Text-to-CAD tests', () => {
) )
await expect(submittingToastMessage).toBeVisible() await expect(submittingToastMessage).toBeVisible()
await page.waitForTimeout(5000)
const generatingToastMessage = page.getByText( const generatingToastMessage = page.getByText(
`Generating parametric model...` `Generating parametric model...`
) )
await expect(generatingToastMessage).toBeVisible() await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
const successToastMessage = page.getByText(`Text-to-CAD successful`) 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() await expect(page.getByText('Copied')).not.toBeVisible()
@ -78,6 +76,52 @@ test.describe('Text-to-CAD tests', () => {
await expect(successToastMessage).not.toBeVisible() 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 ({ test('you can reject text-to-cad output and it does nothing', async ({
page, page,
}) => { }) => {
@ -96,15 +140,13 @@ test.describe('Text-to-CAD tests', () => {
) )
await expect(submittingToastMessage).toBeVisible() await expect(submittingToastMessage).toBeVisible()
await page.waitForTimeout(5000)
const generatingToastMessage = page.getByText( const generatingToastMessage = page.getByText(
`Generating parametric model...` `Generating parametric model...`
) )
await expect(generatingToastMessage).toBeVisible() await expect(generatingToastMessage).toBeVisible({ timeout: 10000 })
const successToastMessage = page.getByText(`Text-to-CAD successful`) const successToastMessage = page.getByText(`Text-to-CAD successful`)
await expect(successToastMessage).toBeVisible() await expect(successToastMessage).toBeVisible({ timeout: 15000 })
// Hit copy to clipboard. // Hit copy to clipboard.
const rejectButton = page.getByRole('button', { name: 'Reject' }) const rejectButton = page.getByRole('button', { name: 'Reject' })
@ -184,7 +226,9 @@ test.describe('Text-to-CAD tests', () => {
await expect(failureToastMessage).not.toBeVisible() 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) const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 }) await page.setViewportSize({ width: 1000, height: 500 })
@ -266,11 +310,86 @@ test.describe('Text-to-CAD tests', () => {
// Look out for the toast message // Look out for the toast message
await expect(submittingToastMessage).toBeVisible() 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(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 }) => { 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 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( const generatingToastMessage = page.getByText(
`Generating parametric model...` `Generating parametric model...`
) )
await expect(generatingToastMessage).toBeVisible() await expect(generatingToastMessage.first()).toBeVisible({ timeout: 10000 })
await page.waitForTimeout(5000)
const successToastMessage = page.getByText(`Text-to-CAD successful`) 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') const cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible() 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() await expect(textToCadCommand.first()).toBeVisible()
// Click the Text-to-CAD command // Click the Text-to-CAD command
await textToCadCommand.first().click() await textToCadCommand.first().click()

View File

@ -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 }) => { 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 // This test can run long if it takes a little too long to load
// the engine. // the engine.
test.setTimeout(90000) test.setTimeout(90000)
@ -273,6 +275,8 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
'weird playwright bug on ubuntu https://github.com/KittyCAD/modeling-app/issues/2444' 'weird playwright bug on ubuntu https://github.com/KittyCAD/modeling-app/issues/2444'
) )
// Load the app with the code pane open // Load the app with the code pane open
await test.step(`Set up test`, async () => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'store', 'store',
@ -284,58 +288,35 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
}) })
) )
}) })
// Wait for the app to be ready for use
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel() await u.closeDebugPanel()
})
const codePane = page.getByRole('textbox').locator('div') const codePane = page.locator('.cm-content')
const codePaneButton = page.getByTestId('code-pane-button')
const lineButton = page.getByRole('button', { name: 'Line', exact: true }) const lineButton = page.getByRole('button', { name: 'Line', exact: true })
const arcButton = page.getByRole('button', { const arcButton = page.getByRole('button', {
name: 'Tangential Arc', name: 'Tangential Arc',
exact: true, exact: true,
}) })
const extrudeButton = page.getByRole('button', { name: 'Extrude' }) 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 await test.step(`Type code with modeling hotkeys, shouldn't fire`, async () => {
// focus is on the code pane
await codePane.click() await codePane.click()
await page.keyboard.press('/') await page.keyboard.type('//')
await page.keyboard.press('/')
await page.keyboard.press('s') await page.keyboard.press('s')
await page.keyboard.press('l') await expect(commandBarComboBox).not.toBeVisible()
await page.keyboard.press('a')
await page.keyboard.press('e') await page.keyboard.press('e')
await expect(page.locator('.cm-content')).toHaveText('//slae') await expect(commandBarComboBox).not.toBeVisible()
await page.keyboard.press('Meta+/') await expect(codePane).toHaveText('//se')
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,
}) })
// 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 * TODO: There is a bug somewhere that causes this test to fail
* if you toggle the codePane closed before your trigger the * if you toggle the codePane closed before your trigger the
@ -345,59 +326,111 @@ test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
* has pinpointed this to the unusual browser behavior: * 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 * https://discuss.codemirror.net/t/how-to-force-unfocus-of-the-codemirror-element-in-safari/8095/3
*/ */
await codePaneButton.click() await blurCodeEditor()
await expect(u.codeLocator).not.toBeVisible() await page.waitForTimeout(1000)
await page.waitForTimeout(300) 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,
})
})
// Draw a line // 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.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await page.waitForTimeout(300)
await page.mouse.move(800, 250, { steps: 5 }) await page.mouse.move(800, 250, { steps: 5 })
await page.mouse.click(800, 250) await page.mouse.click(800, 250)
// Unequip line tool })
await test.step(`Unequip line tool`, async () => {
await page.keyboard.press('l') await page.keyboard.press('l')
await expect(lineButton).not.toHaveAttribute('aria-pressed', 'true') await expect(lineButton).not.toHaveAttribute('aria-pressed', 'true')
// Equip arc tool })
await test.step(`Draw a tangential arc`, async () => {
await page.keyboard.press('a') await page.keyboard.press('a')
await expect(arcButton).toHaveAttribute('aria-pressed', 'true', { await expect(arcButton).toHaveAttribute('aria-pressed', 'true', {
timeout: 10_000, timeout: 10_000,
}) })
await page.mouse.move(1000, 100, { steps: 5 }) await page.mouse.move(1000, 100, { steps: 5 })
await page.mouse.click(1000, 100) 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('Escape')
await page.keyboard.press('l') await page.keyboard.press('l')
await page.waitForTimeout(50)
await expect(lineButton).toHaveAttribute('aria-pressed', 'true') await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
// Close profile })
})
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.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
// On close it will unequip the line tool. // On close it will unequip the line tool.
await expect(lineButton).toHaveAttribute('aria-pressed', 'false') await expect(lineButton).toHaveAttribute('aria-pressed', 'false')
// Exit sketch await expect(exitSketchButton).toBeEnabled()
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
await expect( await expect(
page.getByRole('button', { name: 'Exit Sketch' }) page.getByRole('button', { name: 'Exit Sketch' })
).not.toBeVisible() ).not.toBeVisible()
await page.waitForTimeout(400) })
// Extrude // Extrude with e
await test.step(`Extrude the sketch`, async () => {
await page.mouse.click(750, 150) await page.mouse.click(750, 150)
await expect(extrudeButton).not.toBeDisabled() await blurCodeEditor()
await expect(extrudeButton).toBeEnabled()
await page.keyboard.press('e') await page.keyboard.press('e')
await page.waitForTimeout(100) await page.waitForTimeout(500)
await page.mouse.move(800, 200, { steps: 5 }) await page.mouse.move(800, 200, { steps: 5 })
await page.mouse.click(800, 200) await page.mouse.click(800, 200)
await page.waitForTimeout(300) await page.waitForTimeout(500)
await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible() await expect(page.getByRole('button', { name: 'Continue' })).toBeVisible()
await page.getByRole('button', { name: 'Continue' }).click() await page.getByRole('button', { name: 'Continue' }).click()
await page.waitForTimeout(300)
await expect( await expect(
page.getByRole('button', { name: 'Submit command' }) page.getByRole('button', { name: 'Submit command' })
).toBeVisible() ).toBeVisible()
await page.getByRole('button', { name: 'Submit command' }).click() await page.getByRole('button', { name: 'Submit command' }).click()
await codePaneButton.click()
await expect(page.locator('.cm-content')).toContainText('extrude(') await expect(page.locator('.cm-content')).toContainText('extrude(')
})
// await codePaneButton.click()
// await expect(u.codeLocator).not.toBeVisible()
/**
* work-around: to stop `keyboard.press()` from typing in the editor even when it should be blurred
*/
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 }) => { test('Delete key does not navigate back', async ({ page }) => {

View File

@ -618,6 +618,67 @@
], ],
"type": "object" "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, "additionalProperties": true,
"description": "Calibration.", "description": "Calibration.",
@ -1192,6 +1253,54 @@
], ],
"type": "object" "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, "additionalProperties": true,
"description": "Resume the print.", "description": "Resume the print.",

View File

@ -1,6 +1,6 @@
{ {
"name": "untitled-app", "name": "untitled-app",
"version": "0.24.11", "version": "0.24.13",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.17.0", "@codemirror/autocomplete": "^6.17.0",
@ -37,7 +37,7 @@
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.5", "html2canvas-pro": "^1.5.8",
"json-rpc-2.0": "^1.6.0", "json-rpc-2.0": "^1.6.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"re-resizable": "^6.9.11", "re-resizable": "^6.9.11",

View File

@ -80,5 +80,5 @@
} }
}, },
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"version": "0.24.11" "version": "0.24.13"
} }

View File

@ -13,7 +13,7 @@ import { PATHS } from 'lib/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions' import { useEngineConnectionSubscriptions } from 'hooks/useEngineConnectionSubscriptions'
import { engineCommandManager } from 'lib/singletons' import { codeManager, engineCommandManager } from 'lib/singletons'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
@ -50,7 +50,7 @@ export function App() {
const token = auth?.context?.token const token = auth?.context?.token
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),
[] []
) )

View File

@ -34,7 +34,7 @@ import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants' import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { getState, setState } from 'lib/tauri' import { getState, setState } from 'lib/tauri'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { engineCommandManager } from 'lib/singletons' import { codeManager, engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useHotkeyWrapper from 'lib/hotkeyWrapper' import useHotkeyWrapper from 'lib/hotkeyWrapper'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
@ -181,7 +181,7 @@ function CoreDump() {
const { auth } = useSettingsAuthContext() const { auth } = useSettingsAuthContext()
const token = auth?.context?.token const token = auth?.context?.token
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),
[] []
) )
useHotkeyWrapper(['meta + shift + .'], () => { useHotkeyWrapper(['meta + shift + .'], () => {

View File

@ -21,11 +21,13 @@ import {
rename, rename,
create, create,
writeTextFile, writeTextFile,
exists,
} from '@tauri-apps/plugin-fs' } from '@tauri-apps/plugin-fs'
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { join, sep } from '@tauri-apps/api/path' import { join, sep } from '@tauri-apps/api/path'
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants' import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
import { getProjectInfo } from 'lib/tauri' import { getProjectInfo } from 'lib/tauri'
import { getNextDirName, getNextFileName } from 'lib/tauriFS'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -105,14 +107,20 @@ export const FileMachineProvider = ({
let createdPath: string let createdPath: string
if (event.data.makeDir) { 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) await mkdir(createdPath)
} else { } else {
createdPath = const { name, path } = await getNextFileName({
context.selectedDirectory.path + entryName: createdName,
sep() + baseDir: context.selectedDirectory.path,
createdName + })
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT) createdName = name
createdPath = path
await create(createdPath) await create(createdPath)
if (event.data.content) { if (event.data.content) {
await writeTextFile(createdPath, event.data.content) await writeTextFile(createdPath, event.data.content)
@ -129,14 +137,20 @@ export const FileMachineProvider = ({
let createdPath: string let createdPath: string
if (event.data.makeDir) { 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) await mkdir(createdPath)
} else { } else {
createdPath = const { name, path } = await getNextFileName({
context.selectedDirectory.path + entryName: createdName,
sep() + baseDir: context.selectedDirectory.path,
createdName + })
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT) createdName = name
createdPath = path
await create(createdPath) await create(createdPath)
if (event.data.content) { if (event.data.content) {
await writeTextFile(createdPath, event.data.content) await writeTextFile(createdPath, event.data.content)

View File

@ -382,11 +382,17 @@ export const FileTreeMenu = () => {
const { send } = useFileContext() const { send } = useFileContext()
async function createFile() { async function createFile() {
send({ type: 'Create file', data: { name: '', makeDir: false } }) send({
type: 'Create file',
data: { name: '', makeDir: false },
})
} }
async function createFolder() { async function createFolder() {
send({ type: 'Create file', data: { name: '', makeDir: true } }) send({
type: 'Create file',
data: { name: '', makeDir: true },
})
} }
useHotkeyWrapper(['meta + n'], createFile) useHotkeyWrapper(['meta + n'], createFile)

View File

@ -85,6 +85,7 @@ import {
} from 'lang/std/engineConnection' } from 'lang/std/engineConnection'
import { submitAndAwaitTextToKcl } from 'lib/textToCad' import { submitAndAwaitTextToKcl } from 'lib/textToCad'
import { useFileContext } from 'hooks/useFileContext' import { useFileContext } from 'hooks/useFileContext'
import { PLAYWRIGHT_MOCK_EXPORT_DURATION } from 'lib/constants'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -393,10 +394,24 @@ export const ModelingMachineProvider = ({
selection: { type: 'default_scene' }, 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( toast.promise(
Promise.all([
exportFromEngine({ exportFromEngine({
format: format, format: format,
}), }),
mockExportDuration
? new Promise((resolve) =>
setTimeout(resolve, Number(mockExportDuration))
)
: Promise.resolve(),
]),
{ {
loading: 'Starting print...', loading: 'Starting print...',
success: 'Started print successfully', 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 }) => { 'Submit to Text-to-CAD API': async (_, { data }) => {
const trimmedPrompt = data.prompt.trim() const trimmedPrompt = data.prompt.trim()
if (!trimmedPrompt) return if (!trimmedPrompt) return
@ -549,8 +558,6 @@ export const ModelingMachineProvider = ({
return false return false
} }
}, },
'Has no pending text-to-cad submissions': ({ pendingTextToCad }) =>
!pendingTextToCad,
}, },
services: { services: {
'AST-undo-startSketchOn': async ({ sketchDetails }) => { 'AST-undo-startSketchOn': async ({ sketchDetails }) => {

View File

@ -1,7 +1,7 @@
import { coreDump } from 'lang/wasm' import { coreDump } from 'lang/wasm'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { engineCommandManager } from 'lib/singletons' import { codeManager, engineCommandManager } from 'lib/singletons'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
@ -11,7 +11,7 @@ export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext() const { auth } = useSettingsAuthContext()
const token = auth?.context?.token const token = auth?.context?.token
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),
[] []
) )

View File

@ -6,7 +6,7 @@ import { PATHS } from 'lib/paths'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { sep } from '@tauri-apps/api/path' import { sep } from '@tauri-apps/api/path'
import { TextToCad_type } from '@kittycad/lib/dist/types/src/models' import { TextToCad_type } from '@kittycad/lib/dist/types/src/models'
import { useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { import {
Box3, Box3,
Color, Color,
@ -36,10 +36,12 @@ const FRUSTUM_SIZE = 0.5
const OUTPUT_KEY = 'source.glb' const OUTPUT_KEY = 'source.glb'
export function ToastTextToCadError({ export function ToastTextToCadError({
toastId,
message, message,
prompt, prompt,
commandBarSend, commandBarSend,
}: { }: {
toastId: string
message: string message: string
prompt: string prompt: string
commandBarSend: ( commandBarSend: (
@ -63,7 +65,7 @@ export function ToastTextToCadError({
}} }}
name="Dismiss" name="Dismiss"
onClick={() => { onClick={() => {
toast.dismiss() toast.dismiss(toastId)
}} }}
> >
Dismiss Dismiss
@ -85,7 +87,7 @@ export function ToastTextToCadError({
}, },
}, },
}) })
toast.dismiss() toast.dismiss(toastId)
}} }}
> >
Edit prompt Edit prompt
@ -96,6 +98,7 @@ export function ToastTextToCadError({
} }
export function ToastTextToCadSuccess({ export function ToastTextToCadSuccess({
toastId,
data, data,
navigate, navigate,
context, context,
@ -103,7 +106,7 @@ export function ToastTextToCadSuccess({
fileMachineSend, fileMachineSend,
settings, settings,
}: { }: {
// TODO: update this type to match the actual data when API is done toastId: string
data: TextToCad_type & { fileName: string } data: TextToCad_type & { fileName: string }
navigate: (to: string) => void navigate: (to: string) => void
context: ReturnType<typeof useFileContext>['context'] context: ReturnType<typeof useFileContext>['context']
@ -119,10 +122,40 @@ export function ToastTextToCadSuccess({
}) { }) {
const wrapperRef = useRef<HTMLDivElement | null>(null) const wrapperRef = useRef<HTMLDivElement | null>(null)
const canvasRef = useRef<HTMLCanvasElement | null>(null) const canvasRef = useRef<HTMLCanvasElement | null>(null)
const animationRequestRef = useRef<number>()
const [hasCopied, setHasCopied] = useState(false) const [hasCopied, setHasCopied] = useState(false)
const [showCopiedUi, setShowCopiedUi] = useState(false) const [showCopiedUi, setShowCopiedUi] = useState(false)
const modelId = data.id 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(() => { useEffect(() => {
if (!canvasRef.current) return if (!canvasRef.current) return
@ -130,7 +163,6 @@ export function ToastTextToCadSuccess({
const renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true }) const renderer = new WebGLRenderer({ canvas, antialias: true, alpha: true })
renderer.setSize(CANVAS_SIZE, CANVAS_SIZE) renderer.setSize(CANVAS_SIZE, CANVAS_SIZE)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.setAnimationLoop(animate)
const scene = new Scene() const scene = new Scene()
const ambientLight = new DirectionalLight(new Color('white'), 8.0) const ambientLight = new DirectionalLight(new Color('white'), 8.0)
@ -153,13 +185,6 @@ export function ToastTextToCadSuccess({
return return
} }
function animate() {
requestAnimationFrame(animate)
// required if controls.enableDamping or controls.autoRotate are set to true
controls.update()
renderer.render(scene, camera)
}
loader.parse( loader.parse(
buffer, buffer,
'', '',
@ -210,6 +235,8 @@ export function ToastTextToCadSuccess({
camera.updateProjectionMatrix() camera.updateProjectionMatrix()
controls.update() controls.update()
// render the scene once...
renderer.render(scene, camera)
}, },
// called when loading has errors // called when loading has errors
function (error) { 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 () => { return () => {
renderer.dispose() 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'} {hasCopied ? 'Close' : 'Reject'}
@ -284,7 +329,7 @@ export function ToastTextToCadSuccess({
`${context.project.path}${sep()}${data.fileName}` `${context.project.path}${sep()}${data.fileName}`
)}` )}`
) )
toast.dismiss() toast.dismiss(toastId)
}} }}
> >
Accept Accept

View File

@ -14,6 +14,7 @@ import {
createUpdaterRestartModal, createUpdaterRestartModal,
} from 'components/UpdaterRestartModal' } from 'components/UpdaterRestartModal'
import { AppStreamProvider } from 'AppState' import { AppStreamProvider } from 'AppState'
import { PLAYWRIGHT_KEY, PLAYWRIGHT_TOAST_DURATION } from 'lib/constants'
// uncomment for xstate inspector // uncomment for xstate inspector
// import { DEV } from 'env' // import { DEV } from 'env'
@ -24,6 +25,9 @@ import { AppStreamProvider } from 'AppState'
// }) // })
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
const maybePlaywrightToastDuration = Number(
window?.localStorage.getItem(PLAYWRIGHT_TOAST_DURATION)
)
root.render( root.render(
<HotkeysProvider> <HotkeysProvider>
@ -44,8 +48,10 @@ root.render(
secondary: 'oklch(48.62% 0.1654 142.5deg)', secondary: 'oklch(48.62% 0.1654 142.5deg)',
}, },
duration: duration:
window?.localStorage.getItem('playwright') === 'true' window?.localStorage.getItem(PLAYWRIGHT_KEY) === 'true'
? 10 // speed up e2e tests ? maybePlaywrightToastDuration > 0
? maybePlaywrightToastDuration
: 10 // optionally speed up e2e tests
: 1500, : 1500,
}, },
}} }}

View File

@ -56,3 +56,10 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
/** The default KCL length expression */ /** The default KCL length expression */
export const KCL_DEFAULT_LENGTH = `5` export const KCL_DEFAULT_LENGTH = `5`
export const COOKIE_NAME = '__Secure-next-auth.session-token' 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'

View File

@ -11,6 +11,7 @@ import { APP_VERSION } from 'routes/Settings'
import { UAParser } from 'ua-parser-js' import { UAParser } from 'ua-parser-js'
import screenshot from 'lib/screenshot' import screenshot from 'lib/screenshot'
import { VITE_KC_API_BASE_URL } from 'env' import { VITE_KC_API_BASE_URL } from 'env'
import CodeManager from 'lang/codeManager'
/* eslint-disable suggest-no-throw/suggest-no-throw -- /* eslint-disable suggest-no-throw/suggest-no-throw --
* All the throws in CoreDumpManager are intentional and should be caught and handled properly * 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 // TODO: Throw more
export class CoreDumpManager { export class CoreDumpManager {
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
codeManager: CodeManager
token: string | undefined token: string | undefined
baseUrl: string = VITE_KC_API_BASE_URL baseUrl: string = VITE_KC_API_BASE_URL
constructor( constructor(
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
codeManager: CodeManager,
token: string | undefined token: string | undefined
) { ) {
this.engineCommandManager = engineCommandManager this.engineCommandManager = engineCommandManager
this.codeManager = codeManager
this.token = token this.token = token
} }
@ -61,6 +65,10 @@ export class CoreDumpManager {
return APP_VERSION return APP_VERSION
} }
kclCode(): string {
return this.codeManager.code
}
// Get the backend pool we've requested. // Get the backend pool we've requested.
pool(): string { pool(): string {
return this.engineCommandManager.settings.pool || '' return this.engineCommandManager.settings.pool || ''

View File

@ -264,6 +264,33 @@ export interface components {
} & { } & {
[key: string]: unknown [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} */ /** @enum {string} */
command: 'calibration' command: 'calibration'
@ -528,6 +555,20 @@ export interface components {
} & { } & {
[key: string]: unknown [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} */ /** @enum {string} */
command: 'resume' command: 'resume'

View File

@ -1,7 +1,8 @@
import { appConfigDir } from '@tauri-apps/api/path' import { appConfigDir, join } from '@tauri-apps/api/path'
import { isTauri } from './isTauri' import { isTauri } from './isTauri'
import type { FileEntry } from 'lib/types' import type { FileEntry } from 'lib/types'
import { import {
FILE_EXT,
INDEX_IDENTIFIER, INDEX_IDENTIFIER,
MAX_PADDING, MAX_PADDING,
ONBOARDING_PROJECT_NAME, ONBOARDING_PROJECT_NAME,
@ -15,6 +16,7 @@ import {
readAppSettingsFile, readAppSettingsFile,
} from './tauri' } from './tauri'
import { engineCommandManager } from './singletons' import { engineCommandManager } from './singletons'
import { exists } from '@tauri-apps/plugin-fs'
export const isHidden = (fileOrDir: FileEntry) => export const isHidden = (fileOrDir: FileEntry) =>
!!fileOrDir.name?.startsWith('.') !!fileOrDir.name?.startsWith('.')
@ -162,3 +164,56 @@ export async function createAndOpenNewProject({
) )
return newProject 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,
}
}

View File

@ -13,6 +13,7 @@ import crossPlatformFetch from './crossPlatformFetch'
import { isTauri } from './isTauri' import { isTauri } from './isTauri'
import { Themes } from './theme' import { Themes } from './theme'
import { commandBarMachine } from 'machines/commandBarMachine' import { commandBarMachine } from 'machines/commandBarMachine'
import { getNextFileName } from './tauriFS'
export async function submitTextToCadPrompt( export async function submitTextToCadPrompt(
prompt: string, prompt: string,
@ -91,6 +92,7 @@ export async function submitAndAwaitTextToKcl({
toast.error( toast.error(
() => () =>
ToastTextToCadError({ ToastTextToCadError({
toastId,
message, message,
commandBarSend, commandBarSend,
prompt: trimmedPrompt, prompt: trimmedPrompt,
@ -165,7 +167,7 @@ export async function submitAndAwaitTextToKcl({
showFailureToast('Failed to generate parametric model') showFailureToast('Failed to generate parametric model')
return e return e
}) })
.then((value) => { .then(async (value) => {
if (value.code === undefined || !value.code || value.code.length === 0) { if (value.code === undefined || !value.code || value.code.length === 0) {
// We want to show the real error message to the user. // We want to show the real error message to the user.
if (value.error && value.error.length > 0) { if (value.error && value.error.length > 0) {
@ -179,13 +181,23 @@ export async function submitAndAwaitTextToKcl({
} }
const TRUNCATED_PROMPT_LENGTH = 24 const TRUNCATED_PROMPT_LENGTH = 24
const newFileName = `${value.prompt let newFileName = `${value.prompt
.slice(0, TRUNCATED_PROMPT_LENGTH) .slice(0, TRUNCATED_PROMPT_LENGTH)
.replace(/\s/gi, '-') .replace(/\s/gi, '-')
.replace(/\W/gi, '-') .replace(/\W/gi, '-')
.toLowerCase()}` .toLowerCase()}${FILE_EXT}`
if (isTauri()) { 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({ fileMachineSend({
type: 'Create file', type: 'Create file',
data: { data: {
@ -193,14 +205,13 @@ export async function submitAndAwaitTextToKcl({
makeDir: false, makeDir: false,
content: value.code, content: value.code,
silent: true, silent: true,
makeUnique: true,
}, },
}) })
} }
return { return {
...value, ...value,
fileName: newFileName + FILE_EXT, fileName: newFileName,
} }
}) })
@ -214,6 +225,7 @@ export async function submitAndAwaitTextToKcl({
toast.success( toast.success(
() => () =>
ToastTextToCadSuccess({ ToastTextToCadSuccess({
toastId,
data: textToCadOutputCreated, data: textToCadOutputCreated,
token, token,
navigate, navigate,

Some files were not shown because too many files have changed in this diff Show More