Compare commits

...

16 Commits

Author SHA1 Message Date
e099c95c5f Cut release v0.24.11 (#3423)
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-08-14 15:36:20 -07:00
f23bc673aa Update machine-api spec (#3433)
* YOYO NEW API SPEC!

* New machine-api types

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-14 14:52:40 -07:00
b60c1e874d Add api.zoo.dev to Tauri http scopes (#3432) 2024-08-14 14:47:37 -07:00
5857684ebc make sure we always have an id (#3431)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-14 14:29:14 -07:00
e8fc6bc037 remove debugs (#3429)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-14 11:58:39 -07:00
5bdd090119 Text-to-CAD with alpha model (might have issues) integration (#3299)
* Add close dismiss button to Infinite duration non-loading toasts

* Add text-to-cad icon candidates

* Add a way to silently create files

* Add text-to-cad command with mock backend

* add the actual endpoint

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

* fix the response

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

* fixups

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

* Add `credentials: include`

* add headers

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

* Mostly working? Just getting CORS on desktop

* Merge goof

* fixups

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

* create cross platform fetch;

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

* send the token;

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

* send the token;

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

* better names for files

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

* Commit broken THREEjs success toast

* base64 decode

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

* send telemetry on reject / accept

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

* start of tests

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

* more tests

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

* basic tests;

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

* lego

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

* updates

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

* fixes

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

* updates

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

* fmt

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

* fixes

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

* Get model stylized based on settings

* Don't need automatic dismiss button for Infinity-duration toasts anymore

* Stylize loaded model, add OrbitControls, polish button behavior

* Allow user to retry prompt if one fails

* Add an auto-grow textarea input type to the command bar, set text-to-cad to use it

* Delete the created file in desktop if user rejects it

* Submit with meta+Enter

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

* add more tests and various fixes

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

* Set `prompt` arg defaultValue to failed prompt value on retry

* Add missing `awaits` to playwright tests to get them passing

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

* empty

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-08-14 11:26:44 -07:00
669cab8737 bump derive docs; (#3426)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-14 10:19:36 -07:00
f1ea60d6ab bump libs (#3425)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-14 09:55:33 -07:00
3faec650b1 ensure we never execute over ourselves (#3419)
* ensure we never execute over ourselves

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

* fixups

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

* cleanup

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

* updates

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

* updates

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

* weird logs

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

* fix flake

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

* make faster

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-14 08:49:00 -07:00
b2b62ec163 Add logical not operator using bang ! (#3230)
* Add logical not operator using bang !

* Change to be more concise

* Add codemirror syntax highlighting for bang operator

* Add LSP semantic token type

* Change to runtime error for bang on non-bool

* Add additional assert check

* Fix tests to verify runtime values, not parsing

* Fix test failure messages to be more helpful

* Fix semantic token tests to not care about the index

---------

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-08-13 23:38:37 -07:00
5b798c2aa3 Unify expression execution (#3342)
* Factor out expression execution

* Reduce code duplication

* Rename function to be clearer

* Change to use From::from to be clearer

* Rename to be clearer

* Fix to avoid unneeded clone

* Fix to not need to be mutable

* Remove struct that isn't very useful yet

* Change pipelines to not duplicate expression evaluation

* Rename to be more consistent
2024-08-13 22:57:03 -07:00
a23bd1f034 benchkmarks for execute server rack heavy (#3421)
* benchkmarks for execute server rack heavy

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

* updates

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

* fixes

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

* fixes

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

* updates

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

* cluppy

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-13 22:45:42 -07:00
4d00dddfd8 Bump kittycad from 0.3.13 to 0.3.14 in /src/wasm-lib (#3422)
Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.3.13 to 0.3.14.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.3.13...v0.3.14)

---
updated-dependencies:
- dependency-name: kittycad
  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-13 21:36:14 -07:00
f055acb6a6 more shell examples (#3414)
* more shell examples

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

* update known issues

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-08-13 14:14:23 -07:00
bf9d88e9a5 Add actual argument type to error message (#3340)
* Add actual argument type to error message

* Change to reuse exiting function

* updates

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

* gen docs

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
2024-08-13 13:25:09 -07:00
712a3790e8 Fix lock file (#3412) 2024-08-13 15:46:51 +00:00
67 changed files with 4515 additions and 372 deletions

View File

@ -25,5 +25,5 @@ once fixed in engine will just start working here with no language changes.
Sketching on the chamfered face does not currently work.
- **Shell**: Shell is only working for `end` faces, not for `side` or `start`
faces. We are tracking the engine side bug on this.
- **Shell**: Shell sometimes does not work when arcs or fillets are involved.
We are tracking the engine side bug on this.

File diff suppressed because one or more lines are too long

View File

@ -197582,7 +197582,9 @@
"unpublished": false,
"deprecated": false,
"examples": [
"const firstSketch = startSketchOn('XY')\n |> startProfileAt([-12, 12], %)\n |> line([24, 0], %)\n |> line([0, -24], %)\n |> line([-24, 0], %)\n |> close(%)\n |> extrude(6, %)\n\n// Remove the end face for the extrusion.\nshell({ faces: ['end'], thickness: 0.25 }, firstSketch)"
"const firstSketch = startSketchOn('XY')\n |> startProfileAt([-12, 12], %)\n |> line([24, 0], %)\n |> line([0, -24], %)\n |> line([-24, 0], %)\n |> close(%)\n |> extrude(6, %)\n\n// Remove the end face for the extrusion.\nshell({ faces: ['end'], thickness: 0.25 }, firstSketch)",
"const firstSketch = startSketchOn('-XZ')\n |> startProfileAt([-12, 12], %)\n |> line([24, 0], %)\n |> line([0, -24], %)\n |> line([-24, 0], %)\n |> close(%)\n |> extrude(6, %)\n\n// Remove the start face for the extrusion.\nshell({ faces: ['start'], thickness: 0.25 }, firstSketch)",
"const firstSketch = startSketchOn('XY')\n |> startProfileAt([-12, 12], %)\n |> line([24, 0], %)\n |> line([0, -24], %)\n |> line([-24, 0], %, $myTag)\n |> close(%)\n |> extrude(6, %)\n\n// Remove a tagged face for the extrusion.\nshell({ faces: [myTag], thickness: 0.25 }, firstSketch)"
]
},
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test'
import { test, expect, Page } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
@ -346,55 +346,24 @@ const sketch001 = startSketchAt([-0, -0])
// expect zero errors in guter
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
// export the model
const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible()
// Click the export button
exportButton.click()
// Click the stl.
const stlOption = page.getByText('glTF')
await expect(stlOption).toBeVisible()
await page.keyboard.press('Enter')
// Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
// Find the toast.
// Look out for the toast message
const exportingToastMessage = page.getByText(`Exporting...`)
await expect(exportingToastMessage).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`)
// Try exporting again.
// Click the export button
exportButton.click()
await clickExportButton(page)
// Click the stl.
await expect(stlOption).toBeVisible()
await expect(exportingToastMessage).toBeVisible()
await page.keyboard.press('Enter')
// Click the checkbox
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
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()
@ -406,18 +375,7 @@ const sketch001 = startSketchAt([-0, -0])
await expect(alreadyExportingToastMessage).not.toBeVisible()
// Try exporting again.
// Click the export button
exportButton.click()
// Click the stl.
await expect(stlOption).toBeVisible()
await page.keyboard.press('Enter')
// Click the checkbox
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
await clickExportButton(page)
// Find the toast.
// Look out for the toast message
@ -432,3 +390,24 @@ const sketch001 = startSketchAt([-0, -0])
await expect(successToastMessage).toBeVisible()
})
})
async function clickExportButton(page: Page) {
// export the model
const exportButton = page.getByTestId('export-pane-button')
await expect(exportButton).toBeVisible()
// Click the export button
exportButton.click()
// Click the stl.
const gltfOption = page.getByText('glTF')
await expect(gltfOption).toBeVisible()
await page.keyboard.press('Enter')
// Click the checkbox
const submitButton = page.getByText('Confirm Export')
await expect(submitButton).toBeVisible()
await page.keyboard.press('Enter')
}

View File

@ -12,7 +12,7 @@ import pixelMatch from 'pixelmatch'
import { PNG } from 'pngjs'
import { Protocol } from 'playwright-core/types/protocol'
import type { Models } from '@kittycad/lib'
import { APP_NAME } from 'lib/constants'
import { APP_NAME, COOKIE_NAME } from 'lib/constants'
import waitOn from 'wait-on'
import { secrets } from './secrets'
import { TEST_SETTINGS_KEY, TEST_SETTINGS } from './storageStates'
@ -643,6 +643,16 @@ export async function setup(context: BrowserContext, page: Page) {
settings: TOML.stringify({ settings: TEST_SETTINGS }),
}
)
await context.addCookies([
{
name: COOKIE_NAME,
value: secrets.token,
path: '/',
domain: 'localhost',
secure: true,
},
])
// kill animations, speeds up tests and reduced flakiness
await page.emulateMedia({ reducedMotion: 'reduce' })
}

View File

@ -0,0 +1,358 @@
import { test, expect, Page } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
test.beforeEach(async ({ context, page }) => {
await setup(context, page)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control'
test.describe('Text-to-CAD tests', () => {
test('basic lego happy case', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await sendPromptFromCommandBar(page, 'a 2x4 lego')
// Find the toast.
// Look out for the toast message
const submittingToastMessage = page.getByText(
`Submitting to Text-to-CAD API...`
)
await expect(submittingToastMessage).toBeVisible()
await page.waitForTimeout(5000)
const generatingToastMessage = page.getByText(
`Generating parametric model...`
)
await expect(generatingToastMessage).toBeVisible()
const successToastMessage = page.getByText(`Text-to-CAD successful`)
await expect(successToastMessage).toBeVisible()
await expect(page.getByText('Copied')).not.toBeVisible()
// Hit copy to clipboard.
const copyToClipboardButton = page.getByRole('button', {
name: 'Copy to clipboard',
})
await expect(copyToClipboardButton).toBeVisible()
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()
// 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(`const`)
// make sure a model renders.
// wait for execution done
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// Find the toast close button.
const closeButton = page.getByRole('button', { name: 'Close' })
await expect(closeButton).toBeVisible()
await closeButton.click()
// The toast should disappear.
await expect(successToastMessage).not.toBeVisible()
})
test('you can reject text-to-cad output and it does nothing', async ({
page,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await sendPromptFromCommandBar(page, 'a 2x4 lego')
// Find the toast.
// Look out for the toast message
const submittingToastMessage = page.getByText(
`Submitting to Text-to-CAD API...`
)
await expect(submittingToastMessage).toBeVisible()
await page.waitForTimeout(5000)
const generatingToastMessage = page.getByText(
`Generating parametric model...`
)
await expect(generatingToastMessage).toBeVisible()
const successToastMessage = page.getByText(`Text-to-CAD successful`)
await expect(successToastMessage).toBeVisible()
// Hit copy to clipboard.
const rejectButton = page.getByRole('button', { name: 'Reject' })
await expect(rejectButton).toBeVisible()
await rejectButton.click()
// The toast should disappear.
await expect(successToastMessage).not.toBeVisible()
// Expect no code.
await expect(page.locator('.cm-content')).toContainText(``)
})
test('sending a bad prompt fails, can dismiss', 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()
// Type the prompt.
await page.keyboard.type(
'akjsndladf lajbhflauweyfa;wieufjn;wieJNUF;.wjdfn weh Fwhefb'
)
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()
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()
// Find the toast dismiss button.
const dismissButton = page.getByRole('button', { name: 'Dismiss' })
await expect(dismissButton).toBeVisible()
await dismissButton.click()
// The toast should disappear.
await expect(failureToastMessage).not.toBeVisible()
})
test('sending a bad prompt fails, can start over', 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()
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()
// Click the edit prompt button to try again.
const editPromptButton = page.getByRole('button', { name: 'Edit prompt' })
await expect(editPromptButton).toBeVisible()
await editPromptButton.click()
// The toast should disappear.
await expect(failureToastMessage).not.toBeVisible()
// Make sure the old prompt is still there and can be edited.
await expect(page.locator('textarea')).toContainText(badPrompt)
// Select all and start a new prompt.
await page.keyboard.down(CtrlKey)
await page.keyboard.press('KeyA')
await page.keyboard.up(CtrlKey)
await page.keyboard.type('a 2x4 lego')
// Submit the new prompt.
await page.keyboard.press('Enter')
// Make sure the new prompt works.
// Find the toast.
// Look out for the toast message
await expect(submittingToastMessage).toBeVisible()
await page.waitForTimeout(5000)
await expect(generatingToastMessage).toBeVisible()
await expect(successToastMessage).toBeVisible()
})
test('ensure you can shift+enter in the prompt box', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
const promptWithNewline = `a 2x4\nlego`
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()
// Type the prompt.
await page.keyboard.type('a 2x4')
await page.waitForTimeout(1000)
await page.keyboard.down('Shift')
await page.keyboard.press('Enter')
await page.keyboard.up('Shift')
await page.keyboard.type('lego')
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()
await page.waitForTimeout(1000)
const generatingToastMessage = page.getByText(
`Generating parametric model...`
)
await expect(generatingToastMessage).toBeVisible()
await page.waitForTimeout(5000)
const successToastMessage = page.getByText(`Text-to-CAD successful`)
await expect(successToastMessage).toBeVisible()
await expect(page.getByText(promptWithNewline)).toBeVisible()
})
})
async function sendPromptFromCommandBar(page: Page, promptStr: string) {
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()
// Type the prompt.
await page.keyboard.type(promptStr)
await page.waitForTimeout(1000)
await page.keyboard.press('Enter')
}

View File

@ -188,6 +188,67 @@
}
]
},
"LiveView": {
"description": "A liveview message.",
"oneOf": [
{
"additionalProperties": true,
"description": "Initialize the live view.",
"properties": {
"command": {
"enum": [
"init"
],
"type": "string"
},
"op_protocols": {
"description": "The op protocols.",
"items": {
"$ref": "#/components/schemas/OperationProtocol"
},
"type": "array"
},
"peer_host": {
"description": "The peer host.",
"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",
"op_protocols",
"peer_host",
"result",
"sequence_id"
],
"type": "object"
}
]
},
"Machine": {
"description": "Details for a 3d printer connected over USB.",
"oneOf": [
@ -383,6 +444,32 @@
],
"type": "object"
},
{
"additionalProperties": false,
"description": "A security message.",
"properties": {
"security": {
"$ref": "#/components/schemas/Security"
}
},
"required": [
"security"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "A liveview message.",
"properties": {
"live_view": {
"$ref": "#/components/schemas/LiveView"
}
},
"required": [
"live_view"
],
"type": "object"
},
{
"additionalProperties": false,
"description": "An unknown Json message.",
@ -448,6 +535,25 @@
}
]
},
"OperationProtocol": {
"additionalProperties": true,
"description": "An operation protocol.",
"properties": {
"protocol": {
"description": "The protocol.",
"type": "string"
},
"version": {
"description": "The version.",
"type": "string"
}
},
"required": [
"protocol",
"version"
],
"type": "object"
},
"Pong": {
"description": "The response from the `/ping` endpoint.",
"properties": {
@ -512,6 +618,55 @@
],
"type": "object"
},
{
"additionalProperties": true,
"description": "Calibration.",
"properties": {
"command": {
"enum": [
"calibration"
],
"type": "string"
},
"option": {
"description": "The option.",
"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."
}
},
"required": [
"command",
"option",
"result",
"sequence_id"
],
"type": "object"
},
{
"additionalProperties": true,
"description": "The status of the print.",
@ -1610,6 +1765,83 @@
}
]
},
"Security": {
"description": "A security message.",
"oneOf": [
{
"additionalProperties": true,
"description": "Get the serial number.",
"properties": {
"address": {
"description": "The address.",
"format": "int64",
"type": "integer"
},
"chip_sn": {
"description": "The chip sn.",
"type": "string"
},
"chipsn_len": {
"description": "The chip sn length.",
"format": "int64",
"type": "integer"
},
"command": {
"enum": [
"get_sn"
],
"type": "string"
},
"length": {
"description": "The length.",
"format": "int64",
"type": "integer"
},
"module": {
"description": "The module.",
"type": "string"
},
"reason": {
"allOf": [
{
"$ref": "#/components/schemas/Reason"
}
],
"description": "The reason for the message.",
"nullable": true
},
"sequence_id": {
"allOf": [
{
"$ref": "#/components/schemas/SequenceId"
}
],
"description": "The sequence id."
},
"sn": {
"description": "The serial number.",
"type": "string"
},
"status": {
"description": "The status.",
"type": "string"
}
},
"required": [
"address",
"chip_sn",
"chipsn_len",
"command",
"length",
"module",
"sequence_id",
"sn",
"status"
],
"type": "object"
}
]
},
"SequenceId": {
"anyOf": [
{

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.24.10",
"version": "0.24.11",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.17.0",
@ -17,7 +17,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@kittycad/lib": "^0.0.70",
"@kittycad/lib": "^0.0.76",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.4.1",
"@react-hook/resize-observer": "^2.0.1",

17
src-tauri/Cargo.lock generated
View File

@ -1214,7 +1214,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.22"
version = "0.1.23"
dependencies = [
"Inflector",
"convert_case 0.6.0",
@ -2612,7 +2612,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.4"
version = "0.2.5"
dependencies = [
"anyhow",
"approx",
@ -2672,9 +2672,9 @@ dependencies = [
[[package]]
name = "kittycad"
version = "0.3.12"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bc87dcc307aa8c8dd56a6f022da1cbdf13a0a1e2abacb9ca9f897118a75596d"
checksum = "ce5e9c51976882cdf6777557fd8c3ee68b00bb53e9307fc1721acb397f2ece9a"
dependencies = [
"anyhow",
"async-trait",
@ -2704,6 +2704,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"thiserror",
"tokio",
"tracing",
"url",
"uuid",
@ -4587,9 +4588,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.206"
version = "1.0.207"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b3e4cd94123dd520a128bcd11e34d9e9e423e7e3e50425cb1b4b1e3549d0284"
checksum = "5665e14a49a4ea1b91029ba7d3bca9f299e1f7cfa194388ccc20f14743e784f2"
dependencies = [
"serde_derive",
]
@ -4616,9 +4617,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.206"
version = "1.0.207"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabfb6138d2383ea8208cf98ccf69cdfb1aff4088460681d84189aa259762f97"
checksum = "6aea2634c86b0e8ef2cfdc0c340baede54ec27b1e46febd7f80dffb2aa44a00e"
dependencies = [
"proc-macro2",
"quote",

View File

@ -1,127 +1,129 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Capability for the main window",
"context": "local",
"windows": [
"main"
],
"permissions": [
"cli:default",
"deep-link:default",
"log:default",
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"fs:allow-create",
"fs:allow-read-file",
"fs:allow-read-text-file",
"fs:allow-write-file",
"fs:allow-write-text-file",
"fs:allow-read-dir",
"fs:allow-copy-file",
"fs:allow-mkdir",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-exists",
"fs:allow-stat",
{
"identifier": "fs:scope",
"allow": [
{
"path": "$TEMP"
},
{
"path": "$TEMP/**/*"
},
{
"path": "$HOME"
},
{
"path": "$HOME/**/*"
},
{
"path": "$HOME/.config"
},
{
"path": "$HOME/.config/**/*"
},
{
"path": "$APPCONFIG"
},
{
"path": "$APPCONFIG/**/*"
},
{
"path": "$DOCUMENT"
},
{
"path": "$DOCUMENT/**/*"
}
]
},
"shell:allow-open",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "open",
"cmd": "open",
"args": [
"-R",
{
"validator": "\\S+"
}
],
"sidecar": false
},
{
"name": "explorer",
"cmd": "explorer",
"args": [
"/select",
{
"validator": "\\S+"
}
],
"sidecar": false
}
]
},
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm",
{
"identifier": "http:default",
"allow": [
"https://dev.kittycad.io/*",
"https://dev.zoo.dev/*",
"https://kittycad.io/*",
"https://zoo.dev/*",
"https://api.dev.kittycad.io/*",
"https://api.dev.zoo.dev/*"
]
},
"os:allow-platform",
"os:allow-version",
"os:allow-os-type",
"os:allow-family",
"os:allow-arch",
"os:allow-exe-extension",
"os:allow-locale",
"os:allow-hostname",
"process:allow-restart",
"updater:default"
],
"platforms": [
"linux",
"macOS",
"windows"
]
}
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Capability for the main window",
"context": "local",
"windows": [
"main"
],
"permissions": [
"cli:default",
"deep-link:default",
"log:default",
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"fs:allow-create",
"fs:allow-read-file",
"fs:allow-read-text-file",
"fs:allow-write-file",
"fs:allow-write-text-file",
"fs:allow-read-dir",
"fs:allow-copy-file",
"fs:allow-mkdir",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-exists",
"fs:allow-stat",
{
"identifier": "fs:scope",
"allow": [
{
"path": "$TEMP"
},
{
"path": "$TEMP/**/*"
},
{
"path": "$HOME"
},
{
"path": "$HOME/**/*"
},
{
"path": "$HOME/.config"
},
{
"path": "$HOME/.config/**/*"
},
{
"path": "$APPCONFIG"
},
{
"path": "$APPCONFIG/**/*"
},
{
"path": "$DOCUMENT"
},
{
"path": "$DOCUMENT/**/*"
}
]
},
"shell:allow-open",
{
"identifier": "shell:allow-execute",
"allow": [
{
"name": "open",
"cmd": "open",
"args": [
"-R",
{
"validator": "\\S+"
}
],
"sidecar": false
},
{
"name": "explorer",
"cmd": "explorer",
"args": [
"/select",
{
"validator": "\\S+"
}
],
"sidecar": false
}
]
},
"dialog:allow-open",
"dialog:allow-save",
"dialog:allow-message",
"dialog:allow-ask",
"dialog:allow-confirm",
{
"identifier": "http:default",
"allow": [
"https://dev.kittycad.io/*",
"https://dev.zoo.dev/*",
"https://kittycad.io/*",
"https://zoo.dev/*",
"https://api.dev.kittycad.io/*",
"https://api.dev.zoo.dev/*",
"https://api.kittycad.io",
"https://api.zoo.dev/*"
]
},
"os:allow-platform",
"os:allow-version",
"os:allow-os-type",
"os:allow-family",
"os:allow-arch",
"os:allow-exe-extension",
"os:allow-locale",
"os:allow-hostname",
"process:allow-restart",
"updater:default"
],
"platforms": [
"linux",
"macOS",
"windows"
]
}

View File

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

View File

@ -5,6 +5,7 @@ import { CommandArgument } from 'lib/commandTypes'
import { useCommandsContext } from 'hooks/useCommandsContext'
import CommandBarHeader from './CommandBarHeader'
import CommandBarKclInput from './CommandBarKclInput'
import CommandBarTextareaInput from './CommandBarTextareaInput'
function CommandBarArgument({ stepBack }: { stepBack: () => void }) {
const { commandBarState, commandBarSend } = useCommandsContext()
@ -87,6 +88,14 @@ function ArgumentInput({
return (
<CommandBarKclInput arg={arg} stepBack={stepBack} onSubmit={onSubmit} />
)
case 'text':
return (
<CommandBarTextareaInput
arg={arg}
stepBack={stepBack}
onSubmit={onSubmit}
/>
)
default:
return (
<CommandBarBasicInput

View File

@ -0,0 +1,109 @@
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CommandArgument } from 'lib/commandTypes'
import { RefObject, useEffect, useRef } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
function CommandBarTextareaInput({
arg,
stepBack,
onSubmit,
}: {
arg: CommandArgument<unknown> & {
inputType: 'text'
name: string
}
stepBack: () => void
onSubmit: (event: unknown) => void
}) {
const { commandBarSend, commandBarState } = useCommandsContext()
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
const formRef = useRef<HTMLFormElement>(null)
const inputRef = useRef<HTMLTextAreaElement>(null)
useTextareaAutoGrow(inputRef)
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
inputRef.current.select()
}
}, [arg, inputRef])
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
onSubmit(inputRef.current?.value)
}
return (
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
<label className="flex items-start rounded mx-4 my-4 border border-chalkboard-100 dark:border-chalkboard-80">
<span className="capitalize px-2 py-1 rounded-br bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80">
{arg.name}
</span>
<textarea
id="arg-form"
name={arg.inputType}
ref={inputRef}
required
className="flex-grow mx-2 my-1 !bg-transparent focus:outline-none min-h-12"
placeholder="Enter a value"
defaultValue={
(commandBarState.context.argumentsToSubmit[arg.name] as
| string
| undefined) || (arg.defaultValue as string)
}
onKeyDown={(event) => {
if (event.key === 'Backspace' && !event.currentTarget.value) {
stepBack()
} else if (
event.key === 'Enter' &&
(event.metaKey || event.shiftKey)
) {
// Insert a newline
event.preventDefault()
const target = event.currentTarget
const value = target.value
const selectionStart = target.selectionStart
const selectionEnd = target.selectionEnd
target.value =
value.substring(0, selectionStart) +
'\n' +
value.substring(selectionEnd)
target.selectionStart = selectionStart + 1
target.selectionEnd = selectionStart + 1
} else if (event.key === 'Enter') {
formRef.current?.dispatchEvent(
new Event('submit', { bubbles: true })
)
}
}}
autoFocus
/>
</label>
</form>
)
}
/**
* Modified from https://www.reddit.com/r/reactjs/comments/twmild/comment/i3jf330/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
* Thank you to @sidkh for the original code
*/
const useTextareaAutoGrow = (ref: RefObject<HTMLTextAreaElement>) => {
useEffect(() => {
const listener = () => {
if (ref.current === null) return
ref.current.style.padding = '0px'
ref.current.style.height = ref.current.scrollHeight + 'px'
ref.current.style.removeProperty('padding')
}
if (ref.current === null) return
ref.current.addEventListener('input', listener)
return () => {
if (ref.current === null) return
ref.current.removeEventListener('input', listener)
}
}, [ref.current])
}
export default CommandBarTextareaInput

View File

@ -121,6 +121,16 @@ const CustomIconMap = {
/>
</svg>
),
chat: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17 14.5L15.2929 15.2071L14.0858 14H5C3.89543 14 3 13.1046 3 12V6C3 4.89543 3.89543 4 5 4H14C15.6569 4 17 5.34315 17 7V11.5V13V14.5ZM5 13H14.5L15 13.5L16 14.5V13.0858V13V11.5V7C16 5.89543 15.1046 5 14 5H5C4.44771 5 4 5.44772 4 6V12C4 12.5523 4.44772 13 5 13ZM7 10C7.55228 10 8 9.55228 8 9C8 8.44772 7.55228 8 7 8C6.44772 8 6 8.44772 6 9C6 9.55228 6.44772 10 7 10ZM11 9C11 9.55228 10.5523 10 10 10C9.44772 10 9 9.55228 9 9C9 8.44772 9.44772 8 10 8C10.5523 8 11 8.44772 11 9ZM13 10C13.5523 10 14 9.55228 14 9C14 8.44772 13.5523 8 13 8C12.4477 8 12 8.44772 12 9C12 9.55228 12.4477 10 13 10Z"
fill="currentColor"
/>
</svg>
),
checkmark: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -211,6 +221,16 @@ const CustomIconMap = {
/>
</svg>
),
elephant: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.5 4C4.11929 4 3 5.11929 3 6.5V7C3 10.0376 5.46243 12.5 8.5 12.5H8.96482C9.46635 12.5 9.93469 12.2493 10.2129 11.8321L10.5173 11.3755C11.1396 12.0849 12.0423 12.5 13 12.5H13.75H15V14C15 14.2626 14.9483 14.5227 14.8478 14.7654C14.7472 15.008 14.5999 15.2285 14.4142 15.4142C14.2285 15.5999 14.008 15.7472 13.7654 15.8478C13.5227 15.9483 13.2626 16 13 16C12.7374 16 12.4773 15.9483 12.2346 15.8478C11.992 15.7472 11.7715 15.5999 11.5858 15.4142C11.4001 15.2285 11.2528 15.008 11.1522 14.7654C11.1164 14.6789 11.0868 14.5902 11.0635 14.5H11.8544C11.9168 14.6431 12.0056 14.7734 12.1161 14.8839C12.2322 15 12.37 15.092 12.5216 15.1548C12.6733 15.2177 12.8358 15.25 13 15.25C13.1642 15.25 13.3267 15.2177 13.4784 15.1548C13.63 15.092 13.7678 15 13.8839 14.8839C14 14.7678 14.092 14.63 14.1548 14.4784C14.2177 14.3267 14.25 14.1642 14.25 14V13H13.25V14C13.25 14.0328 13.2435 14.0653 13.231 14.0957C13.2184 14.126 13.2 14.1536 13.1768 14.1768C13.1536 14.2 13.126 14.2184 13.0957 14.231C13.0653 14.2435 13.0328 14.25 13 14.25C12.9672 14.25 12.9347 14.2435 12.9043 14.231C12.874 14.2184 12.8464 14.2 12.8232 14.1768C12.8 14.1536 12.7816 14.126 12.769 14.0957C12.7565 14.0653 12.75 14.0328 12.75 14V13.5H12.25H10.5H10V14C10 14.394 10.0776 14.7841 10.2284 15.1481C10.3791 15.512 10.6001 15.8427 10.8787 16.1213C11.1573 16.3999 11.488 16.6209 11.8519 16.7716C12.2159 16.9224 12.606 17 13 17C13.394 17 13.7841 16.9224 14.1481 16.7716C14.512 16.6209 14.8427 16.3999 15.1213 16.1213C15.3999 15.8427 15.6209 15.512 15.7716 15.1481C15.9224 14.7841 16 14.394 16 14V12.5H17V11.5H16V8.5C16 6.01472 13.9853 4 11.5 4H5.5ZM11.084 10.4746L10.9226 10.2326L9.42875 7.74275L8.57125 8.25725L9.90846 10.4859L9.38084 11.2773C9.28811 11.4164 9.13199 11.5 8.96482 11.5H8.5C6.01472 11.5 4 9.48528 4 7V6.5C4 5.67157 4.67157 5 5.5 5H11.5C13.433 5 15 6.567 15 8.5V11.5H13.75H13C12.2301 11.5 11.5111 11.1152 11.084 10.4746ZM13.5 8.5C13.5 9.05228 13.0523 9.5 12.5 9.5C11.9477 9.5 11.5 9.05228 11.5 8.5C11.5 7.94772 11.9477 7.5 12.5 7.5C13.0523 7.5 13.5 7.94772 13.5 8.5Z"
fill="black"
/>
</svg>
),
equal: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -590,13 +610,7 @@ const CustomIconMap = {
</svg>
),
revolve: (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
@ -645,6 +659,16 @@ const CustomIconMap = {
/>
</svg>
),
sparkles: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6.92704 6.63163L7.67352 4.61429L8.42 6.63163L8.71541 6.92704L10.7327 7.67352L8.71541 8.42L8.42 8.71541L7.67352 10.7327L6.92704 8.71541L6.63163 8.42L4.61429 7.67352L6.63163 6.92704L6.92704 6.63163ZM7.20459 3L6.06897 6.06897L3 7.20459V8.14244L6.06897 9.27807L7.20459 12.347H8.14244L9.27807 9.27807L12.347 8.14244V7.20459L9.27807 6.06897L8.14244 3H7.20459ZM13.4822 13.1868L13.8235 12.2643L14.1649 13.1868L14.4603 13.4822L15.3827 13.8235L14.4603 14.1649L14.1649 14.4603L13.8235 15.3827L13.4822 14.4603L13.1868 14.1649L12.2643 13.8235L13.1868 13.4822L13.4822 13.1868ZM13.3546 10.65L12.6241 12.6241L10.65 13.3546V14.2924L12.6241 15.0229L13.3546 16.997H14.2924L15.0229 15.0229L16.997 14.2924V13.3546L15.0229 12.6241L14.2924 10.65H13.3546Z"
fill="currentColor"
/>
</svg>
),
spline: (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
@ -656,13 +680,7 @@ const CustomIconMap = {
</svg>
),
sweep: (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"

View File

@ -15,7 +15,13 @@ import {
} from 'xstate'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine'
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
import {
mkdir,
remove,
rename,
create,
writeTextFile,
} 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'
@ -94,6 +100,30 @@ export const FileMachineProvider = ({
children: newFiles,
}
},
createAndOpenFile: async (context, event) => {
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
if (event.data.makeDir) {
createdPath = await join(context.selectedDirectory.path, createdName)
await mkdir(createdPath)
} else {
createdPath =
context.selectedDirectory.path +
sep() +
createdName +
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT)
await create(createdPath)
if (event.data.content) {
await writeTextFile(createdPath, event.data.content)
}
}
return {
message: `Successfully created "${createdName}"`,
path: createdPath,
}
},
createFile: async (context, event) => {
let createdName = event.data.name.trim() || DEFAULT_FILE_NAME
let createdPath: string
@ -108,10 +138,12 @@ export const FileMachineProvider = ({
createdName +
(createdName.endsWith(FILE_EXT) ? '' : FILE_EXT)
await create(createdPath)
if (event.data.content) {
await writeTextFile(createdPath, event.data.content)
}
}
return {
message: `Successfully created "${createdName}"`,
path: createdPath,
}
},
@ -182,6 +214,7 @@ export const FileMachineProvider = ({
if (event.type !== 'done.invoke.read-files') return false
return !!event?.data?.children && event.data.children.length > 0
},
'Is not silent': (_, event) => !event.data?.silent,
},
})

View File

@ -71,7 +71,7 @@ import { exportFromEngine } from 'lib/exportFromEngine'
import { Models } from '@kittycad/lib/dist/types/src'
import toast from 'react-hot-toast'
import { EditorSelection, Transaction } from '@codemirror/state'
import { useSearchParams } from 'react-router-dom'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { getVarNameModal } from 'hooks/useToolbarGuards'
import { err, trap } from 'lib/trap'
@ -83,6 +83,8 @@ import {
EngineConnectionStateType,
EngineConnectionEvents,
} from 'lang/std/engineConnection'
import { submitAndAwaitTextToKcl } from 'lib/textToCad'
import { useFileContext } from 'hooks/useFileContext'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -108,6 +110,8 @@ export const ModelingMachineProvider = ({
},
},
} = useSettingsAuthContext()
const navigate = useNavigate()
const { context, send: fileMachineSend } = useFileContext()
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), [])
@ -115,7 +119,7 @@ export const ModelingMachineProvider = ({
let [searchParams] = useSearchParams()
const pool = searchParams.get('pool')
const { commandBarState } = useCommandsContext()
const { commandBarState, commandBarSend } = useCommandsContext()
// Settings machine setup
// const retrievedSettings = useRef(
@ -149,8 +153,6 @@ export const ModelingMachineProvider = ({
},
'sketch exit execute': ({ store }) => {
;(async () => {
// blocks entering a sketch until after exit sketch code has run
kclManager.isExecuting = true
sceneInfra.camControls.syncDirection = 'clientToEngine'
await sceneInfra.camControls.snapToPerspectiveBeforeHandingBackControlToEngine()
@ -465,6 +467,29 @@ 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
void submitAndAwaitTextToKcl({
trimmedPrompt,
fileMachineSend,
navigate,
commandBarSend,
context,
token,
settings: {
theme: theme.current,
highlightEdges: highlightEdges.current,
},
})
},
},
guards: {
'has valid extrude selection': ({ selectionRanges }) => {
@ -524,6 +549,8 @@ export const ModelingMachineProvider = ({
return false
}
},
'Has no pending text-to-cad submissions': ({ pendingTextToCad }) =>
!pendingTextToCad,
},
services: {
'AST-undo-startSketchOn': async ({ sketchDetails }) => {

View File

@ -0,0 +1,364 @@
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { useFileContext } from 'hooks/useFileContext'
import { isTauri } from 'lib/isTauri'
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 {
Box3,
Color,
DirectionalLight,
EdgesGeometry,
LineBasicMaterial,
LineSegments,
Mesh,
MeshBasicMaterial,
OrthographicCamera,
Scene,
Vector3,
WebGLRenderer,
} from 'three'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
import { base64Decode } from 'lang/wasm'
import { sendTelemetry } from 'lib/textToCad'
import { Themes } from 'lib/theme'
import { ActionButton } from './ActionButton'
import { commandBarMachine } from 'machines/commandBarMachine'
import { EventData, EventFrom } from 'xstate'
import { fileMachine } from 'machines/fileMachine'
const CANVAS_SIZE = 128
const PROMPT_TRUNCATE_LENGTH = 128
const FRUSTUM_SIZE = 0.5
const OUTPUT_KEY = 'source.glb'
export function ToastTextToCadError({
message,
prompt,
commandBarSend,
}: {
message: string
prompt: string
commandBarSend: (
event: EventFrom<typeof commandBarMachine>,
data?: EventData
) => void
}) {
return (
<div className="flex flex-col justify-between gap-6">
<section>
<h2>Text-to-CAD failed</h2>
<p className="text-sm text-chalkboard-70 dark:text-chalkboard-30">
{message}
</p>
</section>
<div className="flex justify-between gap-8">
<ActionButton
Element="button"
iconStart={{
icon: 'close',
}}
name="Dismiss"
onClick={() => {
toast.dismiss()
}}
>
Dismiss
</ActionButton>
<ActionButton
Element="button"
iconStart={{
icon: 'refresh',
}}
name="Edit prompt"
onClick={() => {
commandBarSend({
type: 'Find and select command',
data: {
groupId: 'modeling',
name: 'Text-to-CAD',
argDefaultValues: {
prompt,
},
},
})
toast.dismiss()
}}
>
Edit prompt
</ActionButton>
</div>
</div>
)
}
export function ToastTextToCadSuccess({
data,
navigate,
context,
token,
fileMachineSend,
settings,
}: {
// TODO: update this type to match the actual data when API is done
data: TextToCad_type & { fileName: string }
navigate: (to: string) => void
context: ReturnType<typeof useFileContext>['context']
token?: string
fileMachineSend: (
event: EventFrom<typeof fileMachine>,
data?: EventData
) => void
settings: {
theme: Themes
highlightEdges: boolean
}
}) {
const wrapperRef = useRef<HTMLDivElement | null>(null)
const canvasRef = useRef<HTMLCanvasElement | null>(null)
const [hasCopied, setHasCopied] = useState(false)
const [showCopiedUi, setShowCopiedUi] = useState(false)
const modelId = data.id
useEffect(() => {
if (!canvasRef.current) return
const canvas = canvasRef.current
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)
scene.add(ambientLight)
const camera = createCamera()
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
const loader = new GLTFLoader()
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/examples/jsm/libs/draco/')
loader.setDRACOLoader(dracoLoader)
scene.add(camera)
// Get the base64 encoded GLB file
const buffer = base64Decode(data.outputs[OUTPUT_KEY])
if (buffer instanceof Error) {
toast.error('Error loading GLB file: ' + buffer.message)
console.error('decoding buffer from base64 failed', buffer)
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,
'',
// called when the resource is loaded
function (gltf) {
scene.add(gltf.scene)
// Style the objects in the scene
traverseSceneToStyleObjects({
scene,
...settings,
})
// Then we'll calculate the max distance of the bounding box
// and set that as the camera's position
const size = new Vector3()
const boundingBox = new Box3()
boundingBox.setFromObject(gltf.scene)
boundingBox.getSize(size)
const maxDistance = Math.max(size.x, size.y, size.z)
camera.position.set(maxDistance * 2, maxDistance * 2, maxDistance * 2)
camera.lookAt(0, 0, 0)
camera.left = -maxDistance
camera.right = maxDistance
camera.top = maxDistance
camera.bottom = -maxDistance
camera.near = 0
camera.far = maxDistance * 10
// Create and attach the lights,
// since their position depends on the bounding box
const cameraLight1 = new DirectionalLight(new Color('white'), 1)
cameraLight1.position.set(maxDistance * -5, -maxDistance, maxDistance)
camera.add(cameraLight1)
const cameraLight2 = new DirectionalLight(new Color('white'), 1.4)
cameraLight2.position.set(0, 0, 2 * maxDistance)
camera.add(cameraLight2)
const sceneLight = new DirectionalLight(new Color('white'), 1)
sceneLight.position.set(
-2 * maxDistance,
-2 * maxDistance,
2 * maxDistance
)
scene.add(sceneLight)
camera.updateProjectionMatrix()
controls.update()
},
// called when loading has errors
function (error) {
toast.error('Error loading GLB file: ' + error.message)
console.error('Error loading GLB file', error)
return
}
)
return () => {
renderer.dispose()
}
}, [])
return (
<div className="flex gap-4 min-w-80" ref={wrapperRef}>
<div
className="flex-none overflow-hidden"
style={{ width: CANVAS_SIZE + 'px', height: CANVAS_SIZE + 'px' }}
>
<canvas ref={canvasRef} width={CANVAS_SIZE} height={CANVAS_SIZE} />
</div>
<div className="flex flex-col justify-between gap-6">
<section>
<h2>Text-to-CAD successful</h2>
<p className="text-sm text-chalkboard-70 dark:text-chalkboard-30">
Prompt: "
{data.prompt.length > PROMPT_TRUNCATE_LENGTH
? data.prompt.slice(0, PROMPT_TRUNCATE_LENGTH) + '...'
: data.prompt}
"
</p>
</section>
<div className="flex justify-between gap-8">
<ActionButton
Element="button"
iconStart={{
icon: 'close',
}}
name={hasCopied ? 'Close' : 'Reject'}
onClick={() => {
if (!hasCopied) {
sendTelemetry(modelId, 'rejected', token)
}
if (isTauri()) {
// Delete the file from the project
fileMachineSend({
type: 'Delete file',
data: {
name: data.fileName,
path: `${context.project.path}${sep()}${data.fileName}`,
children: null,
},
})
}
toast.dismiss()
}}
>
{hasCopied ? 'Close' : 'Reject'}
</ActionButton>
{isTauri() ? (
<ActionButton
Element="button"
iconStart={{
icon: 'checkmark',
}}
name="Accept"
onClick={() => {
sendTelemetry(modelId, 'accepted', token)
navigate(
`${PATHS.FILE}/${encodeURIComponent(
`${context.project.path}${sep()}${data.fileName}`
)}`
)
toast.dismiss()
}}
>
Accept
</ActionButton>
) : (
<ActionButton
Element="button"
iconStart={{
icon: showCopiedUi ? 'clipboardCheckmark' : 'clipboardPlus',
}}
name="Copy to clipboard"
onClick={() => {
sendTelemetry(modelId, 'accepted', token)
navigator.clipboard.writeText(data.code || '// no code found')
setShowCopiedUi(true)
setHasCopied(true)
// Reset the button text after 5 seconds
setTimeout(() => {
setShowCopiedUi(false)
}, 5000)
}}
>
{showCopiedUi ? 'Copied' : 'Copy to clipboard'}
</ActionButton>
)}
</div>
</div>
</div>
)
}
const createCamera = (): OrthographicCamera => {
return new OrthographicCamera(
-FRUSTUM_SIZE,
FRUSTUM_SIZE,
FRUSTUM_SIZE,
-FRUSTUM_SIZE,
0.5,
3
)
}
function traverseSceneToStyleObjects({
scene,
color = 0x29ffa4,
highlightEdges = false,
}: {
scene: Scene
color?: number
theme: Themes
highlightEdges?: boolean
}) {
scene.traverse((child) => {
if ('isMesh' in child && child.isMesh) {
// Replace the material with our flat color one
;(child as Mesh).material = new MeshBasicMaterial({
color,
})
// Add edges to the mesh if the option is enabled
if (!highlightEdges) return
const edges = new EdgesGeometry((child as Mesh).geometry, 30)
const lines = new LineSegments(
edges,
new LineBasicMaterial({
// We don't respect the theme here on purpose,
// because I found the dark edges to work better
// in light and dark themes,
// but we can change that if needed, it is wired up
color: 0x1f2020,
})
)
scene.add(lines)
}
})
}

View File

@ -6,6 +6,7 @@ export const klcHighlight = styleTags({
'true false': t.bool,
nil: t.null,
'AddOp MultOp ExpOp': t.arithmeticOperator,
BangOp: t.logicOperator,
CompOp: t.logicOperator,
'Equals Arrow': t.definitionOperator,
PipeOperator: t.controlOperator,

View File

@ -38,7 +38,7 @@ expression[@isGroup=Expression] {
expression !exp ExpOp expression |
expression !comp CompOp expression
} |
UnaryExpression { AddOp expression } |
UnaryExpression { UnaryOp expression } |
ParenthesizedExpression { "(" expression ")" } |
CallExpression { expression !call ArgumentList } |
ArrayExpression { "[" commaSep<expression | IntegerRange { expression !range ".." expression }> "]" } |
@ -48,6 +48,8 @@ expression[@isGroup=Expression] {
PipeExpression { expression (!pipe PipeOperator expression)+ }
}
UnaryOp { AddOp | BangOp }
ObjectProperty { PropertyName ":" expression }
ArgumentList { "(" commaSep<expression> ")" }
@ -80,6 +82,7 @@ commaSep<term> { (term ("," term)*)? ","? }
AddOp { "+" | "-" }
MultOp { "/" | "*" | "\\" }
ExpOp { "^" }
BangOp { "!" }
CompOp { $[<>] "="? | "!=" | "==" }
Equals { "=" }
Arrow { "=>" }

View File

@ -34,6 +34,7 @@ root.render(
toastOptions={{
style: {
borderRadius: '3px',
maxInlineSize: 'min(480px, 100%)',
},
className:
'bg-chalkboard-10 dark:bg-chalkboard-90 text-chalkboard-110 dark:text-chalkboard-10 rounded-sm border-chalkboard-20/50 dark:border-chalkboard-80/50',

View File

@ -19,6 +19,16 @@ import { getNodeFromPath } from './queryAst'
import { codeManager, editorManager, sceneInfra } from 'lib/singletons'
import { Diagnostic } from '@codemirror/lint'
interface ExecuteArgs {
ast?: Program
zoomToFit?: boolean
executionId?: number
zoomOnRangeAndType?: {
range: SourceRange
type: string
}
}
export class KclManager {
private _ast: Program = {
body: [],
@ -36,6 +46,7 @@ export class KclManager {
private _lints: Diagnostic[] = []
private _kclErrors: KCLError[] = []
private _isExecuting = false
private _executeIsStale: ExecuteArgs | null = null
private _wasmInitFailed = true
engineCommandManager: EngineCommandManager
@ -113,9 +124,25 @@ export class KclManager {
}
set isExecuting(isExecuting) {
this._isExecuting = isExecuting
// If we have finished executing, but the execute is stale, we should
// execute again.
if (!isExecuting && this.executeIsStale) {
const args = this.executeIsStale
this.executeIsStale = null
this.executeAst(args)
} else {
}
this._isExecutingCallback(isExecuting)
}
get executeIsStale() {
return this._executeIsStale
}
set executeIsStale(executeIsStale) {
this._executeIsStale = executeIsStale
}
get wasmInitFailed() {
return this._wasmInitFailed
}
@ -202,16 +229,16 @@ export class KclManager {
// This NEVER updates the code, if you want to update the code DO NOT add to
// this function, too many other things that don't want it exist.
// just call to codeManager from wherever you want in other files.
async executeAst(
ast: Program = this._ast,
zoomToFit?: boolean,
executionId?: number,
zoomOnRangeAndType?: {
range: SourceRange
type: string
async executeAst(args: ExecuteArgs = {}): Promise<void> {
if (this.isExecuting) {
this.executeIsStale = args
// Exit early if we are already executing.
return
}
): Promise<void> {
const currentExecutionId = executionId || Date.now()
const ast = args.ast || this.ast
const currentExecutionId = args.executionId || Date.now()
this._cancelTokens.set(currentExecutionId, false)
this.isExecuting = true
@ -229,12 +256,12 @@ export class KclManager {
defaultSelectionFilter(programMemory, this.engineCommandManager)
await this.engineCommandManager.waitForAllCommands()
if (zoomToFit) {
if (args.zoomToFit) {
let zoomObjectId: string | undefined = ''
if (zoomOnRangeAndType) {
if (args.zoomOnRangeAndType) {
zoomObjectId = this.engineCommandManager?.mapRangeToObjectId(
zoomOnRangeAndType.range,
zoomOnRangeAndType.type
args.zoomOnRangeAndType.range,
args.zoomOnRangeAndType.type
)
}
@ -259,6 +286,7 @@ export class KclManager {
}
this.isExecuting = false
// Check the cancellation token for this execution before applying side effects
if (this._cancelTokens.get(currentExecutionId)) {
this._cancelTokens.delete(currentExecutionId)
@ -351,8 +379,7 @@ export class KclManager {
return
}
this.ast = { ...ast }
this.isExecuting = true // executeAst sets this to false again
return this.executeAst(ast, zoomToFit)
return this.executeAst({ zoomToFit })
}
format() {
const originalCode = codeManager.code
@ -430,12 +457,11 @@ export class KclManager {
codeManager.updateCodeEditor(newCode)
// Write the file to disk.
await codeManager.writeToFile()
await this.executeAst(
astWithUpdatedSource,
optionalParams?.zoomToFit,
undefined,
optionalParams?.zoomOnRangeAndType
)
await this.executeAst({
ast: astWithUpdatedSource,
zoomToFit: optionalParams?.zoomToFit,
zoomOnRangeAndType: optionalParams?.zoomOnRangeAndType,
})
} else {
// When we don't re-execute, we still want to update the program
// memory with the new ast. So we will hit the mock executor

View File

@ -136,7 +136,7 @@ beforeAll(async () => {
console.error(ast)
return Promise.reject(ast)
}
await kclManager.executeAst(ast)
await kclManager.executeAst({ ast })
cacheToWriteToFileTemp[codeKey] = {
orderedCommands: engineCommandManager.orderedCommands,

View File

@ -17,6 +17,7 @@ import init, {
parse_project_settings,
default_project_settings,
parse_project_route,
base64_decode,
} from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
@ -593,3 +594,13 @@ export function parseProjectRoute(
): ProjectRoute | Error {
return parse_project_route(JSON.stringify(configuration), route_str)
}
export function base64Decode(base64: string): ArrayBuffer | Error {
try {
const decoded = base64_decode(base64)
return new Uint8Array(decoded).buffer
} catch (e) {
console.error('Caught error decoding base64 string: ' + e)
return new Error('Caught error decoding base64 string: ' + e)
}
}

View File

@ -40,6 +40,9 @@ export type ModelingCommandSchema = {
'change tool': {
tool: SketchTool
}
'Text-to-CAD': {
prompt: string
}
}
export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
@ -258,4 +261,14 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
},
},
},
'Text-to-CAD': {
description: 'Use the Zoo Text-to-CAD API to generate part starters.',
icon: 'chat',
args: {
prompt: {
inputType: 'text',
required: true,
},
},
},
}

View File

@ -15,6 +15,7 @@ const PLATFORMS = ['both', 'web', 'desktop'] as const
const INPUT_TYPES = [
'options',
'string',
'text',
'kcl',
'selection',
'boolean',
@ -151,6 +152,16 @@ export type CommandArgumentConfig<
) => OutputType)
defaultValueFromContext?: (context: C) => OutputType
}
| {
inputType: 'text'
defaultValue?:
| OutputType
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: C
) => OutputType)
defaultValueFromContext?: (context: C) => OutputType
}
| {
inputType: 'boolean'
defaultValue?:
@ -213,6 +224,15 @@ export type CommandArgument<
machineContext?: ContextFrom<T>
) => OutputType)
}
| {
inputType: 'text'
defaultValue?:
| OutputType
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: ContextFrom<T>
) => OutputType)
}
| {
inputType: 'boolean'
defaultValue?:

View File

@ -55,3 +55,4 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
} as const
/** The default KCL length expression */
export const KCL_DEFAULT_LENGTH = `5`
export const COOKIE_NAME = '__Secure-next-auth.session-token'

View File

@ -0,0 +1,56 @@
import { DEV } from 'env'
import { isTauri } from './isTauri'
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
const headers = (token?: string): HeadersInit => ({
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
})
export default async function crossPlatformFetch<T>(
url: string,
options?: RequestInit,
token?: string
): Promise<T | Error> {
let response = null
let opts = options || {}
if (isTauri()) {
if (!token) {
return new Error('No token provided')
}
opts.headers = headers(token)
response = await tauriFetch(url, opts)
} else {
// Add credentials: 'include' to options
// We send the token with the headers only in development mode, DO NOT
// DO THIS IN PRODUCTION, as it is a security risk.
opts.headers = headers(DEV ? token : undefined)
opts.credentials = 'include'
response = await fetch(url, opts)
}
if (!response) {
return new Error('Failed to request endpoint: ' + url)
}
if (!response.ok) {
console.error(
'Failed to request endpoint: ' + url,
JSON.stringify(response)
)
return new Error(
'Failed to request endpoint: ' +
url +
' with status: ' +
response.status +
' ' +
response.statusText
)
}
const data = (await response.json()) as T | Error
return data
}

View File

@ -137,6 +137,23 @@ export interface components {
LedMode: 'on' | 'off' | 'flashing'
/** @description The node for the led. */
LedNode: 'chamber_light' | 'work_light'
/** @description A liveview message. */
LiveView: {
/** @enum {string} */
command: 'init'
/** @description The op protocols. */
op_protocols: components['schemas']['OperationProtocol'][]
/** @description The peer host. */
peer_host: 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
}
/** @description Details for a 3d printer connected over USB. */
Machine:
| {
@ -201,6 +218,12 @@ export interface components {
| {
system: components['schemas']['System']
}
| {
security: components['schemas']['Security']
}
| {
live_view: components['schemas']['LiveView']
}
| {
json: unknown
}
@ -211,6 +234,15 @@ export interface components {
NetworkPrinterManufacturer: 'Bambu' | 'Formlabs'
/** @description A nozzle type. */
NozzleType: 'hardened_steel' | 'stainless_steel'
/** @description An operation protocol. */
OperationProtocol: {
/** @description The protocol. */
protocol: string
/** @description The version. */
version: string
} & {
[key: string]: unknown
}
/** @description The response from the `/ping` endpoint. */
Pong: {
/** @description The pong response. */
@ -232,6 +264,23 @@ export interface components {
} & {
[key: string]: unknown
})
| ({
/** @enum {string} */
command: 'calibration'
/**
* Format: int64
* @description The option.
*/
option: 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']
} & {
[key: string]: unknown
})
| ({
/** @description The ams. */
ams?: components['schemas']['PrintAms'] | null
@ -721,6 +770,40 @@ export interface components {
}
/** @description The result of a message. */
Result: 'SUCCESS' | 'FAIL'
/** @description A security message. */
Security: {
/**
* Format: int64
* @description The address.
*/
address: number
/** @description The chip sn. */
chip_sn: string
/**
* Format: int64
* @description The chip sn length.
*/
chipsn_len: number
/** @enum {string} */
command: 'get_sn'
/**
* Format: int64
* @description The length.
*/
length: number
/** @description The module. */
module: string
/** @description The reason for the message. */
reason?: components['schemas']['Reason'] | null
/** @description The sequence id. */
sequence_id: components['schemas']['SequenceId']
/** @description The serial number. */
sn: string
/** @description The status. */
status: string
} & {
[key: string]: unknown
}
/** @description The sequence id type. */
SequenceId: string | number
/** @description A system command. */

247
src/lib/textToCad.ts Normal file
View File

@ -0,0 +1,247 @@
import { Models } from '@kittycad/lib'
import {
ToastTextToCadError,
ToastTextToCadSuccess,
} from 'components/ToastTextToCad'
import { VITE_KC_API_BASE_URL } from 'env'
import toast from 'react-hot-toast'
import { FILE_EXT } from './constants'
import { ContextFrom, EventData, EventFrom } from 'xstate'
import { fileMachine } from 'machines/fileMachine'
import { NavigateFunction } from 'react-router-dom'
import crossPlatformFetch from './crossPlatformFetch'
import { isTauri } from './isTauri'
import { Themes } from './theme'
import { commandBarMachine } from 'machines/commandBarMachine'
export async function submitTextToCadPrompt(
prompt: string,
token?: string
): Promise<Models['TextToCad_type'] | Error> {
const body: Models['TextToCadCreateBody_type'] = { prompt }
// Glb has a smaller footprint than gltf, should we want to render it.
const url = VITE_KC_API_BASE_URL + '/ai/text-to-cad/glb?kcl=true'
const data: Models['TextToCad_type'] | Error = await crossPlatformFetch(
url,
{
method: 'POST',
body: JSON.stringify(body),
},
token
)
// Make sure we have an id.
if (data instanceof Error) {
return data
}
if (!data.id) {
return new Error('No id returned from Text-to-CAD API')
}
return data
}
export async function getTextToCadResult(
id: string,
token?: string
): Promise<Models['TextToCad_type'] | Error> {
const url = VITE_KC_API_BASE_URL + '/user/text-to-cad/' + id
const data: Models['TextToCad_type'] | Error = await crossPlatformFetch(
url,
{
method: 'GET',
},
token
)
return data
}
interface TextToKclProps {
trimmedPrompt: string
fileMachineSend: (
type: EventFrom<typeof fileMachine>,
data?: EventData
) => unknown
navigate: NavigateFunction
commandBarSend: (
type: EventFrom<typeof commandBarMachine>,
data?: EventData
) => unknown
context: ContextFrom<typeof fileMachine>
token?: string
settings: {
theme: Themes
highlightEdges: boolean
}
}
export async function submitAndAwaitTextToKcl({
trimmedPrompt,
fileMachineSend,
navigate,
commandBarSend,
context,
token,
settings,
}: TextToKclProps) {
const toastId = toast.loading('Submitting to Text-to-CAD API...')
const showFailureToast = (message: string) => {
toast.error(
() =>
ToastTextToCadError({
message,
commandBarSend,
prompt: trimmedPrompt,
}),
{
id: toastId,
duration: Infinity,
}
)
}
const textToCadQueued = await submitTextToCadPrompt(trimmedPrompt, token)
.then((value) => {
if (value instanceof Error) {
return Promise.reject(value)
}
return value
})
.catch((error) => {
showFailureToast('Failed to submit to Text-to-CAD API')
return error
})
if (textToCadQueued instanceof Error) {
showFailureToast('Failed to submit to Text-to-CAD API')
return
}
toast.loading('Generating parametric model...', {
id: toastId,
})
// Check the status of the text-to-cad API job
// until it is completed
const textToCadComplete = new Promise<Models['TextToCad_type']>(
async (resolve, reject) => {
const value = await textToCadQueued
if (value instanceof Error) {
reject(value)
}
const MAX_CHECK_TIMEOUT = 3 * 60_000
const CHECK_INTERVAL = 3000
let timeElapsed = 0
const interval = setInterval(async () => {
timeElapsed += CHECK_INTERVAL
if (timeElapsed >= MAX_CHECK_TIMEOUT) {
clearInterval(interval)
reject(new Error('Text-to-CAD API timed out'))
}
const check = await getTextToCadResult(value.id, token)
if (check instanceof Error) {
clearInterval(interval)
reject(check)
}
if (check instanceof Error || check.status === 'failed') {
clearInterval(interval)
reject(check)
} else if (check.status === 'completed') {
clearInterval(interval)
resolve(check)
}
}, CHECK_INTERVAL)
}
)
const textToCadOutputCreated = await textToCadComplete
.catch((e) => {
showFailureToast('Failed to generate parametric model')
return e
})
.then((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) {
const error = value.error.replace('Text-to-CAD server:', '').trim()
showFailureToast(error)
return Promise.reject(new Error(error))
} else {
showFailureToast('No KCL code returned')
return Promise.reject(new Error('No KCL code returned'))
}
}
const TRUNCATED_PROMPT_LENGTH = 24
const newFileName = `${value.prompt
.slice(0, TRUNCATED_PROMPT_LENGTH)
.replace(/\s/gi, '-')
.replace(/\W/gi, '-')
.toLowerCase()}`
if (isTauri()) {
fileMachineSend({
type: 'Create file',
data: {
name: newFileName,
makeDir: false,
content: value.code,
silent: true,
makeUnique: true,
},
})
}
return {
...value,
fileName: newFileName + FILE_EXT,
}
})
if (textToCadOutputCreated instanceof Error) {
showFailureToast('Failed to generate parametric model')
return
}
// Show a custom toast with the .glb model preview
// and options to reject or accept the model
toast.success(
() =>
ToastTextToCadSuccess({
data: textToCadOutputCreated,
token,
navigate,
context,
fileMachineSend,
settings,
}),
{
id: toastId,
duration: Infinity,
icon: null,
}
)
return textToCadOutputCreated
}
export async function sendTelemetry(
id: string,
feedback: Models['AiFeedback_type'],
token?: string
): Promise<void> {
const url =
VITE_KC_API_BASE_URL + '/user/text-to-cad/' + id + '?feedback=' + feedback
await crossPlatformFetch(
url,
{
method: 'POST',
},
token
)
}

View File

@ -4,6 +4,7 @@ import withBaseURL from '../lib/withBaseURL'
import { isTauri } from 'lib/isTauri'
import { VITE_KC_API_BASE_URL, VITE_KC_DEV_TOKEN } from 'env'
import { getUser as getUserTauri } from 'lib/tauri'
import { COOKIE_NAME } from 'lib/constants'
const SKIP_AUTH =
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
@ -38,7 +39,6 @@ export type Events =
token?: string
}
const COOKIE_NAME = '__Secure-next-auth.session-token'
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
const persistedToken =
getCookie(COOKIE_NAME) || localStorage?.getItem(TOKEN_PERSIST_KEY) || ''

View File

@ -22,7 +22,7 @@ export type CommandBarMachineEvent =
| { type: 'Clear' }
| {
type: 'Select command'
data: { command: Command }
data: { command: Command; argDefaultValues?: { [x: string]: unknown } }
}
| { type: 'Deselect command' }
| { type: 'Submit command'; data: { [x: string]: unknown } }
@ -57,7 +57,11 @@ export type CommandBarMachineEvent =
}
| {
type: 'Find and select command'
data: { name: string; groupId: string }
data: {
name: string
groupId: string
argDefaultValues?: { [x: string]: unknown }
}
}
| {
type: 'Change current argument'
@ -328,7 +332,12 @@ export const commandBarMachine = createMachine(
context.argumentsToSubmit[argName] === undefined ||
(rejectedArg && rejectedArg.name === argName))
if (mustNotSkipArg === true) {
if (
mustNotSkipArg === true ||
argIndex + 1 === Object.keys(selectedCommand.args).length
) {
// If we have reached the end of the arguments and none are skippable,
// return the last argument.
return {
...selectedCommand.args[argName],
name: argName,
@ -393,7 +402,11 @@ export const commandBarMachine = createMachine(
const args: { [x: string]: unknown } = {}
for (const [argName, arg] of Object.entries(command.args)) {
args[argName] =
arg.skip && 'defaultValue' in arg ? arg.defaultValue : undefined
e.data.argDefaultValues && argName in e.data.argDefaultValues
? e.data.argDefaultValues[argName]
: arg.skip && 'defaultValue' in arg
? arg.defaultValue
: undefined
}
return args
},

View File

@ -4,7 +4,7 @@ import { Project } from 'wasm-lib/kcl/bindings/Project'
export const fileMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiAKwBGcQDoALACYAHAE4AzGoUA2JXK1yANCACeiabOmSlGpkqsqAvg6NpMuQiXIUASmABmAE5wRMxsSCDcfAJC4WIIWgpMMtpqkgrKalJa0kamCJIA7AUySgX6mkwK4gXVckpOLhjY+MRkYDIAEtRYpFxYfk2wFADCQXi82AOYocKRqPyCwnGSmbJa4ulKquJqcnriuYiaKsm2BSpVTAVyTOJyDSCuzR5tnd1TcD5gpHg4k00zcJzBYxUBxFRKWRKLS3XRqFSSHTVQ4IXRJAppRJWSr6erOR5NdytchvWD9QYjMYTcnTVizHjzaJLMyrGTrTbbXb7FF3E6SOSaNRMJhaLQFeEKB5PImedpdMkfIYAETAmGpH0BnAZIOZ+VZ7OUnL26xRui0MnE2WkpQxq0l+OlLVlpJpnwA8hxvq7NRFtUzYizxGsNoaVDtjQcTIh1JISsLMiLUlIrlLCU7XvLXUMAMpgXhYWCqsAECYQLAQVBBEtcALGH3A-1gsxpYoIla6BQqcUqbIo65JGF3LRCjSSJj3B1pl4k0ZgcZkKCuigQQTtMgANy4AGt2gQqWAALQaulAv2LAP5ZQnaoQuR3K5KJg5KMIeFJJSJfQFeM7NSQ1NuOmM5UguS5gAEAQ1jIHDoOMfg1jgMh7nOExHgCJ5alE55NnqloyBCWjqIo0iVJGeR2DImidgociIukOiEQBzzEu0vg-DgoEfMuq4yBu27tEE7GHseYSYYy2GiEcTBqPIVQ1FcEL8toKIJOaaQXPY2TWOIeKNIB06sd8vycU0FDgZBATQbBvDwQEiGCb8wnoaJvpYaCkmvp28gqD5kJ-lc-LQiif7mqKFwXNk8aVExMqvCqaomZg3EknxO4yBARaoSJ9JubqKx4SsNyWmkGgfkoPIQvhOg1AoRQ+TpkgxUB7TxXmiWUOZUEwXBCHpZlTm0i5DYScsmTFHopRPn+cg9oifZ3MkNp-to4iJloTUGTIvh4BWpCLoqyVrqQm5pWMEBoZgsD1me7lxHYMljpUNGJOKahin2dTyH+BR2J2w6WmoG0sVtc67ftFIrilx38TIZ0XXADCSENN26vdMiPekihXBo70vkmyQ2D5Qrik+BRA8621g1mZkQV11m2fZoPw1dGGueJt2IGjGPPdjb0FCi6QKPh4g9gK1G-qKTj4r0GXwOEjoGTl7O6gomgWtcVR3kVH6GC+B5QuUlphtJKhaxOenMc6ma9FmSs6hekiFLGyjfnUdwzQokjBV5ahlGUqSVPCYbmwS+nA5mip242HmOz9bKFEU7t3rVaimjcJSPoU1jSAUnviOTryzvOe2ulHI1mHcraclsOyiyoPLCnGthpDcGLrGTk5hxTRkcSXHxlxzCDKEktSa-eOk0aajslLstyu9J1j2hbsUkq1-B900A95ZX+HV35demtJBMTRCwqdpoBckpT7Vy2J9s4RsSgzyLiQRqTKJ7CcOtYtcqSFetndLavA9N8dqW8HY7whGGP8+99D1xfCoWwFodiijGrcT2eInBAA */
/** @xstate-layout N4IgpgJg5mDOIC5QDECWAbMACAtgQwGMALVAOzAGI9ZZUpSBtABgF1FQAHAe1oBdUupdiAAeiACwAmADQgAnogAcANgCsAOnHiAjOICcAZh3K9TRQHYAvpdlpMuQiXIUASmABmAJzhFmbJCDcfAJCAWIIUrIKCHpq6nraipJJKorahqrWthjY+MRkYOoAEtRYpFxY7jmwFADC3ni82FWYfsJBqPyCwuG6hurKTAYG5mlSyeJRiHqS2prKg6Mj2pKz4lkgdrmOBcWlLXCuYKR4OM05bQEdXaGgvdpD6qPiioqqieJM2gaqUxELT3MQz0eleBlMhmUGy2Dny5D2sEq1TqDSaSNarHaPE6IR6iD6BgGQxGY1Wikm8gkQM0n2UknERm0qmUw2hOVhTkKJURBxqABEwJg0QdLpxsTc8QhtNLlOozKNlNpzOZDEwTH80upEuZkg9zKoDJIDGo2fY8pyEejDgB5DjHK2iwLi3FhfH6QmDYajXRkinRHSKLXgph6VSSVQqh4GU3bOFc-bIgDKYF4WFggrABCaECwEFQ3izXE8ckd1xdd3xkhZ6neP0UQ1mZL+xvEcul4PSiiMOkkMY5u3qYEaZCgWDwpBzXDtpBHVooEEEhTIADcuABrQoEVFgAC044gO6nxx3IsxV2d3VdUtr6iSPwDH0+fvxTCrNdUTHEzKYP8NPz75oDqis77lgR4zqQo4HBQYCeJ4RbqBw6CNO4RY4OoW5Dk0e4Toe04nhcZ5isEl4VteTJaiy+riKMqhJF+fwRkw6jGskigJOSaTfABOzwm4Jw4LO0ELvCK7roU3gCbup7+MROKkaIiAjOY6j0uYyg6iyBqKuYfx0hoqixECIaGZITCqJkNibOygF8ccpxCTkMFwQhSEoWh6iSac0mEbJTokbcikIMpqk0RpVY-MaSp-Dqki3mCuhmGoRp6DxcbqAKQqOZg86LuoYkbuoEAZthMlYgFkohWp4VaVFumUsFP5apIoYWO26nKmlFqZSm2WULB8GeIhyG8KhnjocVQo+Rifllgp4RVWFmmRTpfxmXFXb1l+OlMl8XW7G4eB5pBVo1CJS6kKuhUNAevKlhegXhDMzE-OYwxqMoryhtoMUrHKhraIqtWzIo+12UdfVnXlBUSUOt3VAw2izQ9krPSxEbvcyX3vH8piyoqBoWF9rWpVZMK2YUh3HVByIDa5I1jehN0EZgsD3RVV5o69mOfexOMNfWegsUayphVWT5g-GPLIoOjTnK0SPlfJj1uv0nokj6EyMWZmhfMqiq0iGvZkzZvGFLL-AncJ0OXeJGHbizYDs8rkpMiqAyqDRryfcaJgGH8KzpKp9IvF+UhfJZ2Rmmb6gW31zmDcN7njfbWHTU7RH+S7V5MvW8R6C8hoGqYTDmD9DXSjqqlaeInpSAYTLWFZ5TFfAATk2bSsSleO7KH8O4aCCQ-D8PP6R9Z0fpdyZQVLyXflkF3whgMtcQq8+gWc+MSzAMm2GTMZkGEkkuWnP54c2R3xKpoEzfEfEYBn8q+aK8iS6EYmkn3HJ2geBfXz-NfEow4o+gsKsT6LVIgV1fISdS7EdQpAeBZE+-EHJWxyAAlWEQZANUWKpYYDYhiJGUF+E+PVLY00wJgyU7EBiA09uYHQ3YLL1WiJ2VSOofySEYR-AmKC4aQ2oVeOkcx7xh3JHoZUkjcbL09gXX84UuIn1tMcf+59s6X2AVqKQYCqxJALjg6I4JCQRi2jpZUnsv7AXQVQ9R3dNFJG0ckWKECDEByVHFUuFkwzGjeHRJulggA */
id: 'File machine',
initial: 'Reading files',
@ -29,7 +29,7 @@ export const fileMachine = createMachine(
'Has no files': {
on: {
'Create file': {
target: 'Creating file',
target: 'Creating and opening file',
},
},
},
@ -40,9 +40,13 @@ export const fileMachine = createMachine(
target: 'Renaming file',
},
'Create file': {
target: 'Creating file',
},
'Create file': [
{
target: 'Creating and opening file',
cond: 'Is not silent',
},
'Creating file',
],
'Delete file': {
target: 'Deleting file',
@ -59,10 +63,10 @@ export const fileMachine = createMachine(
},
},
'Creating file': {
'Creating and opening file': {
invoke: {
id: 'create-file',
src: 'createFile',
id: 'create-and-open-file',
src: 'createAndOpenFile',
onDone: [
{
target: 'Reading files',
@ -147,6 +151,15 @@ export const fileMachine = createMachine(
'Opening file': {
entry: ['navigateToFile'],
},
'Creating file': {
invoke: {
src: 'createFile',
id: 'create-file',
onDone: 'Reading files',
onError: 'Reading files',
},
},
},
schema: {
@ -156,7 +169,16 @@ export const fileMachine = createMachine(
type: 'Rename file'
data: { oldName: string; newName: string; isDir: boolean }
}
| { type: 'Create file'; data: { name: string; makeDir: boolean } }
| {
type: 'Create file'
data: {
name: string
makeDir: boolean
content?: string
silent?: boolean
makeUnique?: boolean
}
}
| { type: 'Delete file'; data: FileEntry }
| { type: 'Set selected directory'; data: FileEntry }
| { type: 'navigate'; data: { name: string } }
@ -173,12 +195,18 @@ export const fileMachine = createMachine(
}
}
| {
type: 'done.invoke.create-file'
type: 'done.invoke.create-and-open-file'
data: {
message: string
path: string
}
}
| {
type: 'done.invoke.create-file'
data: {
path: string
}
}
| { type: 'assign'; data: { [key: string]: any } }
| { type: 'Refresh' },
},

File diff suppressed because one or more lines are too long

View File

@ -686,9 +686,9 @@ dependencies = [
[[package]]
name = "data-encoding"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]]
name = "databake"
@ -724,7 +724,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.22"
version = "0.1.23"
dependencies = [
"Inflector",
"anyhow",
@ -1397,7 +1397,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.4"
version = "0.2.5"
dependencies = [
"anyhow",
"approx",
@ -1467,7 +1467,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.6"
version = "0.1.7"
dependencies = [
"anyhow",
"hyper",
@ -1480,9 +1480,9 @@ dependencies = [
[[package]]
name = "kittycad"
version = "0.3.13"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda4573582df5e220e6c3104dba6dc01acff529b36e289f41ca9cb5f91c117ae"
checksum = "ce5e9c51976882cdf6777557fd8c3ee68b00bb53e9307fc1721acb397f2ece9a"
dependencies = [
"anyhow",
"async-trait",
@ -3617,6 +3617,7 @@ dependencies = [
"bson",
"clap",
"console_error_panic_hook",
"data-encoding",
"futures",
"gloo-utils",
"hyper",

View File

@ -12,6 +12,7 @@ crate-type = ["cdylib"]
[dependencies]
bson = { version = "2.11.0", features = ["uuid-1", "chrono"] }
clap = "4.5.15"
data-encoding = "2.6.0"
gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad.workspace = true
@ -69,7 +70,7 @@ members = [
]
[workspace.dependencies]
kittycad = { version = "0.3.13", default-features = false, features = ["js", "requests"] }
kittycad = { version = "0.3.14", default-features = false, features = ["js", "requests"] }
kittycad-modeling-session = "0.1.4"
[[test]]

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.22"
version = "0.1.23"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-test-server"
description = "A test server for KCL"
version = "0.1.6"
version = "0.1.7"
edition = "2021"
license = "MIT"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.4"
version = "0.2.5"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -20,7 +20,7 @@ clap = { version = "4.5.15", default-features = false, optional = true }
convert_case = "0.6.0"
dashmap = "6.0.1"
databake = { version = "0.1.8", features = ["derive"] }
derive-docs = { version = "0.1.22", path = "../derive-docs" }
derive-docs = { version = "0.1.23", path = "../derive-docs" }
form_urlencoded = "1.2.1"
futures = { version = "0.3.30" }
git_rev = "0.1.0"
@ -116,3 +116,11 @@ required-features = ["lsp-test-util"]
name = "lsp_semantic_tokens_benchmark_iai"
harness = false
required-features = ["lsp-test-util"]
[[bench]]
name = "executor_benchmark_criterion"
harness = false
[[bench]]
name = "executor_benchmark_iai"
harness = false

View File

@ -0,0 +1,36 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use kcl_lib::test_server;
use tokio::runtime::Runtime;
pub fn bench_execute(c: &mut Criterion) {
for (name, code) in [
("big_kitt", KITT_PROGRAM),
("cube", CUBE_PROGRAM),
("server_rack_heavy", SERVER_RACK_HEAVY_PROGRAM),
] {
let mut group = c.benchmark_group("executor");
// Configure Criterion.rs to detect smaller differences and increase sample size to improve
// precision and counteract the resulting noise.
group.sample_size(10);
group.bench_with_input(BenchmarkId::new("execute_", name), &code, |b, &s| {
let rt = Runtime::new().unwrap();
// Spawn a future onto the runtime
b.iter(|| {
rt.block_on(test_server::execute_and_snapshot(
s,
kcl_lib::settings::types::UnitLength::Mm,
))
.unwrap();
});
});
group.finish();
}
}
criterion_group!(benches, bench_execute);
criterion_main!(benches);
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl");
const SERVER_RACK_HEAVY_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-heavy.kcl");

View File

@ -0,0 +1,16 @@
use iai::black_box;
async fn execute_server_rack_heavy() {
let code = SERVER_RACK_HEAVY_PROGRAM;
black_box(
kcl_lib::test_server::execute_and_snapshot(code, kcl_lib::settings::types::UnitLength::Mm)
.await
.unwrap(),
);
}
iai::main! {
execute_server_rack_heavy,
}
const SERVER_RACK_HEAVY_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-heavy.kcl");

View File

@ -1332,7 +1332,7 @@ impl CallExpression {
source_range: SourceRange([arg.start(), arg.end()]),
};
let result = ctx
.arg_into_mem_item(
.execute_expr(
arg,
memory,
dynamic_state,
@ -2977,7 +2977,7 @@ impl MemberExpression {
source_ranges: vec![self.clone().into()],
})),
(being_indexed, _) => {
let t = human_friendly_type(being_indexed);
let t = human_friendly_type(&being_indexed);
Err(KclError::Semantic(KclErrorDetails {
message: format!("Only arrays and objects can be indexed, but you're trying to index a {t}"),
source_ranges: vec![self.clone().into()],
@ -3196,6 +3196,18 @@ pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
}
}
/// JSON value as bool. If it isn't a bool, returns None.
pub fn json_as_bool(j: &serde_json::Value) -> Option<bool> {
match j {
JValue::Null => None,
JValue::Bool(b) => Some(*b),
JValue::Number(_) => None,
JValue::String(_) => None,
JValue::Array(_) => None,
JValue::Object(_) => None,
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
@ -3336,6 +3348,27 @@ impl UnaryExpression {
pipe_info: &PipeInfo,
ctx: &ExecutorContext,
) -> Result<KclValue, KclError> {
if self.operator == UnaryOperator::Not {
let value = self
.argument
.get_result(memory, dynamic_state, pipe_info, ctx)
.await?
.get_json_value()?;
let Some(bool_value) = json_as_bool(&value) else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Cannot apply unary operator ! to non-boolean value: {}", value),
source_ranges: vec![self.into()],
}));
};
let negated = !bool_value;
return Ok(KclValue::UserVal(UserVal {
value: serde_json::Value::Bool(negated),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let num = parse_json_number_as_f64(
&self
.argument
@ -3549,7 +3582,7 @@ async fn execute_pipe_body(
source_range: SourceRange([first.start(), first.end()]),
};
let output = ctx
.arg_into_mem_item(
.execute_expr(
first,
memory,
dynamic_state,
@ -3565,26 +3598,39 @@ async fn execute_pipe_body(
new_pipe_info.previous_results = Some(output);
// Evaluate remaining elements.
for expression in body {
let output = match expression {
Expr::BinaryExpression(binary_expression) => {
binary_expression
.get_result(memory, dynamic_state, &new_pipe_info, ctx)
.await?
}
Expr::CallExpression(call_expression) => {
call_expression
.execute(memory, dynamic_state, &new_pipe_info, ctx)
.await?
}
Expr::Identifier(identifier) => memory.get(&identifier.name, identifier.into())?.clone(),
_ => {
// Return an error this should not happen.
match expression {
Expr::TagDeclarator(_) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("This cannot be in a PipeExpression: {:?}", expression),
source_ranges: vec![expression.into()],
}));
}
Expr::Literal(_)
| Expr::Identifier(_)
| Expr::BinaryExpression(_)
| Expr::FunctionExpression(_)
| Expr::CallExpression(_)
| Expr::PipeExpression(_)
| Expr::PipeSubstitution(_)
| Expr::ArrayExpression(_)
| Expr::ObjectExpression(_)
| Expr::MemberExpression(_)
| Expr::UnaryExpression(_)
| Expr::None(_) => {}
};
let metadata = Metadata {
source_range: SourceRange([expression.start(), expression.end()]),
};
let output = ctx
.execute_expr(
expression,
memory,
dynamic_state,
&new_pipe_info,
&metadata,
StatementKind::Expression,
)
.await?;
new_pipe_info.previous_results = Some(output);
}
// Safe to unwrap here, because `newpipe_info` always has something pushed in when the `match first` executes.
@ -4071,7 +4117,7 @@ impl ConstraintLevels {
}
}
fn human_friendly_type(j: JValue) -> &'static str {
pub(crate) fn human_friendly_type(j: &JValue) -> &'static str {
match j {
JValue::Null => "null",
JValue::Bool(_) => "boolean (true/false value)",

View File

@ -11,7 +11,10 @@ use serde_json::Value as JValue;
use tower_lsp::lsp_types::{Position as LspPosition, Range as LspRange};
use crate::{
ast::types::{BodyItem, Expr, FunctionExpression, KclNone, Program, TagDeclarator},
ast::types::{
human_friendly_type, BodyItem, Expr, ExpressionStatement, FunctionExpression, KclNone, Program,
ReturnStatement, TagDeclarator,
},
engine::EngineManager,
errors::{KclError, KclErrorDetails},
fs::FileManager,
@ -311,6 +314,24 @@ impl KclValue {
_ => anyhow::bail!("Not a extrude group or extrude groups: {:?}", self),
}
}
/// Human readable type name used in error messages. Should not be relied
/// on for program logic.
pub(crate) fn human_friendly_type(&self) -> &'static str {
match self {
KclValue::UserVal(u) => human_friendly_type(&u.value),
KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::SketchGroup(_) => "SketchGroup",
KclValue::SketchGroups { .. } => "SketchGroups",
KclValue::ExtrudeGroup(_) => "ExtrudeGroup",
KclValue::ExtrudeGroups { .. } => "ExtrudeGroups",
KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "Function",
KclValue::Plane(_) => "Plane",
KclValue::Face(_) => "Face",
}
}
}
impl From<SketchGroupSet> for KclValue {
@ -1268,6 +1289,22 @@ impl From<SourceRange> for Metadata {
}
}
impl From<&ExpressionStatement> for Metadata {
fn from(exp_statement: &ExpressionStatement) -> Self {
Self {
source_range: SourceRange::new(exp_statement.start, exp_statement.end),
}
}
}
impl From<&ReturnStatement> for Metadata {
fn from(return_statement: &ReturnStatement) -> Self {
Self {
source_range: SourceRange::new(return_statement.start, return_statement.end),
}
}
}
/// A base path.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -1716,20 +1753,26 @@ impl ExecutorContext {
for statement in &program.body {
match statement {
BodyItem::ExpressionStatement(expression_statement) => {
if let Expr::PipeExpression(pipe_expr) = &expression_statement.expression {
pipe_expr.get_result(memory, dynamic_state, &pipe_info, self).await?;
} else if let Expr::CallExpression(call_expr) = &expression_statement.expression {
call_expr.execute(memory, dynamic_state, &pipe_info, self).await?;
}
let metadata = Metadata::from(expression_statement);
// Discard return value.
self.execute_expr(
&expression_statement.expression,
memory,
dynamic_state,
&pipe_info,
&metadata,
StatementKind::Expression,
)
.await?;
}
BodyItem::VariableDeclaration(variable_declaration) => {
for declaration in &variable_declaration.declarations {
let var_name = declaration.id.name.to_string();
let source_range: SourceRange = declaration.init.clone().into();
let source_range = SourceRange::from(&declaration.init);
let metadata = Metadata { source_range };
let memory_item = self
.arg_into_mem_item(
.execute_expr(
&declaration.init,
memory,
dynamic_state,
@ -1741,51 +1784,20 @@ impl ExecutorContext {
memory.add(&var_name, memory_item, source_range)?;
}
}
BodyItem::ReturnStatement(return_statement) => match &return_statement.argument {
Expr::BinaryExpression(bin_expr) => {
let result = bin_expr.get_result(memory, dynamic_state, &pipe_info, self).await?;
memory.return_ = Some(result);
}
Expr::UnaryExpression(unary_expr) => {
let result = unary_expr.get_result(memory, dynamic_state, &pipe_info, self).await?;
memory.return_ = Some(result);
}
Expr::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?.clone();
memory.return_ = Some(value);
}
Expr::Literal(literal) => {
memory.return_ = Some(literal.into());
}
Expr::TagDeclarator(tag) => {
memory.return_ = Some(tag.into());
}
Expr::ArrayExpression(array_expr) => {
let result = array_expr.execute(memory, dynamic_state, &pipe_info, self).await?;
memory.return_ = Some(result);
}
Expr::ObjectExpression(obj_expr) => {
let result = obj_expr.execute(memory, dynamic_state, &pipe_info, self).await?;
memory.return_ = Some(result);
}
Expr::CallExpression(call_expr) => {
let result = call_expr.execute(memory, dynamic_state, &pipe_info, self).await?;
memory.return_ = Some(result);
}
Expr::MemberExpression(member_expr) => {
let result = member_expr.get_result(memory)?;
memory.return_ = Some(result);
}
Expr::PipeExpression(pipe_expr) => {
let result = pipe_expr.get_result(memory, dynamic_state, &pipe_info, self).await?;
memory.return_ = Some(result);
}
Expr::PipeSubstitution(_) => {}
Expr::FunctionExpression(_) => {}
Expr::None(none) => {
memory.return_ = Some(KclValue::from(none));
}
},
BodyItem::ReturnStatement(return_statement) => {
let metadata = Metadata::from(return_statement);
let value = self
.execute_expr(
&return_statement.argument,
memory,
dynamic_state,
&pipe_info,
&metadata,
StatementKind::Expression,
)
.await?;
memory.return_ = Some(value);
}
}
}
@ -1804,7 +1816,7 @@ impl ExecutorContext {
Ok(memory.clone())
}
pub async fn arg_into_mem_item<'a>(
pub async fn execute_expr<'a>(
&self,
init: &Expr,
memory: &mut ProgramMemory,
@ -1814,8 +1826,8 @@ impl ExecutorContext {
statement_kind: StatementKind<'a>,
) -> Result<KclValue, KclError> {
let item = match init {
Expr::None(none) => none.into(),
Expr::Literal(literal) => literal.into(),
Expr::None(none) => KclValue::from(none),
Expr::Literal(literal) => KclValue::from(literal),
Expr::TagDeclarator(tag) => tag.execute(memory).await?,
Expr::Identifier(identifier) => {
let value = memory.get(&identifier.name, identifier.into())?;
@ -2044,6 +2056,15 @@ mod tests {
Ok(memory)
}
/// Convenience function to get a JSON value from memory and unwrap.
fn mem_get_json(memory: &ProgramMemory, name: &str) -> serde_json::Value {
memory
.get(name, SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_assign_two_variables() {
let ast = r#"const myVar = 5
@ -2355,6 +2376,29 @@ const thisBox = box({start: [0,0], l: 6, w: 10, h: 3})
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
#[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
async fn test_object_member_starting_pipeline() {
let ast = r#"
fn test2 = () => {
return {
thing: startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([0, 1], %)
|> line([1, 0], %)
|> line([0, -1], %)
|> close(%)
}
}
const x2 = test2()
x2.thing
|> extrude(10, %)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
#[ignore] // ignore til we get loops
async fn test_execute_with_function_sketch_loop_objects() {
@ -2691,6 +2735,172 @@ const bracket = startSketchOn('XY')
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_unary_operator_not_succeeds() {
let ast = r#"
fn returnTrue = () => { return !false }
const t = true
const f = false
let notTrue = !t
let notFalse = !f
let c = !!true
let d = !returnTrue()
assert(!false, "expected to pass")
fn check = (x) => {
assert(!x, "expected argument to be false")
return true
}
check(false)
"#;
let mem = parse_execute(ast).await.unwrap();
assert_eq!(serde_json::json!(false), mem_get_json(&mem, "notTrue"));
assert_eq!(serde_json::json!(true), mem_get_json(&mem, "notFalse"));
assert_eq!(serde_json::json!(true), mem_get_json(&mem, "c"));
assert_eq!(serde_json::json!(false), mem_get_json(&mem, "d"));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_unary_operator_not_on_non_bool_fails() {
let code1 = r#"
// Yup, this is null.
let myNull = 0 / 0
let notNull = !myNull
"#;
assert_eq!(
parse_execute(code1).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: null".to_owned(),
source_ranges: vec![SourceRange([56, 63])],
})
);
let code2 = "let notZero = !0";
assert_eq!(
parse_execute(code2).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: 0".to_owned(),
source_ranges: vec![SourceRange([14, 16])],
})
);
let code3 = r#"
let notEmptyString = !""
"#;
assert_eq!(
parse_execute(code3).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: \"\"".to_owned(),
source_ranges: vec![SourceRange([22, 25])],
})
);
let code4 = r#"
let obj = { a: 1 }
let notMember = !obj.a
"#;
assert_eq!(
parse_execute(code4).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: 1".to_owned(),
source_ranges: vec![SourceRange([36, 42])],
})
);
let code5 = "
let a = []
let notArray = !a";
assert_eq!(
parse_execute(code5).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: []".to_owned(),
source_ranges: vec![SourceRange([27, 29])],
})
);
let code6 = "
let x = {}
let notObject = !x";
assert_eq!(
parse_execute(code6).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: {}".to_owned(),
source_ranges: vec![SourceRange([28, 30])],
})
);
let code7 = "
fn x = () => { return 1 }
let notFunction = !x";
let fn_err = parse_execute(code7).await.unwrap_err().downcast::<KclError>().unwrap();
// These are currently printed out as JSON objects, so we don't want to
// check the full error.
assert!(
fn_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: "),
"Actual error: {:?}",
fn_err
);
let code8 = "
let myTagDeclarator = $myTag
let notTagDeclarator = !myTagDeclarator";
let tag_declarator_err = parse_execute(code8).await.unwrap_err().downcast::<KclError>().unwrap();
// These are currently printed out as JSON objects, so we don't want to
// check the full error.
assert!(
tag_declarator_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: {\"type\":\"TagDeclarator\","),
"Actual error: {:?}",
tag_declarator_err
);
let code9 = "
let myTagDeclarator = $myTag
let notTagIdentifier = !myTag";
let tag_identifier_err = parse_execute(code9).await.unwrap_err().downcast::<KclError>().unwrap();
// These are currently printed out as JSON objects, so we don't want to
// check the full error.
assert!(
tag_identifier_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: {\"type\":\"TagIdentifier\","),
"Actual error: {:?}",
tag_identifier_err
);
let code10 = "let notPipe = !(1 |> 2)";
assert_eq!(
// TODO: We don't currently parse this, but we should. It should be
// a runtime error instead.
parse_execute(code10).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Syntax(KclErrorDetails {
message: "Unexpected token".to_owned(),
source_ranges: vec![SourceRange([14, 15])],
})
);
let code11 = "
fn identity = (x) => { return x }
let notPipeSub = 1 |> identity(!%))";
assert_eq!(
// TODO: We don't currently parse this, but we should. It should be
// a runtime error instead.
parse_execute(code11).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Syntax(KclErrorDetails {
message: "Unexpected token".to_owned(),
source_ranges: vec![SourceRange([54, 56])],
})
);
// TODO: Add these tests when we support these types.
// let notNan = !NaN
// let notInfinity = !Infinity
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_negative_variable_in_binary_expression() {
let ast = r#"const sigmaAllow = 35000 // psi

View File

@ -972,11 +972,21 @@ async fn test_kcl_lsp_semantic_tokens() {
assert_eq!(semantic_tokens.data[0].length, 13);
assert_eq!(semantic_tokens.data[0].delta_start, 0);
assert_eq!(semantic_tokens.data[0].delta_line, 0);
assert_eq!(semantic_tokens.data[0].token_type, 8);
assert_eq!(
semantic_tokens.data[0].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::FUNCTION)
.unwrap()
);
assert_eq!(semantic_tokens.data[1].length, 4);
assert_eq!(semantic_tokens.data[1].delta_start, 14);
assert_eq!(semantic_tokens.data[1].delta_line, 0);
assert_eq!(semantic_tokens.data[1].token_type, 3);
assert_eq!(
semantic_tokens.data[1].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::STRING)
.unwrap()
);
} else {
panic!("Expected semantic tokens");
}
@ -1229,29 +1239,64 @@ const sphereDia = 0.5"#
assert_eq!(semantic_tokens.data[0].length, 15);
assert_eq!(semantic_tokens.data[0].delta_start, 0);
assert_eq!(semantic_tokens.data[0].delta_line, 0);
assert_eq!(semantic_tokens.data[0].token_type, 6);
assert_eq!(
semantic_tokens.data[0].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::COMMENT)
.unwrap()
);
assert_eq!(semantic_tokens.data[1].length, 232);
assert_eq!(semantic_tokens.data[1].delta_start, 0);
assert_eq!(semantic_tokens.data[1].delta_line, 1);
assert_eq!(semantic_tokens.data[1].token_type, 6);
assert_eq!(
semantic_tokens.data[1].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::COMMENT)
.unwrap()
);
assert_eq!(semantic_tokens.data[2].length, 88);
assert_eq!(semantic_tokens.data[2].delta_start, 0);
assert_eq!(semantic_tokens.data[2].delta_line, 2);
assert_eq!(semantic_tokens.data[2].token_type, 6);
assert_eq!(
semantic_tokens.data[2].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::COMMENT)
.unwrap()
);
assert_eq!(semantic_tokens.data[3].length, 5);
assert_eq!(semantic_tokens.data[3].delta_start, 0);
assert_eq!(semantic_tokens.data[3].delta_line, 1);
assert_eq!(semantic_tokens.data[3].token_type, 4);
assert_eq!(
semantic_tokens.data[3].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::KEYWORD)
.unwrap()
);
assert_eq!(semantic_tokens.data[4].length, 9);
assert_eq!(semantic_tokens.data[4].delta_start, 6);
assert_eq!(semantic_tokens.data[4].delta_line, 0);
assert_eq!(semantic_tokens.data[4].token_type, 1);
assert_eq!(
semantic_tokens.data[4].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::VARIABLE)
.unwrap()
);
assert_eq!(semantic_tokens.data[5].length, 1);
assert_eq!(semantic_tokens.data[5].delta_start, 10);
assert_eq!(semantic_tokens.data[5].token_type, 2);
assert_eq!(
semantic_tokens.data[5].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::OPERATOR)
.unwrap()
);
assert_eq!(semantic_tokens.data[6].length, 3);
assert_eq!(semantic_tokens.data[6].delta_start, 2);
assert_eq!(semantic_tokens.data[6].token_type, 0);
assert_eq!(
semantic_tokens.data[6].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::NUMBER)
.unwrap()
);
} else {
panic!("Expected semantic tokens");
}

View File

@ -1136,11 +1136,11 @@ fn unary_expression(i: TokenSlice) -> PResult<UnaryExpression> {
let (operator, op_token) = any
.try_map(|token: Token| match token.token_type {
TokenType::Operator if token.value == "-" => Ok((UnaryOperator::Neg, token)),
// TODO: negation. Original parser doesn't support `not` yet.
TokenType::Operator => Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{EXPECTED} but found {} which is an operator, but not a unary one (unary operators apply to just a single operand, your operator applies to two or more operands)", token.value.as_str(),),
})),
TokenType::Bang => Ok((UnaryOperator::Not, token)),
other => Err(KclError::Syntax(KclErrorDetails { source_ranges: token.as_source_ranges(), message: format!("{EXPECTED} but found {} which is {}", token.value.as_str(), other,) })),
})
.context(expected("a unary expression, e.g. -x or -3"))

View File

@ -460,8 +460,9 @@ where
let Some(val) = T::from_mem_item(arg) else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Argument at index {i} was supposed to be type {} but wasn't",
"Argument at index {i} was supposed to be type {} but found {}",
type_name::<T>(),
arg.human_friendly_type()
),
source_ranges: vec![args.source_range],
}));
@ -479,8 +480,9 @@ where
let Some(val) = T::from_mem_item(arg) else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Argument at index {i} was supposed to be type {} but wasn't",
type_name::<T>()
"Argument at index {i} was supposed to be type {} but found {}",
type_name::<T>(),
arg.human_friendly_type()
),
source_ranges: vec![args.source_range],
}));

View File

@ -50,6 +50,7 @@ pub async fn circle(args: Args) -> Result<KclValue, KclError> {
/// |> hole(circle([0, 15], 5, %), %)
///
/// const example = extrude(5, exampleSketch)
/// ```
#[stdlib {
name = "circle",
}]

View File

@ -50,6 +50,38 @@ pub async fn shell(args: Args) -> Result<KclValue, KclError> {
/// thickness: 0.25,
/// }, firstSketch)
/// ```
///
/// ```no_run
/// const firstSketch = startSketchOn('-XZ')
/// |> startProfileAt([-12, 12], %)
/// |> line([24, 0], %)
/// |> line([0, -24], %)
/// |> line([-24, 0], %)
/// |> close(%)
/// |> extrude(6, %)
///
/// // Remove the start face for the extrusion.
/// shell({
/// faces: ['start'],
/// thickness: 0.25,
/// }, firstSketch)
/// ```
///
/// ```no_run
/// const firstSketch = startSketchOn('XY')
/// |> startProfileAt([-12, 12], %)
/// |> line([24, 0], %)
/// |> line([0, -24], %)
/// |> line([-24, 0], %, $myTag)
/// |> close(%)
/// |> extrude(6, %)
///
/// // Remove a tagged face for the extrusion.
/// shell({
/// faces: [myTag],
/// thickness: 0.25,
/// }, firstSketch)
/// ```
#[stdlib {
name = "shell",
}]

View File

@ -72,6 +72,7 @@ impl TryFrom<TokenType> for SemanticTokenType {
TokenType::Operator => Self::OPERATOR,
TokenType::QuestionMark => Self::OPERATOR,
TokenType::String => Self::STRING,
TokenType::Bang => Self::OPERATOR,
TokenType::LineComment => Self::COMMENT,
TokenType::BlockComment => Self::COMMENT,
TokenType::Function => Self::FUNCTION,
@ -83,7 +84,6 @@ impl TryFrom<TokenType> for SemanticTokenType {
| TokenType::DoublePeriod
| TokenType::Hash
| TokenType::Dollar
| TokenType::Bang
| TokenType::Unknown => {
anyhow::bail!("unsupported token type: {:?}", token_type)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

142
src/wasm-lib/output.txt Normal file
View File

@ -0,0 +1,142 @@
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 16 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 824 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
test visuals::server_rack_heavy has been running for over 60 seconds
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
extrude
test visuals::server_rack_heavy ... FAILED
failures:
failures:
visuals::server_rack_heavy
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 142 filtered out; finished in 279.58s

View File

@ -564,3 +564,26 @@ pub fn parse_project_route(configuration: &str, route: &str) -> Result<JsValue,
// gloo-serialize crate instead.
JsValue::from_serde(&route).map_err(|e| e.to_string())
}
static ALLOWED_DECODING_FORMATS: &[data_encoding::Encoding] = &[
data_encoding::BASE64,
data_encoding::BASE64URL,
data_encoding::BASE64URL_NOPAD,
data_encoding::BASE64_MIME,
data_encoding::BASE64_NOPAD,
];
/// Base64 decode a string.
#[wasm_bindgen]
pub fn base64_decode(input: &str) -> Result<Vec<u8>, JsValue> {
console_error_panic_hook::set_once();
// Forgive alt base64 decoding formats
for config in ALLOWED_DECODING_FORMATS {
if let Ok(data) = config.decode(input.as_bytes()) {
return Ok(data);
}
}
Err(JsValue::from_str("Invalid base64 encoding"))
}

File diff suppressed because it is too large Load Diff

View File

@ -903,7 +903,7 @@ const part = rectShape([0, 0], 20, 20)
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([887, 936])], message: "Argument at index 0 was supposed to be type [f64; 2] but wasn't" }"#,
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([887, 936])], message: "Argument at index 0 was supposed to be type [f64; 2] but found string (text)" }"#,
);
}
@ -1425,7 +1425,7 @@ const secondSketch = startSketchOn(part001, '')
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([272, 298])], message: "Argument at index 1 was supposed to be type kcl_lib::std::sketch::FaceTag but wasn't" }"#
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([272, 298])], message: "Argument at index 1 was supposed to be type kcl_lib::std::sketch::FaceTag but found string (text)" }"#
);
}
@ -1763,7 +1763,7 @@ const baseExtrusion = extrude(width, sketch001)
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_engine_error_source_range_on_last_command() {
async fn kcl_test_shell_with_tag() {
let code = r#"const sketch001 = startSketchOn('XZ')
|> startProfileAt([61.74, 206.13], %)
|> xLine(305.11, %, $seg01)
@ -1778,12 +1778,8 @@ async fn kcl_test_engine_error_source_range_on_last_command() {
}, %)
"#;
let result = execute_and_snapshot(code, UnitLength::Mm).await;
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"engine: KclErrorDetails { source_ranges: [SourceRange([256, 312])], message: "Modeling command failed: [ApiError { error_code: InternalEngine, message: \"Invalid brep after shell operation\" }]" }"#
);
let result = execute_and_snapshot(code, UnitLength::Mm).await.unwrap();
assert_out("shell_with_tag", &result);
}
#[tokio::test(flavor = "multi_thread")]
@ -2236,7 +2232,7 @@ someFunction('INVALID')
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([37, 61]), SourceRange([65, 88])], message: "Argument at index 0 was supposed to be type kcl_lib::std::sketch::SketchData but wasn't" }"#
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([37, 61]), SourceRange([65, 88])], message: "Argument at index 0 was supposed to be type kcl_lib::std::sketch::SketchData but found string (text)" }"#
);
}
@ -2257,7 +2253,7 @@ someFunction('INVALID')
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([89, 114]), SourceRange([126, 155]), SourceRange([159, 182])], message: "Argument at index 0 was supposed to be type kcl_lib::std::sketch::SketchData but wasn't" }"#
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([89, 114]), SourceRange([126, 155]), SourceRange([159, 182])], message: "Argument at index 0 was supposed to be type kcl_lib::std::sketch::SketchData but found string (text)" }"#
);
}
@ -2269,6 +2265,6 @@ async fn kcl_test_fillet_and_shell() {
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
r#"engine: KclErrorDetails { source_ranges: [SourceRange([2004, 2065])], message: "Modeling command failed: [ApiError { error_code: InternalEngine, message: \"Shell of non-planar solid3d not available yet\" }]" }"#
r#"engine: KclErrorDetails { source_ranges: [SourceRange([2004, 2065])], message: "Modeling command failed: [ApiError { error_code: InternalEngine, message: \"Invalid brep after shell operation\" }]" }"#
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@ -1748,10 +1748,10 @@
resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60"
integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==
"@kittycad/lib@^0.0.70":
version "0.0.70"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.70.tgz#398ec3b2385cef055bbd7c2681040f08b3751ac6"
integrity sha512-P6IyfUIiCZ5Cc7EDx/apXBsmHAUmO/yhMw5E6fviMCRt0sNJnUfed6iTmfTpq2m44k7k8Vrn0WwrkQMsReLUhA==
"@kittycad/lib@^0.0.76":
version "0.0.76"
resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-0.0.76.tgz#d544a50c54547139d6dfa15730354313a51d0e49"
integrity sha512-14zzP7JS7J8xiwKltJqiszOCF9LdQeJK2nN58Xjiep+LOEVWtiLSGuILTameU2ryjA3aeQzPtNc1WJZ2JYRg2A==
dependencies:
node-fetch "3.3.2"
openapi-types "^12.0.0"
@ -7712,7 +7712,16 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -7790,7 +7799,14 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -8666,7 +8682,7 @@ workerpool@6.2.1:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -8684,6 +8700,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"