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:
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' }}

View File

@ -3,7 +3,7 @@ name: Create Release
on:
push:
branches:
- main
- tauri
jobs:
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:
```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

View File

@ -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')
})
}

View File

@ -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()

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 }) => {
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 }) => {

View File

@ -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.",

View File

@ -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",

View File

@ -80,5 +80,5 @@
}
},
"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 { 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),
[]
)

View File

@ -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 + .'], () => {

View File

@ -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)

View File

@ -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)

View File

@ -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 }) => {

View File

@ -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),
[]
)

View File

@ -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

View File

@ -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,
},
}}

View File

@ -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'

View File

@ -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 || ''

View File

@ -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'

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 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,
}
}

View File

@ -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