Compare commits
16 Commits
kurt-rebas
...
v0.24.11
| Author | SHA1 | Date | |
|---|---|---|---|
| e099c95c5f | |||
| f23bc673aa | |||
| b60c1e874d | |||
| 5857684ebc | |||
| e8fc6bc037 | |||
| 5bdd090119 | |||
| 669cab8737 | |||
| f1ea60d6ab | |||
| 3faec650b1 | |||
| b2b62ec163 | |||
| 5b798c2aa3 | |||
| a23bd1f034 | |||
| 4d00dddfd8 | |||
| f055acb6a6 | |||
| bf9d88e9a5 | |||
| 712a3790e8 |
@ -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.
|
||||
|
||||
@ -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)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 49 KiB |
@ -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')
|
||||
}
|
||||
|
||||
@ -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' })
|
||||
}
|
||||
|
||||
358
e2e/playwright/text-to-cad-tests.spec.ts
Normal 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')
|
||||
}
|
||||
@ -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": [
|
||||
{
|
||||
|
||||
@ -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
@ -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",
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@ -80,5 +80,5 @@
|
||||
}
|
||||
},
|
||||
"productName": "Zoo Modeling App",
|
||||
"version": "0.24.10"
|
||||
"version": "0.24.11"
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
109
src/components/CommandBar/CommandBarTextareaInput.tsx
Normal 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
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
364
src/components/ToastTextToCad.tsx
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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 { "=>" }
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -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?:
|
||||
|
||||
@ -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'
|
||||
|
||||
56
src/lib/crossPlatformFetch.ts
Normal 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
|
||||
}
|
||||
83
src/lib/machine-api.d.ts
vendored
@ -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
@ -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
|
||||
)
|
||||
}
|
||||
@ -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) || ''
|
||||
|
||||
@ -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
|
||||
},
|
||||
|
||||
@ -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' },
|
||||
},
|
||||
|
||||
15
src/wasm-lib/Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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]]
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
36
src/wasm-lib/kcl/benches/executor_benchmark_criterion.rs
Normal 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");
|
||||
16
src/wasm-lib/kcl/benches/executor_benchmark_iai.rs
Normal 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");
|
||||
@ -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)",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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],
|
||||
}));
|
||||
|
||||
@ -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",
|
||||
}]
|
||||
|
||||
@ -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",
|
||||
}]
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 152 KiB |
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_shell1.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
src/wasm-lib/kcl/tests/outputs/serial_test_example_shell2.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
142
src/wasm-lib/output.txt
Normal 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
|
||||
|
||||
@ -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"))
|
||||
}
|
||||
|
||||
1869
src/wasm-lib/tests/executor/inputs/server-rack-heavy.kcl
Normal 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\" }]" }"#
|
||||
);
|
||||
}
|
||||
|
||||
BIN
src/wasm-lib/tests/executor/outputs/shell_with_tag.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
39
yarn.lock
@ -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"
|
||||
|
||||