Compare commits

...

18 Commits

Author SHA1 Message Date
5b207d7d1a chore: adding unit test to test getNodeFromPath failures (#5134)
* chore: adding unit test to test the parsing of the code

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* chore: trying to console error more content for getNodeByPath since it is cryptic

* fix: removing testing unit test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* fix: fmt

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* fix: ...

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* fix: found da bug

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 15:13:43 -06:00
2fac213c58 Add engine message to dry run validation error toasts (#5175)
* Add engine message to dry run validation error toasts
Fixes #5174

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Add unit tests

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Reset snapshots

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Revert snapshot changes

* Fix lint

* Fix test

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 16:13:35 -05:00
2f72a8ef14 Helper functions for meta settings (get/update) (#5200)
* get the units back out

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

* updates

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

* edit

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

* updates

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

* updates

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

* add to wasmts

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

* fmt

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

* updates

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-01-31 21:11:15 +00:00
27ce9f8aa4 Remove project.toml fetch during Open Sample (#5203)
* Fix units in tests

* Don't default to mm

* Last fix

* FIx lint

* Remove project.toml fetching from samples
2025-01-31 15:38:04 -05:00
b0426e3f94 Refactor: separate authMachine from React (#5110)
* Create a global appMachine

* Strip authMachine of side-effects

* Replace react-bound authMachine use with XState actor use

* Fix import goof

* Register auth commands directly!

* @lf94 feedback: conver `AuthNavigationHandler` to `useAuthNavigation`

* Uh, fix signing out thank you @lf94

* Fix tsc

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)"

This reverts commit 8dc50b6a26.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 14:47:08 -05:00
d707c66e53 Migrate Extrude AST mod from XState action to actor (#5146)
* Migrate Extrude AST mod from XState action to actor

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* @lf94 feedback

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 18:34:38 +00:00
f8f44743fa Bug fix: open project command lists project names (#5176)
* Amend project open test to show failing case

* Fix command config to use live context value

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Update src/lib/commandBarConfigs/projectsCommandConfig.ts

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-01-31 16:04:34 +00:00
f262eda12a Turn off snapshot commit bot (#5197)
Our team is actively investigating a recent spike in flaky WebRTC
connection behavior, which has impacted the web-only Playwright snapshot
tests particularly hard. The bot squashes other GH Actions in it's wake,
so I think we should turn it off so we can see the other E2E tests more
clearly
2025-01-31 10:57:52 -05:00
9e1136195a Fix: Center rectangle now works again, works with new LiteralValue structure (#5172)
* fix: new Literal data structure update

* fix: updated the LiteralValue dereferencing and added some type narrowing helpers

* fix: updating formatting

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* fix: implementing a safer dereference method until we update createLiteraly()

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* why is this fallback here

* fix: updating the type narrowing function

* fix: restore this... see if snapshots trigger again

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* bump

* bump

* Add number with units formatting as KCL (#5195)

* Add number with units formatting as KCL

* Change type assertion helper to check what we need

* Fix rectangle unit test

* fix: adding a wait for execution to prevent clicking before lines are rendered

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-01-31 09:45:39 -06:00
4ff07ddaee Fix: Clearing sketch DOM elements after redirecting to the home page (#4965)
* fix: clear the previous DOM elements after page redirect

* fix: removed await delay since it can take awhile to destroy the sketch

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* fix: added E2E test which actually caught a logic bug, moved the logic to the correct location

* fix: removing unused import

* fix: push main back...

* fix: restoring code to old state

* fix: moved cleanup code

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-31 07:01:51 -06:00
1e565379a7 allow setting units in subfiles/ turn on execution in modules (#5178)
* allow setting units in subfiles

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

* updates

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

* turn on execution in modules

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

* updates to tests

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

* updates

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

* cleanup bad snaps

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

* updates

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

* change some snapshots;

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

* fix tests

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-01-30 00:26:49 +00:00
76e34ac4da Ignore cloned kcl-samples (#5179) 2025-01-29 17:20:03 -05:00
4cd427bf91 fix: Remove link from code editor docs (#5170)
Remove link from the stdlib import() docs
2025-01-29 16:00:00 -05:00
f321ecdff0 add more unit names/abreevs (#5173)
* add more unit names

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

* typo

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-01-29 12:09:27 +11:00
d114ab798c fix subdirs (opening kcl-samples from kcl-samples dir) (#5171)
* WIP: Add the KCL file path into the executor

* reuse the stuff that works with settings.project_directory

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

* fixes on both sides

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

* fixes on both sides

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

* helper method

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

* fixes

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

* update kcl-samples tests to not change dirs

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

* update kcl-samples tests to not change dirs

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

* fmt

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-01-28 23:43:39 +00:00
69fec37107 prepare change in fillet api (#5168)
* updates

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

* bump version

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

* updates

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

* more snaps

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-01-28 22:22:19 +00:00
8ca8c49cc3 Fix to not error when using imported whole modules (#5161)
* Fix to not error when using imported whole modules

* Add TODO about adding a warning
2025-01-28 20:17:27 +00:00
b25fc302fd Support using import statements for non-KCL files (#5122)
* Improve checking of import paths

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Import path kinds in AST

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Use import statement for foreign files

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-01-29 08:28:32 +13:00
140 changed files with 7831 additions and 1164 deletions

View File

@ -126,20 +126,20 @@ jobs:
- name: build electron - name: build electron
shell: bash shell: bash
run: yarn tron:package run: yarn tron:package
- name: Run ubuntu/chrome snapshots # - name: Run ubuntu/chrome snapshots
if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }} # if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
shell: bash # shell: bash
# TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest, # # TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest,
# but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes. # # but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes.
run: | # run: |
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1 # PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1
env: # env:
CI: true # CI: true
NODE_ENV: development # NODE_ENV: development
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} # VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
VITE_KC_SKIP_AUTH: true # VITE_KC_SKIP_AUTH: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }} # token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }} # snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
with: with:
@ -162,20 +162,20 @@ jobs:
then echo "modified=true" >> $GITHUB_OUTPUT then echo "modified=true" >> $GITHUB_OUTPUT
else echo "modified=false" >> $GITHUB_OUTPUT else echo "modified=false" >> $GITHUB_OUTPUT
fi fi
- name: Commit changes, if any # - name: Commit changes, if any
if: steps.git-check.outputs.modified == 'true' # if: steps.git-check.outputs.modified == 'true'
shell: bash # shell: bash
run: | # run: |
git add . # git add .
git config --local user.email "github-actions[bot]@users.noreply.github.com" # git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]" # git config --local user.name "github-actions[bot]"
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git # git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git fetch origin # git fetch origin
echo ${{ github.head_ref }} # echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }} # git checkout ${{ github.head_ref }}
git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true # git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true
git push # git push
git push origin ${{ github.head_ref }} # git push origin ${{ github.head_ref }}
# only upload artifacts if there's actually changes # only upload artifacts if there's actually changes
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: steps.git-check.outputs.modified == 'true' if: steps.git-check.outputs.modified == 'true'

2
.gitignore vendored
View File

@ -44,7 +44,7 @@ e2e/playwright/temp3.png
e2e/playwright/export-snapshots/* e2e/playwright/export-snapshots/*
!e2e/playwright/export-snapshots/*.png !e2e/playwright/export-snapshots/*.png
/kcl-samples
/test-results/ /test-results/
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/

View File

@ -4,14 +4,16 @@ excerpt: "Import a CAD file."
layout: manual layout: manual
--- ---
**WARNING:** This function is deprecated.
Import a CAD file. Import a CAD file.
**DEPRECATED** Prefer to use import statements.
For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory. For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory.
Note: The import command currently only works when using the native Modeling App. Note: The import command currently only works when using the native Modeling App.
For importing KCL functions using the `import` statement, see the docs on [KCL modules](/docs/kcl/modules).
```js ```js
import(file_path: String, options?: ImportFormat) -> ImportedGeometry import(file_path: String, options?: ImportFormat) -> ImportedGeometry
``` ```

View File

@ -51,7 +51,6 @@ layout: manual
* [`helixRevolutions`](kcl/helixRevolutions) * [`helixRevolutions`](kcl/helixRevolutions)
* [`hole`](kcl/hole) * [`hole`](kcl/hole)
* [`hollow`](kcl/hollow) * [`hollow`](kcl/hollow)
* [`import`](kcl/import)
* [`inch`](kcl/inch) * [`inch`](kcl/inch)
* [`lastSegX`](kcl/lastSegX) * [`lastSegX`](kcl/lastSegX)
* [`lastSegY`](kcl/lastSegY) * [`lastSegY`](kcl/lastSegY)

View File

@ -92765,7 +92765,7 @@
{ {
"name": "import", "name": "import",
"summary": "Import a CAD file.", "summary": "Import a CAD file.",
"description": "For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory.\n\nNote: The import command currently only works when using the native Modeling App.\n\nFor importing KCL functions using the `import` statement, see the docs on [KCL modules](/docs/kcl/modules).", "description": "**DEPRECATED** Prefer to use import statements.\n\nFor formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory.\n\nNote: The import command currently only works when using the native Modeling App.",
"tags": [], "tags": [],
"keywordArguments": false, "keywordArguments": false,
"args": [ "args": [
@ -93168,7 +93168,7 @@
"labelRequired": true "labelRequired": true
}, },
"unpublished": false, "unpublished": false,
"deprecated": false, "deprecated": true,
"examples": [ "examples": [
"model = import(\"tests/inputs/cube.obj\")", "model = import(\"tests/inputs/cube.obj\")",
"model = import(\"tests/inputs/cube.obj\", { format = \"obj\", units = \"m\" })", "model = import(\"tests/inputs/cube.obj\", { format = \"obj\", units = \"m\" })",

View File

@ -4,7 +4,6 @@ import { expect } from '@playwright/test'
type CmdBarSerialised = type CmdBarSerialised =
| { | {
stage: 'commandBarClosed' stage: 'commandBarClosed'
// TODO no more properties needed but needs to be implemented in _serialiseCmdBar
} }
| { | {
stage: 'pickCommand' stage: 'pickCommand'
@ -37,6 +36,9 @@ export class CmdBarFixture {
} }
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => { private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) {
return { stage: 'commandBarClosed' }
}
const reviewForm = this.page.locator('#review-form') const reviewForm = this.page.locator('#review-form')
const getHeaderArgs = async () => { const getHeaderArgs = async () => {
const inputs = await this.page.getByTestId('cmd-bar-input-tab').all() const inputs = await this.page.getByTestId('cmd-bar-input-tab').all()

View File

@ -1078,7 +1078,7 @@ sketch002 = startSketchOn('XZ')
await page.waitForTimeout(500) await page.waitForTimeout(500)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await expect( await expect(
page.getByText('Unable to sweep with the provided selection') page.getByText('Unable to sweep with the current selection. Reason:')
).toBeVisible() ).toBeVisible()
}) })
}) })
@ -1846,7 +1846,7 @@ sweep001 = sweep({ path = sketch002 }, sketch001)
await page.waitForTimeout(500) await page.waitForTimeout(500)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await expect( await expect(
page.getByText('Unable to shell with the provided selection') page.getByText('Unable to shell with the current selection. Reason:')
).toBeVisible() ).toBeVisible()
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
}) })

View File

@ -1525,7 +1525,7 @@ extrude001 = extrude(200, sketch001)`)
test( test(
'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)', 'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ context, page, cmdBar, homePage }, testInfo) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
await Promise.all([ await Promise.all([
fsp.mkdir(path.join(dir, 'router-template-slate'), { recursive: true }), fsp.mkdir(path.join(dir, 'router-template-slate'), { recursive: true }),
@ -1563,11 +1563,38 @@ test(
const pointOnModel = { x: 630, y: 280 } const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => { await test.step('Opening the bracket project via command palette should load the stream', async () => {
// expect to see the text bracket await homePage.expectState({
await expect(page.getByText('bracket')).toBeVisible() projectCards: [
{
title: 'bracket',
fileCount: 1,
},
{
title: 'router-template-slate',
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
await page.getByText('bracket').click() await cmdBar.openCmdBar()
await cmdBar.chooseCommand('open project')
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Open project',
currentArgKey: 'name',
currentArgValue: '',
headerArguments: {
Name: '',
},
highlightedHeaderArg: 'name',
})
await cmdBar.argumentInput.fill('brac')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'commandBarClosed',
})
await u.waitForPageLoad() await u.waitForPageLoad()
@ -1588,7 +1615,7 @@ test(
await expect(page.getByText('Create project')).toBeVisible() await expect(page.getByText('Create project')).toBeVisible()
}) })
await test.step('Opening the router-template project should load the stream', async () => { await test.step('Opening the router-template project via link should load the stream', async () => {
// expect to see the text bracket // expect to see the text bracket
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
@ -1605,16 +1632,26 @@ test(
.toBeLessThan(15) .toBeLessThan(15)
}) })
await test.step('Opening the router-template project should load the stream', async () => { await test.step('The projects on the home page should still be normal', async () => {
await page.getByTestId('project-sidebar-toggle').click() await page.getByTestId('project-sidebar-toggle').click()
await expect( await expect(
page.getByRole('button', { name: 'Go to Home' }) page.getByRole('button', { name: 'Go to Home' })
).toBeVisible() ).toBeVisible()
await page.getByRole('button', { name: 'Go to Home' }).click() await page.getByRole('button', { name: 'Go to Home' }).click()
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() await homePage.expectState({
await expect(page.getByText('router-template-slate')).toBeVisible() projectCards: [
await expect(page.getByText('Create project')).toBeVisible() {
title: 'bracket',
fileCount: 1,
},
{
title: 'router-template-slate',
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
}) })
} }
) )

View File

@ -34,7 +34,7 @@ test.describe('Sketch tests', () => {
screwRadius = 3 screwRadius = 3
wireRadius = 2 wireRadius = 2
wireOffset = 0.5 wireOffset = 0.5
screwHole = startSketchOn('XY') screwHole = startSketchOn('XY')
${startProfileAt1} ${startProfileAt1}
|> arc({ |> arc({
@ -42,7 +42,7 @@ test.describe('Sketch tests', () => {
angleStart = 0, angleStart = 0,
angleEnd = 360 angleEnd = 360
}, %) }, %)
part001 = startSketchOn('XY') part001 = startSketchOn('XY')
${startProfileAt2} ${startProfileAt2}
|> xLine(width * .5, %) |> xLine(width * .5, %)
@ -51,7 +51,7 @@ test.describe('Sketch tests', () => {
|> close(%) |> close(%)
|> hole(screwHole, %) |> hole(screwHole, %)
|> extrude(thickness, %) |> extrude(thickness, %)
part002 = startSketchOn('-XZ') part002 = startSketchOn('-XZ')
${startProfileAt3} ${startProfileAt3}
|> xLine(width / 4, %) |> xLine(width / 4, %)
@ -99,6 +99,7 @@ test.describe('Sketch tests', () => {
test('Can delete most of a sketch and the line tool will still work', async ({ test('Can delete most of a sketch and the line tool will still work', async ({
page, page,
homePage, homePage,
scene,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
@ -112,12 +113,13 @@ test.describe('Sketch tests', () => {
}) })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await expect(async () => { await expect(async () => {
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click() await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
await expect( await expect(
page.getByRole('button', { name: 'Edit Sketch' }) page.getByRole('button', { name: 'Edit Sketch' })
).toBeEnabled({ timeout: 1000 }) ).toBeEnabled({ timeout: 2000 })
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
}).toPass({ timeout: 40_000, intervals: [1_000] }) }).toPass({ timeout: 40_000, intervals: [1_000] })
@ -1063,7 +1065,7 @@ test.describe('Sketch tests', () => {
`lugHeadLength = 0.25 `lugHeadLength = 0.25
lugDiameter = 0.5 lugDiameter = 0.5
lugLength = 2 lugLength = 2
fn lug = (origin, length, diameter, plane) => { fn lug = (origin, length, diameter, plane) => {
lugSketch = startSketchOn(plane) lugSketch = startSketchOn(plane)
|> startProfileAt([origin[0] + lugDiameter / 2, origin[1]], %) |> startProfileAt([origin[0] + lugDiameter / 2, origin[1]], %)
@ -1072,10 +1074,10 @@ test.describe('Sketch tests', () => {
|> yLineTo(0, %) |> yLineTo(0, %)
|> close(%) |> close(%)
|> revolve({ axis = "Y" }, %) |> revolve({ axis = "Y" }, %)
return lugSketch return lugSketch
} }
lug([0, 0], 10, .5, "XY")` lug([0, 0], 10, .5, "XY")`
) )
}) })
@ -1127,14 +1129,14 @@ test.describe('Sketch tests', () => {
`fn in2mm = (inches) => { `fn in2mm = (inches) => {
return inches * 25.4 return inches * 25.4
} }
const railTop = in2mm(.748) const railTop = in2mm(.748)
const railSide = in2mm(.024) const railSide = in2mm(.024)
const railBaseWidth = in2mm(.612) const railBaseWidth = in2mm(.612)
const railWideWidth = in2mm(.835) const railWideWidth = in2mm(.835)
const railBaseLength = in2mm(.200) const railBaseLength = in2mm(.200)
const railClampable = in2mm(.200) const railClampable = in2mm(.200)
const rail = startSketchOn('XZ') const rail = startSketchOn('XZ')
|> startProfileAt([ |> startProfileAt([
-railTop / 2, -railTop / 2,
@ -1405,3 +1407,46 @@ test.describe(`Click based selection don't brick the app when clicked out of ran
}) })
}) })
}) })
// Regression test for https://github.com/KittyCAD/modeling-app/issues/4372
test.describe('Redirecting to home page and back to the original file should clear sketch DOM elements', () => {
test('Can redirect to home page and back to original file and have a cleared DOM', async ({
context,
page,
scene,
toolbar,
editor,
homePage,
}) => {
// We seed the scene with a single offset plane
await context.addInitScript(() => {
localStorage.setItem(
'persistCode',
` sketch001 = startSketchOn('XZ')
|> startProfileAt([256.85, 14.41], %)
|> lineTo([0, 211.07], %)
`
)
})
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
const [objClick] = scene.makeMouseHelpers(634, 274)
await objClick()
// Enter sketch mode
await toolbar.editSketch()
await expect(page.getByText('323.49')).toBeVisible()
// Open navigation side bar
await page.getByTestId('project-sidebar-toggle').click()
const goToHome = page.getByRole('button', {
name: 'Go to Home',
})
await goToHome.click()
await homePage.openProject('testDefault')
await expect(page.getByText('323.49')).not.toBeVisible()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -69,7 +69,6 @@ test.describe('Testing in-app sample loading', () => {
await confirmButton.click() await confirmButton.click()
await editor.expectEditor.toContain('// ' + newSample.title) await editor.expectEditor.toContain('// ' + newSample.title)
await expect(unitsToast('in')).toBeVisible()
}) })
}) })
@ -158,7 +157,6 @@ test.describe('Testing in-app sample loading', () => {
await editor.expectEditor.toContain('// ' + sampleOne.title) await editor.expectEditor.toContain('// ' + sampleOne.title)
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible() await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
await expect(projectMenuButton).toContainText(sampleOne.file) await expect(projectMenuButton).toContainText(sampleOne.file)
await expect(unitsToast('in')).toBeVisible()
}) })
await test.step(`Now overwrite the current file`, async () => { await test.step(`Now overwrite the current file`, async () => {
@ -188,7 +186,6 @@ test.describe('Testing in-app sample loading', () => {
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible() await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
await expect(newlyCreatedFile(sampleTwo.file)).not.toBeVisible() await expect(newlyCreatedFile(sampleTwo.file)).not.toBeVisible()
await expect(projectMenuButton).toContainText(sampleOne.file) await expect(projectMenuButton).toContainText(sampleOne.file)
await expect(unitsToast('mm')).toBeVisible()
}) })
} }
) )

View File

@ -25,6 +25,7 @@ import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher' import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
import { maybeWriteToDisk } from 'lib/telemetry' import { maybeWriteToDisk } from 'lib/telemetry'
import { commandBarActor } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
maybeWriteToDisk() maybeWriteToDisk()
.then(() => {}) .then(() => {})
.catch(() => {}) .catch(() => {})
@ -60,8 +61,8 @@ export function App() {
useHotKeyListener() useHotKeyListener()
const { auth, settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const token = auth?.context?.token const token = useToken()
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),

View File

@ -1,10 +1,10 @@
import { useAuthState } from 'machines/appMachine'
import Loading from './components/Loading' import Loading from './components/Loading'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
// Wrapper around protected routes, used in src/Router.tsx // Wrapper around protected routes, used in src/Router.tsx
export const Auth = ({ children }: React.PropsWithChildren) => { export const Auth = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext() const authState = useAuthState()
const isLoggingIn = auth?.state.matches('checkIfLoggedIn') const isLoggingIn = authState.matches('checkIfLoggedIn')
return isLoggingIn ? ( return isLoggingIn ? (
<Loading> <Loading>

View File

@ -37,7 +37,6 @@ import { KclContextProvider } from 'lang/KclProvider'
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants' import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
import { CoreDumpManager } from 'lib/coredump' import { CoreDumpManager } from 'lib/coredump'
import { codeManager, engineCommandManager } from 'lib/singletons' import { codeManager, engineCommandManager } from 'lib/singletons'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import useHotkeyWrapper from 'lib/hotkeyWrapper' import useHotkeyWrapper from 'lib/hotkeyWrapper'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { coreDump } from 'lang/wasm' import { coreDump } from 'lang/wasm'
@ -47,6 +46,7 @@ import { reportRejection } from 'lib/trap'
import { RouteProvider } from 'components/RouteProvider' import { RouteProvider } from 'components/RouteProvider'
import { ProjectsContextProvider } from 'components/ProjectsContextProvider' import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler' import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
import { useToken } from 'machines/appMachine'
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
@ -203,8 +203,7 @@ export const Router = () => {
} }
function CoreDump() { function CoreDump() {
const { auth } = useSettingsAuthContext() const token = useToken()
const token = auth?.context?.token
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),
[] []

View File

@ -124,6 +124,14 @@ export const ClientSideScene = ({
'mouseup', 'mouseup',
toSync(sceneInfra.onMouseUp, reportRejection) toSync(sceneInfra.onMouseUp, reportRejection)
) )
sceneEntitiesManager
.tearDownSketch()
.then(() => {
// no op
})
.catch((e) => {
console.error(e)
})
} }
}, []) }, [])

View File

@ -2,11 +2,11 @@ import { Toolbar } from '../Toolbar'
import UserSidebarMenu from 'components/UserSidebarMenu' import UserSidebarMenu from 'components/UserSidebarMenu'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import ProjectSidebarMenu from './ProjectSidebarMenu' import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import styles from './AppHeader.module.css' import styles from './AppHeader.module.css'
import { RefreshButton } from 'components/RefreshButton' import { RefreshButton } from 'components/RefreshButton'
import { CommandBarOpenButton } from './CommandBarOpenButton' import { CommandBarOpenButton } from './CommandBarOpenButton'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { useUser } from 'machines/appMachine'
interface AppHeaderProps extends React.PropsWithChildren { interface AppHeaderProps extends React.PropsWithChildren {
showToolbar?: boolean showToolbar?: boolean
@ -24,8 +24,7 @@ export const AppHeader = ({
style, style,
enableMenu = false, enableMenu = false,
}: AppHeaderProps) => { }: AppHeaderProps) => {
const { auth } = useSettingsAuthContext() const user = useUser()
const user = auth?.context?.user
return ( return (
<header <header

View File

@ -98,6 +98,7 @@ export const CommandBar = () => {
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' + 'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
(isSelectionArgument ? 'pointer-events-none' : '') (isSelectionArgument ? 'pointer-events-none' : '')
} }
data-testid="command-bar-wrapper"
> >
<Transition.Child <Transition.Child
enter="duration-100 ease-out" enter="duration-100 ease-out"

View File

@ -30,6 +30,7 @@ import {
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
import { commandBarActor } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -47,7 +48,8 @@ export const FileMachineProvider = ({
children: React.ReactNode children: React.ReactNode
}) => { }) => {
const navigate = useNavigate() const navigate = useNavigate()
const { settings, auth } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const token = useToken()
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const { project, file } = projectData const { project, file } = projectData
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>( const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
@ -297,7 +299,7 @@ export const FileMachineProvider = ({
const kclCommandMemo = useMemo( const kclCommandMemo = useMemo(
() => () =>
kclCommands({ kclCommands({
authToken: auth?.context?.token ?? '', authToken: token ?? '',
projectData, projectData,
settings: { settings: {
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm', defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',

View File

@ -27,6 +27,7 @@ import { PROJECT_ENTRYPOINT } from 'lib/constants'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { codeManager } from 'lib/singletons' import { codeManager } from 'lib/singletons'
import { useToken } from 'machines/appMachine'
function getWorkspaceFolders(): LSP.WorkspaceFolder[] { function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
return [] return []
@ -69,8 +70,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const [isKclLspReady, setIsKclLspReady] = useState(false) const [isKclLspReady, setIsKclLspReady] = useState(false)
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false) const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
const { auth } = useSettingsAuthContext() const token = useToken()
const token = auth?.context.token
const navigate = useNavigate() const navigate = useNavigate()
// So this is a bit weird, we need to initialize the lsp server and client. // So this is a bit weird, we need to initialize the lsp server and client.

View File

@ -89,6 +89,7 @@ import { Node } from 'wasm-lib/kcl/bindings/Node'
import { promptToEditFlow } from 'lib/promptToEdit' import { promptToEditFlow } from 'lib/promptToEdit'
import { kclEditorActor } from 'machines/kclEditorMachine' import { kclEditorActor } from 'machines/kclEditorMachine'
import { commandBarActor } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
import { useToken } from 'machines/appMachine'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -110,7 +111,6 @@ export const ModelingMachineProvider = ({
children: React.ReactNode children: React.ReactNode
}) => { }) => {
const { const {
auth,
settings: { settings: {
context: { context: {
app: { theme, enableSSAO, allowOrbitInSketchMode }, app: { theme, enableSSAO, allowOrbitInSketchMode },
@ -127,7 +127,7 @@ export const ModelingMachineProvider = ({
const navigate = useNavigate() const navigate = useNavigate()
const { context, send: fileMachineSend } = useFileContext() const { context, send: fileMachineSend } = useFileContext()
const { file } = useLoaderData() as IndexLoaderData const { file } = useLoaderData() as IndexLoaderData
const token = auth?.context?.token const token = useToken()
const streamRef = useRef<HTMLDivElement>(null) const streamRef = useRef<HTMLDivElement>(null)
const persistedContext = useMemo(() => getPersistedContext(), []) const persistedContext = useMemo(() => getPersistedContext(), [])

View File

@ -20,6 +20,7 @@ import { useSelector } from '@xstate/react'
import { copyFileShareLink } from 'lib/links' import { copyFileShareLink } from 'lib/links'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { DEV } from 'env' import { DEV } from 'env'
import { useToken } from 'machines/appMachine'
const ProjectSidebarMenu = ({ const ProjectSidebarMenu = ({
project, project,
@ -103,7 +104,8 @@ function ProjectMenuPopover({
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
const { settings, auth } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const token = useToken()
const machineManager = useContext(MachineManagerContext) const machineManager = useContext(MachineManagerContext)
const commands = useSelector(commandBarActor, commandsSelector) const commands = useSelector(commandBarActor, commandsSelector)
@ -194,7 +196,7 @@ function ProjectMenuPopover({
disabled: !DEV, disabled: !DEV,
onClick: async () => { onClick: async () => {
await copyFileShareLink({ await copyFileShareLink({
token: auth?.context.token || '', token: token ?? '',
code: codeManager.code, code: codeManager.code,
name: project?.name || '', name: project?.name || '',
units: settings.context.modeling.defaultUnit.current, units: settings.context.modeling.defaultUnit.current,

View File

@ -8,10 +8,10 @@ import Tooltip from './Tooltip'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { toSync } from 'lib/utils' import { toSync } from 'lib/utils'
import { useToken } from 'machines/appMachine'
export const RefreshButton = ({ children }: React.PropsWithChildren) => { export const RefreshButton = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext() const token = useToken()
const token = auth?.context?.token
const coreDumpManager = useMemo( const coreDumpManager = useMemo(
() => new CoreDumpManager(engineCommandManager, codeManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),
[] []

View File

@ -2,13 +2,16 @@ import { useEffect, useState, createContext, ReactNode } from 'react'
import { useNavigation, useLocation } from 'react-router-dom' import { useNavigation, useLocation } from 'react-router-dom'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
import { useAuthNavigation } from 'hooks/useAuthNavigation'
export const RouteProviderContext = createContext({}) export const RouteProviderContext = createContext({})
export function RouteProvider({ children }: { children: ReactNode }) { export function RouteProvider({ children }: { children: ReactNode }) {
useAuthNavigation()
const [first, setFirstState] = useState(true) const [first, setFirstState] = useState(true)
const navigation = useNavigation() const navigation = useNavigation()
const location = useLocation() const location = useLocation()
useEffect(() => { useEffect(() => {
// On initialization, the react-router-dom does not send a 'loading' state event. // On initialization, the react-router-dom does not send a 'loading' state event.
// it sends an idle event first. // it sends an idle event first.

View File

@ -2,10 +2,7 @@ import { trap } from 'lib/trap'
import { useMachine, useSelector } from '@xstate/react' import { useMachine, useSelector } from '@xstate/react'
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom' import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
import { PATHS, BROWSER_PATH } from 'lib/paths' import { PATHS, BROWSER_PATH } from 'lib/paths'
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useState } from 'react' import React, { createContext, useEffect, useState } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { settingsMachine } from 'machines/settingsMachine' import { settingsMachine } from 'machines/settingsMachine'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { import {
@ -16,7 +13,6 @@ import {
} from 'lib/theme' } from 'lib/theme'
import decamelize from 'decamelize' import decamelize from 'decamelize'
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate' import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import { import {
kclManager, kclManager,
sceneInfra, sceneInfra,
@ -50,7 +46,6 @@ type MachineContext<T extends AnyStateMachine> = {
} }
type SettingsAuthContextType = { type SettingsAuthContextType = {
auth: MachineContext<typeof authMachine>
settings: MachineContext<typeof settingsMachine> settings: MachineContext<typeof settingsMachine>
} }
@ -370,40 +365,9 @@ export const SettingsAuthProviderBase = ({
) )
}, [settingsState.context.textEditor.blinkingCursor.current]) }, [settingsState.context.textEditor.blinkingCursor.current])
// Auth machine setup
const [authState, authSend, authActor] = useMachine(
authMachine.provide({
actions: {
goToSignInPage: () => {
navigate(PATHS.SIGN_IN)
// eslint-disable-next-line @typescript-eslint/no-floating-promises
logout()
},
goToIndexPage: () => {
if (location.pathname.includes(PATHS.SIGN_IN)) {
navigate(PATHS.INDEX)
}
},
},
})
)
useStateMachineCommands({
machineId: 'auth',
state: authState,
send: authSend,
commandBarConfig: authCommandBarConfig,
actor: authActor,
})
return ( return (
<SettingsAuthContext.Provider <SettingsAuthContext.Provider
value={{ value={{
auth: {
state: authState,
context: authState.context,
send: authSend,
},
settings: { settings: {
state: settingsState, state: settingsState,
context: settingsState.context, context: settingsState.context,
@ -417,12 +381,3 @@ export const SettingsAuthProviderBase = ({
} }
export default SettingsAuthProvider export default SettingsAuthProvider
export async function logout() {
localStorage.removeItem(TOKEN_PERSIST_KEY)
if (isDesktop()) return Promise.resolve(null)
return fetch(withBaseUrl('/logout'), {
method: 'POST',
credentials: 'include',
})
}

View File

@ -4,12 +4,12 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useMemo, useState } from 'react' import { Fragment, useMemo, useState } from 'react'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { CustomIcon } from './CustomIcon' import { CustomIcon } from './CustomIcon'
import { authActor } from 'machines/appMachine'
type User = Models['User_type'] type User = Models['User_type']
@ -20,7 +20,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
const displayedName = getDisplayName(user) const displayedName = getDisplayName(user)
const [imageLoadFailed, setImageLoadFailed] = useState(false) const [imageLoadFailed, setImageLoadFailed] = useState(false)
const navigate = useNavigate() const navigate = useNavigate()
const send = useSettingsAuthContext()?.auth?.send const send = authActor.send
// We filter this memoized list so that no orphan "break" elements are rendered. // We filter this memoized list so that no orphan "break" elements are rendered.
const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>( const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>(

View File

@ -0,0 +1,29 @@
import { PATHS } from 'lib/paths'
import { useAuthState } from 'machines/appMachine'
import { useEffect } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
/**
* A simple hook that listens to the auth state of the app and navigates
* accordingly.
*/
export function useAuthNavigation() {
const navigate = useNavigate()
const location = useLocation()
const authState = useAuthState()
// Subscribe to the auth state of the app and navigate accordingly.
useEffect(() => {
if (
authState.matches('loggedIn') &&
location.pathname.includes(PATHS.SIGN_IN)
) {
navigate(PATHS.INDEX)
} else if (
authState.matches('loggedOut') &&
!location.pathname.includes(PATHS.SIGN_IN)
) {
navigate(PATHS.SIGN_IN)
}
}, [authState])
}

View File

@ -322,6 +322,7 @@ export class KclManager {
await this.ensureWasmInit() await this.ensureWasmInit()
const { logs, errors, execState, isInterrupted } = await executeAst({ const { logs, errors, execState, isInterrupted } = await executeAst({
ast, ast,
path: codeManager.currentFilePath || undefined,
engineCommandManager: this.engineCommandManager, engineCommandManager: this.engineCommandManager,
}) })

View File

@ -80,6 +80,10 @@ export default class CodeManager {
})) }))
} }
get currentFilePath(): string | null {
return this._currentFilePath
}
updateCurrentFilePath(path: string) { updateCurrentFilePath(path: string) {
this._currentFilePath = path this._currentFilePath = path
} }

View File

@ -52,27 +52,22 @@ afterAll(async () => {
} catch (e) {} } catch (e) {}
}) })
afterEach(() => { describe('Test KCL Samples from public Github repository', () => {
process.chdir('..') describe('when performing enginelessExecutor', () => {
})
// The tests have to be sequential because we need to change directories
// to support `import` working properly.
// @ts-expect-error
describe.sequential('Test KCL Samples from public Github repository', () => {
// @ts-expect-error
describe.sequential('when performing enginelessExecutor', () => {
manifest.forEach((file: KclSampleFile) => { manifest.forEach((file: KclSampleFile) => {
// @ts-expect-error it(
it.sequential(
`should execute ${file.title} (${file.file}) successfully`, `should execute ${file.title} (${file.file}) successfully`,
async () => { async () => {
const [dirProject, fileKcl] = const code = await fs.readFile(
file.pathFromProjectDirectoryToFirstFile.split('/') file.pathFromProjectDirectoryToFirstFile,
process.chdir(dirProject) 'utf-8'
const code = await fs.readFile(fileKcl, 'utf-8') )
const ast = assertParse(code) const ast = assertParse(code)
await enginelessExecutor(ast, programMemoryInit()) await enginelessExecutor(
ast,
programMemoryInit(),
file.pathFromProjectDirectoryToFirstFile
)
}, },
files.length * 1000 files.length * 1000
) )

View File

@ -46,12 +46,14 @@ export const toolTips: Array<ToolTip> = [
export async function executeAst({ export async function executeAst({
ast, ast,
path,
engineCommandManager, engineCommandManager,
// If you set programMemoryOverride we assume you mean mock mode. Since that // If you set programMemoryOverride we assume you mean mock mode. Since that
// is the only way to go about it. // is the only way to go about it.
programMemoryOverride, programMemoryOverride,
}: { }: {
ast: Node<Program> ast: Node<Program>
path?: string
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
programMemoryOverride?: ProgramMemory programMemoryOverride?: ProgramMemory
isInterrupted?: boolean isInterrupted?: boolean
@ -63,8 +65,8 @@ export async function executeAst({
}> { }> {
try { try {
const execState = await (programMemoryOverride const execState = await (programMemoryOverride
? enginelessExecutor(ast, programMemoryOverride) ? enginelessExecutor(ast, programMemoryOverride, path)
: executor(ast, engineCommandManager)) : executor(ast, engineCommandManager, path))
await engineCommandManager.waitForAllCommands() await engineCommandManager.waitForAllCommands()

View File

@ -5,6 +5,8 @@ import {
Identifier, Identifier,
SourceRange, SourceRange,
topLevelRange, topLevelRange,
LiteralValue,
Literal,
} from './wasm' } from './wasm'
import { import {
createLiteral, createLiteral,
@ -37,10 +39,26 @@ beforeAll(async () => {
}) })
describe('Testing createLiteral', () => { describe('Testing createLiteral', () => {
it('should create a literal', () => { it('should create a literal number without units', () => {
const result = createLiteral(5) const result = createLiteral(5)
expect(result.type).toBe('Literal') expect(result.type).toBe('Literal')
expect((result as any).value.value).toBe(5) expect((result as any).value.value).toBe(5)
expect((result as any).value.suffix).toBe('None')
expect((result as Literal).raw).toBe('5')
})
it('should create a literal number with units', () => {
const lit: LiteralValue = { value: 5, suffix: 'Mm' }
const result = createLiteral(lit)
expect(result.type).toBe('Literal')
expect((result as any).value.value).toBe(5)
expect((result as any).value.suffix).toBe('Mm')
expect((result as Literal).raw).toBe('5mm')
})
it('should create a literal boolean', () => {
const result = createLiteral(false)
expect(result.type).toBe('Literal')
expect((result as Literal).value).toBe(false)
expect((result as Literal).raw).toBe('false')
}) })
}) })
describe('Testing createIdentifier', () => { describe('Testing createIdentifier', () => {

View File

@ -20,6 +20,7 @@ import {
SourceRange, SourceRange,
sketchFromKclValue, sketchFromKclValue,
isPathToNodeNumber, isPathToNodeNumber,
formatNumber,
} from './wasm' } from './wasm'
import { import {
isNodeSafeToReplacePath, isNodeSafeToReplacePath,
@ -743,11 +744,26 @@ export function splitPathAtPipeExpression(pathToNode: PathToNode): {
return splitPathAtPipeExpression(pathToNode.slice(0, -1)) return splitPathAtPipeExpression(pathToNode.slice(0, -1))
} }
/**
* Note: This depends on WASM, but it's not async. Callers are responsible for
* awaiting init of the WASM module.
*/
export function createLiteral(value: LiteralValue | number): Node<Literal> { export function createLiteral(value: LiteralValue | number): Node<Literal> {
const raw = `${value}`
if (typeof value === 'number') { if (typeof value === 'number') {
value = { value, suffix: 'None' } value = { value, suffix: 'None' }
} }
let raw: string
if (typeof value === 'string') {
// TODO: Should we handle escape sequences?
raw = `${value}`
} else if (typeof value === 'boolean') {
raw = `${value}`
} else if (typeof value.value === 'number' && value.suffix === 'None') {
// Fast path for numbers when there are no units.
raw = `${value.value}`
} else {
raw = formatNumber(value.value, value.suffix)
}
return { return {
type: 'Literal', type: 'Literal',
start: 0, start: 0,

View File

@ -5,7 +5,11 @@ import {
PathToNode, PathToNode,
Identifier, Identifier,
topLevelRange, topLevelRange,
PipeExpression,
CallExpression,
VariableDeclarator,
} from './wasm' } from './wasm'
import { ProgramMemory } from 'lang/wasm'
import { import {
findAllPreviousVariables, findAllPreviousVariables,
isNodeSafeToReplace, isNodeSafeToReplace,
@ -25,9 +29,11 @@ import {
createCallExpression, createCallExpression,
createLiteral, createLiteral,
createPipeSubstitution, createPipeSubstitution,
createCallExpressionStdLib,
} from './modifyAst' } from './modifyAst'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { codeRefFromRange } from './std/artifactGraph' import { codeRefFromRange } from './std/artifactGraph'
import { addCallExpressionsToPipe, addCloseToPipe } from 'lang/std/sketch'
beforeAll(async () => { beforeAll(async () => {
await initPromise await initPromise
@ -680,3 +686,115 @@ myNestedVar = [
expect(pathToNode).toEqual(pathToNode2) expect(pathToNode).toEqual(pathToNode2)
}) })
}) })
describe('Testing specific sketch getNodeFromPath workflow', () => {
it('should parse the code', () => {
const openSketch = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)`
const ast = assertParse(openSketch)
expect(ast.start).toEqual(0)
expect(ast.end).toEqual(227)
})
it('should find the location to add new lineTo', () => {
const openSketch = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)`
const ast = assertParse(openSketch)
const sketchSnippet = `startProfileAt([0.02, 0.22], %)`
const sketchRange = topLevelRange(
openSketch.indexOf(sketchSnippet),
openSketch.indexOf(sketchSnippet) + sketchSnippet.length
)
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
const modifiedAst = addCallExpressionsToPipe({
node: ast,
programMemory: ProgramMemory.empty(),
pathToNode: sketchPathToNode,
expressions: [
createCallExpressionStdLib(
'lineTo', // We are forcing lineTo!
[
createArrayExpression([
createCallExpressionStdLib('profileStartX', [
createPipeSubstitution(),
]),
createCallExpressionStdLib('profileStartY', [
createPipeSubstitution(),
]),
]),
createPipeSubstitution(),
]
),
],
})
if (err(modifiedAst)) throw modifiedAst
const recasted = recast(modifiedAst)
const expectedCode = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
`
expect(recasted).toEqual(expectedCode)
})
it('it should find the location to add close', () => {
const openSketch = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
`
const ast = assertParse(openSketch)
const sketchSnippet = `startProfileAt([0.02, 0.22], %)`
const sketchRange = topLevelRange(
openSketch.indexOf(sketchSnippet),
openSketch.indexOf(sketchSnippet) + sketchSnippet.length
)
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
const modifiedAst = addCloseToPipe({
node: ast,
programMemory: ProgramMemory.empty(),
pathToNode: sketchPathToNode,
})
if (err(modifiedAst)) throw modifiedAst
const recasted = recast(modifiedAst)
const expectedCode = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0.02, 0.22], %)
|> xLine(0.39, %)
|> line([0.02, -0.17], %)
|> yLine(-0.15, %)
|> line([-0.21, -0.02], %)
|> xLine(-0.15, %)
|> line([-0.02, 0.21], %)
|> line([-0.08, 0.05], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
expect(recasted).toEqual(expectedCode)
})
})

View File

@ -21,6 +21,7 @@ import {
topLevelRange, topLevelRange,
VariableDeclaration, VariableDeclaration,
VariableDeclarator, VariableDeclarator,
recast,
} from './wasm' } from './wasm'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst' import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
@ -68,7 +69,28 @@ export function getNodeFromPath<T>(
deepPath: successfulPaths, deepPath: successfulPaths,
} }
} }
return new Error('not an object') const stackTraceError = new Error()
const sourceCode = recast(node)
const levels = stackTraceError.stack?.split('\n')
const aFewFunctionNames: string[] = []
let tree = ''
levels?.forEach((val, index) => {
const fnName = val.trim().split(' ')[1]
const ending = index === levels.length - 1 ? ' ' : ' > '
tree += fnName + ending
if (index < 3) {
aFewFunctionNames.push(fnName)
}
})
const error = new Error(
`Failed to stopAt ${stopAt}, ${aFewFunctionNames
.filter((a) => a)
.join(' > ')}`
)
console.error(tree)
console.error(sourceCode)
console.error(error.stack)
return error
} }
currentNode = currentNode?.[pathItem[0]] currentNode = currentNode?.[pathItem[0]]
successfulPaths.push(pathItem) successfulPaths.push(pathItem)

View File

@ -1999,7 +1999,7 @@ export class EngineCommandManager extends EventTarget {
.catch((e) => { .catch((e) => {
// TODO: Previously was never caught, we are not rejecting these pendingCommands but this needs to be handled at some point. // TODO: Previously was never caught, we are not rejecting these pendingCommands but this needs to be handled at some point.
/*noop*/ /*noop*/
return null return e
}) })
} }
/** /**

View File

@ -31,6 +31,9 @@ class FileSystemManager {
} }
async join(dir: string, path: string): Promise<string> { async join(dir: string, path: string): Promise<string> {
if (path.startsWith(dir)) {
path = path.slice(dir.length)
}
return Promise.resolve(window.electron.path.join(dir, path)) return Promise.resolve(window.electron.path.join(dir, path))
} }

View File

@ -6,6 +6,8 @@ import {
ArrayExpression, ArrayExpression,
BinaryExpression, BinaryExpression,
ArtifactGraph, ArtifactGraph,
LiteralValue,
NumericSuffix,
} from './wasm' } from './wasm'
import { filterArtifacts } from 'lang/std/artifactGraph' import { filterArtifacts } from 'lang/std/artifactGraph'
import { isOverlap } from 'lib/utils' import { isOverlap } from 'lib/utils'
@ -69,3 +71,15 @@ export function isLiteral(e: any): e is Literal {
export function isBinaryExpression(e: any): e is BinaryExpression { export function isBinaryExpression(e: any): e is BinaryExpression {
return e && e.type === 'BinaryExpression' return e && e.type === 'BinaryExpression'
} }
export function isLiteralValueNumber(
e: LiteralValue
): e is { value: number; suffix: NumericSuffix } {
return (
typeof e === 'object' &&
'value' in e &&
typeof e.value === 'number' &&
'suffix' in e &&
typeof e.suffix === 'string'
)
}

View File

@ -1,5 +1,5 @@
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { initPromise, parse, ParseResult } from './wasm' import { formatNumber, initPromise, parse, ParseResult } from './wasm'
import { enginelessExecutor } from 'lib/testHelpers' import { enginelessExecutor } from 'lib/testHelpers'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { Program } from '../wasm-lib/kcl/bindings/Program' import { Program } from '../wasm-lib/kcl/bindings/Program'
@ -20,3 +20,12 @@ it('can execute parsed AST', async () => {
expect(err(execState)).toEqual(false) expect(err(execState)).toEqual(false)
expect(execState.memory.get('x')?.value).toEqual(1) expect(execState.memory.get('x')?.value).toEqual(1)
}) })
it('formats numbers with units', () => {
expect(formatNumber(1, 'None')).toEqual('1')
expect(formatNumber(1, 'Count')).toEqual('1_')
expect(formatNumber(1, 'Mm')).toEqual('1mm')
expect(formatNumber(1, 'Inch')).toEqual('1in')
expect(formatNumber(0.5, 'Mm')).toEqual('0.5mm')
expect(formatNumber(-0.5, 'Mm')).toEqual('-0.5mm')
})

View File

@ -2,6 +2,7 @@ import {
init, init,
parse_wasm, parse_wasm,
recast_wasm, recast_wasm,
format_number,
execute, execute,
kcl_lint, kcl_lint,
modify_ast_for_sketch_wasm, modify_ast_for_sketch_wasm,
@ -17,6 +18,7 @@ import {
default_project_settings, default_project_settings,
base64_decode, base64_decode,
clear_scene_and_bust_cache, clear_scene_and_bust_cache,
change_kcl_settings,
reloadModule, reloadModule,
} from 'lib/wasm_lib_wrapper' } from 'lib/wasm_lib_wrapper'
@ -54,6 +56,8 @@ import { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifact' import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifact'
import { Artifact } from './std/artifactGraph' import { Artifact } from './std/artifactGraph'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix'
import { MetaSettings } from 'wasm-lib/kcl/bindings/MetaSettings'
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact' export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact' export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
@ -90,6 +94,7 @@ export type { Literal } from '../wasm-lib/kcl/bindings/Literal'
export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue' export type { LiteralValue } from '../wasm-lib/kcl/bindings/LiteralValue'
export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression' export type { ArrayExpression } from '../wasm-lib/kcl/bindings/ArrayExpression'
export type { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange' export type { SourceRange } from 'wasm-lib/kcl/bindings/SourceRange'
export type { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix'
export type SyntaxType = export type SyntaxType =
| 'Program' | 'Program'
@ -566,9 +571,19 @@ export function sketchFromKclValue(
return result return result
} }
/**
* Execute a KCL program.
* @param node The AST of the program to execute.
* @param path The full path of the file being executed. Use `null` for
* expressions that don't have a file, like expressions in the command bar.
* @param programMemoryOverride If this is not `null`, this will be used as the
* initial program memory, and the execution will be engineless (AKA mock
* execution).
*/
export const executor = async ( export const executor = async (
node: Node<Program>, node: Node<Program>,
engineCommandManager: EngineCommandManager, engineCommandManager: EngineCommandManager,
path?: string,
programMemoryOverride: ProgramMemory | Error | null = null programMemoryOverride: ProgramMemory | Error | null = null
): Promise<ExecState> => { ): Promise<ExecState> => {
if (programMemoryOverride !== null && err(programMemoryOverride)) if (programMemoryOverride !== null && err(programMemoryOverride))
@ -590,6 +605,7 @@ export const executor = async (
} }
const execOutcome: RustExecOutcome = await execute( const execOutcome: RustExecOutcome = await execute(
JSON.stringify(node), JSON.stringify(node),
path,
JSON.stringify(programMemoryOverride?.toRaw() || null), JSON.stringify(programMemoryOverride?.toRaw() || null),
JSON.stringify({ settings: jsAppSettings }), JSON.stringify({ settings: jsAppSettings }),
engineCommandManager, engineCommandManager,
@ -627,6 +643,13 @@ export const recast = (ast: Program): string | Error => {
return recast_wasm(JSON.stringify(ast)) return recast_wasm(JSON.stringify(ast))
} }
/**
* Format a number with suffix as KCL.
*/
export function formatNumber(value: number, suffix: NumericSuffix): string {
return format_number(value, JSON.stringify(suffix))
}
export const makeDefaultPlanes = async ( export const makeDefaultPlanes = async (
engineCommandManager: EngineCommandManager engineCommandManager: EngineCommandManager
): Promise<DefaultPlanes> => { ): Promise<DefaultPlanes> => {
@ -823,3 +846,17 @@ export function base64Decode(base64: string): ArrayBuffer | Error {
return new Error('Caught error decoding base64 string: ' + e) return new Error('Caught error decoding base64 string: ' + e)
} }
} }
/// Change the meta settings for the kcl file.
/// Returns the new kcl string with the updated settings.
export function changeKclSettings(
kcl: string,
settings: MetaSettings
): string | Error {
try {
return change_kcl_settings(kcl, JSON.stringify(settings))
} catch (e) {
console.error('Caught error changing kcl settings: ' + e)
return new Error('Caught error changing kcl settings: ' + e)
}
}

View File

@ -1,17 +1,14 @@
import { StateMachineCommandSetConfig } from 'lib/commandTypes' import { Command } from 'lib/commandTypes'
import { authMachine } from 'machines/authMachine' import { authActor } from 'machines/appMachine'
import { ACTOR_IDS } from 'machines/machineConstants'
type AuthCommandSchema = {} export const authCommands: Command[] = [
{
export const authCommandBarConfig: StateMachineCommandSetConfig< groupId: ACTOR_IDS.AUTH,
typeof authMachine, name: 'log-out',
AuthCommandSchema displayName: 'Log out',
> = {
'Log in': {
hide: 'both',
},
'Log out': {
args: [],
icon: 'arrowLeft', icon: 'arrowLeft',
needsReview: false,
onSubmit: () => authActor.send({ type: 'Log out' }),
}, },
} ]

View File

@ -41,12 +41,11 @@ export const projectsCommandBarConfig: StateMachineCommandSetConfig<
name: { name: {
inputType: 'options', inputType: 'options',
required: true, required: true,
options: [], options: (_, context) =>
optionsFromContext: (context) => context?.projects.map((p) => ({
context.projects.map((p) => ({ name: p.name,
name: p.name!, value: p.name,
value: p.name!, })) || [],
})),
}, },
}, },
}, },

View File

@ -0,0 +1,19 @@
import { parseEngineErrorMessage } from './validators'
describe('parseEngineErrorMessage', () => {
it('takes an engine error string and parses its json message', () => {
const engineError =
'engine error: [{"error_code":"internal_engine","message":"Trajectory curve must be G1 continuous (with continuous tangents)"}]'
const message = parseEngineErrorMessage(engineError)
expect(message).toEqual(
'Trajectory curve must be G1 continuous (with continuous tangents)'
)
})
it('retuns undefined on strings with different formats', () => {
const s1 = 'engine error: []'
const s2 = 'blabla'
expect(parseEngineErrorMessage(s1)).toBeUndefined()
expect(parseEngineErrorMessage(s2)).toBeUndefined()
})
})

View File

@ -3,6 +3,7 @@ import { engineCommandManager } from 'lib/singletons'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { CommandBarContext } from 'machines/commandBarMachine' import { CommandBarContext } from 'machines/commandBarMachine'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { ApiError_type } from '@kittycad/lib/dist/types/src/models'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => { export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
for (let tries = 0; tries < numberOfRetries; tries++) { for (let tries = 0; tries < numberOfRetries; tries++) {
@ -46,6 +47,20 @@ function isSelections(selections: unknown): selections is Selections {
) )
} }
export function parseEngineErrorMessage(engineError: string) {
const parts = engineError.split('engine error: ')
if (parts.length < 2) {
return undefined
}
const errors = JSON.parse(parts[1]) as ApiError_type[]
if (!errors[0]) {
return undefined
}
return errors[0].message
}
export const revolveAxisValidator = async ({ export const revolveAxisValidator = async ({
data, data,
context, context,
@ -83,7 +98,7 @@ export const revolveAxisValidator = async ({
value: 360, value: 360,
} }
const revolveAboutEdgeCommand = async () => { const command = async () => {
return await engineCommandManager.sendSceneCommand({ return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),
@ -96,13 +111,13 @@ export const revolveAxisValidator = async ({
}, },
}) })
} }
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand) const result = await dryRunWrapper(command)
if (attemptRevolve?.success) { if (result?.success) {
return true return true
} else {
// return error message for the toast
return 'Unable to revolve with selected edge'
} }
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to revolve with the current selection. Reason: ${reason}`
} }
export const loftValidator = async ({ export const loftValidator = async ({
@ -128,7 +143,7 @@ export const loftValidator = async ({
return 'Unable to loft, selection contains less than two solid2ds' return 'Unable to loft, selection contains less than two solid2ds'
} }
const loftCommand = async () => { const command = async () => {
// TODO: check what to do with these // TODO: check what to do with these
const DEFAULT_V_DEGREE = 2 const DEFAULT_V_DEGREE = 2
const DEFAULT_TOLERANCE = 2 const DEFAULT_TOLERANCE = 2
@ -145,13 +160,13 @@ export const loftValidator = async ({
}, },
}) })
} }
const attempt = await dryRunWrapper(loftCommand) const result = await dryRunWrapper(command)
if (attempt?.success) { if (result?.success) {
return true return true
} else {
// return error message for the toast
return 'Unable to loft with selected sketches'
} }
const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to loft with the current selection. Reason: ${reason}`
} }
export const shellValidator = async ({ export const shellValidator = async ({
@ -180,7 +195,7 @@ export const shellValidator = async ({
return "Unable to shell, couldn't find the solid" return "Unable to shell, couldn't find the solid"
} }
const shellCommand = async () => { const command = async () => {
// TODO: figure out something better than an arbitrarily small value // TODO: figure out something better than an arbitrarily small value
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9 const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
const DEFAULT_HOLLOW = false const DEFAULT_HOLLOW = false
@ -200,12 +215,13 @@ export const shellValidator = async ({
}) })
} }
const attemptShell = await dryRunWrapper(shellCommand) const result = await dryRunWrapper(command)
if (attemptShell?.success) { if (result?.success) {
return true return true
} }
return 'Unable to shell with the provided selection' const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to shell with the current selection. Reason: ${reason}`
} }
export const sweepValidator = async ({ export const sweepValidator = async ({
@ -241,7 +257,7 @@ export const sweepValidator = async ({
} }
const target = targetArtifact.pathId const target = targetArtifact.pathId
const sweepCommand = async () => { const command = async () => {
// TODO: second look on defaults here // TODO: second look on defaults here
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7 const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
const DEFAULT_SECTIONAL = false const DEFAULT_SECTIONAL = false
@ -261,10 +277,11 @@ export const sweepValidator = async ({
}) })
} }
const attemptSweep = await dryRunWrapper(sweepCommand) const result = await dryRunWrapper(command)
if (attemptSweep?.success) { if (result?.success) {
return true return true
} }
return 'Unable to sweep with the provided selection' const reason = parseEngineErrorMessage(result) || 'unknown'
return `Unable to sweep with the current selection. Reason: ${reason}`
} }

View File

@ -1,13 +1,10 @@
import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning' import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarning'
import { Command, CommandArgumentOption } from './commandTypes' import { Command, CommandArgumentOption } from './commandTypes'
import { codeManager, kclManager } from './singletons' import { kclManager } from './singletons'
import { isDesktop } from './isDesktop' import { isDesktop } from './isDesktop'
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants' import { FILE_EXT } from './constants'
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models' import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { parseProjectSettings } from 'lang/wasm' import { reportRejection } from './trap'
import { err, reportRejection } from './trap'
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
import { copyFileShareLink } from './links'
import { IndexLoaderData } from './types' import { IndexLoaderData } from './types'
interface OnSubmitProps { interface OnSubmitProps {
@ -68,56 +65,23 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
const sampleCodeUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent( const sampleCodeUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
projectPathPart projectPathPart
)}/${encodeURIComponent(primaryKclFile)}` )}/${encodeURIComponent(primaryKclFile)}`
const sampleSettingsFileUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
projectPathPart
)}/${PROJECT_SETTINGS_FILE_NAME}`
Promise.allSettled([fetch(sampleCodeUrl), fetch(sampleSettingsFileUrl)]) fetch(sampleCodeUrl)
.then((results) => { .then(async (codeResponse): Promise<OnSubmitProps> => {
const a = if (!codeResponse.ok) {
'value' in results[0] ? results[0].value : results[0].reason console.error(
const b = 'Failed to fetch sample code:',
'value' in results[1] ? results[1].value : results[1].reason codeResponse.statusText
return [a, b] )
}) return Promise.reject(new Error('Failed to fetch sample code'))
.then(
async ([
codeResponse,
settingsResponse,
]): Promise<OnSubmitProps> => {
if (!codeResponse.ok) {
console.error(
'Failed to fetch sample code:',
codeResponse.statusText
)
return Promise.reject(new Error('Failed to fetch sample code'))
}
const code = await codeResponse.text()
// It's possible that a sample doesn't have a project.toml
// associated with it.
let projectSettingsPayload: ReturnType<
typeof projectConfigurationToSettingsPayload
> = {}
if (settingsResponse.ok) {
const parsedProjectSettings = parseProjectSettings(
await settingsResponse.text()
)
if (!err(parsedProjectSettings)) {
projectSettingsPayload =
projectConfigurationToSettingsPayload(parsedProjectSettings)
}
}
return {
sampleName: data.sample.split('/')[0] + FILE_EXT,
code,
method: data.method,
sampleUnits:
projectSettingsPayload.modeling?.defaultUnit || 'mm',
}
} }
) const code = await codeResponse.text()
return {
sampleName: data.sample.split('/')[0] + FILE_EXT,
code,
method: data.method,
}
})
.then((props) => { .then((props) => {
if (props?.code) { if (props?.code) {
commandProps.specialPropsForSampleCommand commandProps.specialPropsForSampleCommand

View File

@ -0,0 +1,92 @@
import { expect } from 'vitest'
import {
recast,
assertParse,
topLevelRange,
VariableDeclaration,
initPromise,
} from 'lang/wasm'
import { updateCenterRectangleSketch } from './rectangleTool'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { getNodeFromPath } from 'lang/queryAst'
import { findUniqueName } from 'lang/modifyAst'
import { err, trap } from './trap'
beforeAll(async () => {
await initPromise
})
describe('library rectangleTool helper functions', () => {
describe('updateCenterRectangleSketch', () => {
// regression test for https://github.com/KittyCAD/modeling-app/issues/5157
test('should update AST and source code', async () => {
// Base source code that will be edited in place
const sourceCode = `sketch001 = startSketchOn('XZ')
|> startProfileAt([120.37, 162.76], %)
|> angledLine([0, 0], %, $rectangleSegmentA001)
|> angledLine([segAng(rectangleSegmentA001) + 90, 0], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
// Create ast
const _ast = assertParse(sourceCode)
let ast = structuredClone(_ast)
// Find some nodes and paths to reference
const sketchSnippet = `startProfileAt([120.37, 162.76], %)`
const sketchRange = topLevelRange(
sourceCode.indexOf(sketchSnippet),
sourceCode.indexOf(sketchSnippet) + sketchSnippet.length
)
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
const _node = getNodeFromPath<VariableDeclaration>(
ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node)) return
const sketchInit = _node.node?.declaration.init
// Hard code inputs that a user would have taken with their mouse
const x = 40
const y = 60
const rectangleOrigin = [120, 180]
const tags: [string, string, string] = [
'rectangleSegmentA001',
'rectangleSegmentB001',
'rectangleSegmentC001',
]
// Update the ast
if (sketchInit.type === 'PipeExpression') {
updateCenterRectangleSketch(
sketchInit,
x,
y,
tags[0],
rectangleOrigin[0],
rectangleOrigin[1]
)
}
// ast is edited in place from the updateCenterRectangleSketch
const expectedSourceCode = `sketch001 = startSketchOn('XZ')
|> startProfileAt([80, 120], %)
|> angledLine([0, 80], %, $rectangleSegmentA001)
|> angledLine([segAng(rectangleSegmentA001) + 90, 120], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
`
const recasted = recast(ast)
expect(recasted).toEqual(expectedSourceCode)
})
})
})

View File

@ -8,13 +8,19 @@ import {
createTagDeclarator, createTagDeclarator,
createUnaryExpression, createUnaryExpression,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { ArrayExpression, CallExpression, PipeExpression } from 'lang/wasm' import {
ArrayExpression,
CallExpression,
PipeExpression,
recast,
} from 'lang/wasm'
import { roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
import { import {
isCallExpression, isCallExpression,
isArrayExpression, isArrayExpression,
isLiteral, isLiteral,
isBinaryExpression, isBinaryExpression,
isLiteralValueNumber,
} from 'lang/util' } from 'lang/util'
/** /**
@ -140,10 +146,12 @@ export function updateCenterRectangleSketch(
if (isArrayExpression(arrayExpression)) { if (isArrayExpression(arrayExpression)) {
const literal = arrayExpression.elements[0] const literal = arrayExpression.elements[0]
if (isLiteral(literal)) { if (isLiteral(literal)) {
callExpression.arguments[0] = createArrayExpression([ if (isLiteralValueNumber(literal.value)) {
createLiteral(literal.value), callExpression.arguments[0] = createArrayExpression([
createLiteral(Math.abs(twoX)), createLiteral(literal.value),
]) createLiteral(Math.abs(twoX)),
])
}
} }
} }
} }

View File

@ -80,7 +80,8 @@ class MockEngineCommandManager {
export async function enginelessExecutor( export async function enginelessExecutor(
ast: Node<Program>, ast: Node<Program>,
pmo: ProgramMemory | Error = ProgramMemory.empty() pmo: ProgramMemory | Error = ProgramMemory.empty(),
path?: string
): Promise<ExecState> { ): Promise<ExecState> {
if (pmo !== null && err(pmo)) return Promise.reject(pmo) if (pmo !== null && err(pmo)) return Promise.reject(pmo)
@ -90,7 +91,7 @@ export async function enginelessExecutor(
}) as any as EngineCommandManager }) as any as EngineCommandManager
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
mockEngineCommandManager.startNewSession() mockEngineCommandManager.startNewSession()
const execState = await executor(ast, mockEngineCommandManager, pmo) const execState = await executor(ast, mockEngineCommandManager, path, pmo)
await mockEngineCommandManager.waitForAllCommands() await mockEngineCommandManager.waitForAllCommands()
return execState return execState
} }

View File

@ -10,6 +10,7 @@
import { import {
parse_wasm as ParseWasm, parse_wasm as ParseWasm,
recast_wasm as RecastWasm, recast_wasm as RecastWasm,
format_number as FormatNumber,
execute as Execute, execute as Execute,
kcl_lint as KclLint, kcl_lint as KclLint,
modify_ast_for_sketch_wasm as ModifyAstForSketch, modify_ast_for_sketch_wasm as ModifyAstForSketch,
@ -25,6 +26,7 @@ import {
default_project_settings as DefaultProjectSettings, default_project_settings as DefaultProjectSettings,
base64_decode as Base64Decode, base64_decode as Base64Decode,
clear_scene_and_bust_cache as ClearSceneAndBustCache, clear_scene_and_bust_cache as ClearSceneAndBustCache,
change_kcl_settings as ChangeKclSettings,
} from '../wasm-lib/pkg/wasm_lib' } from '../wasm-lib/pkg/wasm_lib'
type ModuleType = typeof import('../wasm-lib/pkg/wasm_lib') type ModuleType = typeof import('../wasm-lib/pkg/wasm_lib')
@ -51,6 +53,9 @@ export const parse_wasm: typeof ParseWasm = (...args) => {
export const recast_wasm: typeof RecastWasm = (...args) => { export const recast_wasm: typeof RecastWasm = (...args) => {
return getModule().recast_wasm(...args) return getModule().recast_wasm(...args)
} }
export const format_number: typeof FormatNumber = (...args) => {
return getModule().format_number(...args)
}
export const execute: typeof Execute = (...args) => { export const execute: typeof Execute = (...args) => {
return getModule().execute(...args) return getModule().execute(...args)
} }
@ -106,3 +111,6 @@ export const clear_scene_and_bust_cache: typeof ClearSceneAndBustCache = (
) => { ) => {
return getModule().clear_scene_and_bust_cache(...args) return getModule().clear_scene_and_bust_cache(...args)
} }
export const change_kcl_settings: typeof ChangeKclSettings = (...args) => {
return getModule().change_kcl_settings(...args)
}

View File

@ -0,0 +1,30 @@
import { ActorRefFrom, createActor, setup } from 'xstate'
import { authMachine } from './authMachine'
import { useSelector } from '@xstate/react'
import { ACTOR_IDS } from './machineConstants'
const appMachine = setup({
actors: {
[ACTOR_IDS.AUTH]: authMachine,
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5gF8A0IB2B7CdGgAoBbAQwGMALASwzAEp8QAHLWKgFyqw0YA9EAjACZ0AT0FDkU5EA */
id: 'modeling-app',
invoke: [
{
src: ACTOR_IDS.AUTH,
systemId: ACTOR_IDS.AUTH,
},
],
})
export const appActor = createActor(appMachine).start()
export const authActor = appActor.system.get(ACTOR_IDS.AUTH) as ActorRefFrom<
typeof authMachine
>
export const useAuthState = () => useSelector(authActor, (state) => state)
export const useToken = () =>
useSelector(authActor, (state) => state.context.token)
export const useUser = () =>
useSelector(authActor, (state) => state.context.user)

View File

@ -15,6 +15,8 @@ import {
} from 'lib/desktop' } from 'lib/desktop'
import { COOKIE_NAME } from 'lib/constants' import { COOKIE_NAME } from 'lib/constants'
import { markOnce } from 'lib/performance' import { markOnce } from 'lib/performance'
import { ACTOR_IDS } from './machineConstants'
import withBaseUrl from '../lib/withBaseURL'
const SKIP_AUTH = VITE_KC_SKIP_AUTH === 'true' && DEV const SKIP_AUTH = VITE_KC_SKIP_AUTH === 'true' && DEV
@ -50,7 +52,7 @@ export type Events =
} }
export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY' export const TOKEN_PERSIST_KEY = 'TOKEN_PERSIST_KEY'
const persistedToken = export const persistedToken =
VITE_KC_DEV_TOKEN || VITE_KC_DEV_TOKEN ||
getCookie(COOKIE_NAME) || getCookie(COOKIE_NAME) ||
localStorage?.getItem(TOKEN_PERSIST_KEY) || localStorage?.getItem(TOKEN_PERSIST_KEY) ||
@ -69,18 +71,17 @@ export const authMachine = setup({
} }
} }
}, },
actions: {
goToIndexPage: () => {},
goToSignInPage: () => {},
},
actors: { actors: {
getUser: fromPromise(({ input }: { input: { token?: string } }) => getUser: fromPromise(({ input }: { input: { token?: string } }) =>
getUser(input) getUser(input)
), ),
logout: fromPromise(async () =>
isDesktop() ? writeTokenFile('') : logout()
),
}, },
}).createMachine({ }).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwAWQ9gBspuQCYAnAGYAHPYCsx+4ccAaEAE9E1q7YcoZyxrYR1m7mcrYAvnE+aFh4BMTk1LSQjExgAE55VHnYKmIAhuhkRQC2qcLikpDSDPJKSCBqGlo67QYI9gDs5tge5o6h5vau7oY+-v3mA9jWco4u5iu21ua2YcYJSRg4Eln0zJkABFQYrbqdmtoMun2GA7YjxuPmLqvGNh5zRCfJaOcyLUzuAYuFyGcwHEDJY6NCAAeQwTEuskUd3UDx6oD6Im2wUcAzkMJ2cjBxlMgIWLmwZLWljecjJTjh8IYVAgcF0iJxXUez0QIgGxhJZIpu2ptL8AWwtje1nCW2iq1shns8MRdXSlGRjEFeKevUQjkcy3sqwGHimbg83nlCF22GMytVUWMMUc8USCKO2BOdCN7Xu3VNBKMKsVFp2hm2vu+1id83slkVrgTxhcW0pNJ1geDkDR6GNEZFCAT1kZZLk9cMLltb0WdPMjewjjC1mzOZCtk5CSAA */ /** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOhzEwGsBJAMwBkB7KGCa-AYgkcJIIDdGlMGWwVKAWgA2zVhIIBtAAwBdRKAAOjWLgAuuHupAAPRAGYArAEYSADgu2AnGYBMLpVYBsZz7YA0IACeiG6OJM62tmZKLgDsno5KtvEAvikBaFh4hKTkVHRMLJDsHGAATmWMZSQaUui6tFWoouLSspDy+MpqSCBaOvqGvaYIljb2Tq7uXj7+QYgALFYW4clWy1ZmVgsWsZtpGRg4BMQkMkVsnIUABIwArrrdRv16BvhGI74LJBYW7o5WKJmKILObBUZeEgJP4LTxKMwIhZmBYLA4gTLHHJnWQEKAAeQeXB4IgEQhEGOyp3OUFxBN0CFJmHqb26T16L0G72GiCsSg8PyszkBCViTiUjgC4Jcnhc4SUsQcvgsoL2VjRFJOpGptMJ5Uq1Vq9UaZWaGqx2vw+IeDPwgiZnNZqme2leQ1An1s31+-0BCJBYJCLm+lk8CRl9hRyos6qOlK17QgdI4N0UTvZLs5Hx58NsJARuys0tDSl+AYQthsgNi0TMqt2LjVaPwjAgcCMZuIzoGbyzCAknkliH7Maympa+QYCfYXddXPdixcg4QvKUdk2u2iLkcsXhCRHmKpU7nfQzPe5CAsMpIXi8MvFKM8VliS5c1jzj53W3isNFqPS6NjMcLStXQZ0zc8ohsJI-kcFxXEcR9HAWF9gTzDxbCUXxAQWEsdn3ONsQuOkwLPedl22MIzFg3YP1gl9PG+bYvGsSxlUcRJozSFIgA */
id: 'Auth', id: ACTOR_IDS.AUTH,
initial: 'checkIfLoggedIn', initial: 'checkIfLoggedIn',
context: { context: {
token: persistedToken, token: persistedToken,
@ -112,19 +113,30 @@ export const authMachine = setup({
}, },
}, },
loggedIn: { loggedIn: {
entry: ['goToIndexPage'],
on: { on: {
'Log out': { 'Log out': {
target: 'loggedOut', target: 'loggingOut',
actions: () => { },
// eslint-disable-next-line @typescript-eslint/no-floating-promises },
if (isDesktop()) writeTokenFile('') },
}, loggingOut: {
invoke: {
src: 'logout',
onDone: 'loggedOut',
onError: {
target: 'loggedIn',
actions: [
({ event }) => {
console.error(
'Error while logging out',
'error' in event ? `: ${event.error}` : ''
)
},
],
}, },
}, },
}, },
loggedOut: { loggedOut: {
entry: ['goToSignInPage'],
on: { on: {
'Log in': { 'Log in': {
target: 'checkIfLoggedIn', target: 'checkIfLoggedIn',
@ -235,3 +247,12 @@ async function getAndSyncStoredToken(input: {
localStorage.setItem(TOKEN_PERSIST_KEY, fileToken) localStorage.setItem(TOKEN_PERSIST_KEY, fileToken)
return fileToken return fileToken
} }
async function logout() {
localStorage.removeItem(TOKEN_PERSIST_KEY)
if (isDesktop()) return Promise.resolve(null)
return fetch(withBaseUrl('/logout'), {
method: 'POST',
credentials: 'include',
})
}

View File

@ -10,6 +10,7 @@ import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
import { MachineManager } from 'components/MachineManagerProvider' import { MachineManager } from 'components/MachineManagerProvider'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { useSelector } from '@xstate/react' import { useSelector } from '@xstate/react'
import { authCommands } from 'lib/commandBarConfigs/authCommandConfig'
export type CommandBarContext = { export type CommandBarContext = {
commands: Command[] commands: Command[]
@ -80,6 +81,7 @@ export type CommandBarMachineEvent =
export const commandBarMachine = setup({ export const commandBarMachine = setup({
types: { types: {
context: {} as CommandBarContext, context: {} as CommandBarContext,
input: {} as { commands: Command[] },
events: {} as CommandBarMachineEvent, events: {} as CommandBarMachineEvent,
}, },
actions: { actions: {
@ -409,8 +411,8 @@ export const commandBarMachine = setup({
}, },
}).createMachine({ }).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAIwB2AHTiAHAE45AZjkAmdcoaSArCoA0IAJ6JxDOdJ2SF4gCySHaqZIa2Avm8NpMuAsXJUYKSMLEggHLA8fAJhIgiiOgBssokKTpJqiXK2OsqJaoYm8eJqytKJ9hqq+eJW2h5eGNh4RKRkAGKcLb74tJRgAMbc+ANNviGCEVH8gnEKubK5ympVrrlyhabm0rZ5CtYM4hVq83ININ7NfqTSVDSQZADybGA4E2FTvDOxiGoMDNJMjo9H9lLYGDpKpsEModOJpA5XOIwakwcidOdLj1-LdqLQIGQAIIQAijHx4WDvdhcL4xUBxURqSTw3LzfYKZSSOQZWzQ5S1crMuTAiElRKuTFjFo4u74sgAJTA6FQADcwCMpRBKcxJjTorMxEzkhouftuXCGGpIdCpADmZJxfzJLY5Cp5pLydcSLj7gSqeE9d96Yh+TppKoXfkGKdlHloUyFIDUvMXBzbFl3J4LprWt6AMpgfpDLpQDWesgFovDMlXf2ffU-BBZZKuczWWylRRwvlM6Q2LIMZQ2QdHZQeq65245vqDbgPOuBunCEP88MqPQchwVNLKaHptTSYUuRI6f4npyJcfYm5Ylozobz8ShamRWkGhBmeTSQeJX+JXQ6H88jQnCMiugoELCpypQKJeWa3l6U6er0hazvOajPgGr4NsGH6WhYsHOkycglLCEJ7iclgaIosKSLGGgKFe0o3AA4lg3AABZgCQJb4KQUAAK7oK83CwBQHG4DAIwCSQJAiXxJCCcJODcAu2FBsuH55GGJ7irYHYqHpGzGKYWiWB2nK2Kcv6wWO8E5jibGcdxvH8UJIliQAInAqFDGWtY6h8i7vlkZREbYZg6JuwEmQgfxhnkHbiHYJ7yHYTGIU5XE8TgpZucponSISADuWBRLl+BdGwAncBWAkAEboDwClKSJanTEucS1Bk36DicDCpOYLoKHu-x9tGuhgoozKpBlk5ZS5FX5R50gAGpYJQnAQOxJZkBA-BgNIXQqqgADWh0qhtW3sWAeYlv0hKKe5KntW+YRFGi5RyOKDrZEaUW8pppTguGuwDRFEO-nNjnsdlrlPQVsBrVd228LlZDcSQqDemwlDsQAZtj6DSJdm2o7d91gI9rUvYFL4dYIH0RV9P0Zv9CiA3EwMAmCWgVHYKVwY0yE4oqKqcGAxV1Y1zU1uMdNYQzjbMpYcgQlFxFq66u6xXRALbkyg7-Bz0bQzcYsS1LxIEMttOYfWGldYcCWwQmNiRrU0Jq2GRxJCbHZqC6ahm96FuSwqSqquqtuqQrDudVs4iJhUyZtn9qjQokYKHjk6hSLsZhciH0hh1LACiEDNTHr04ZpJT6bIEXxcKp50YDRT-mGrrJbUgFDp3xfIFxAynbx1PPaJe0HUdOAnedJMozd4+IzXjuIJCLIQqUWjJS4MVFBByS7BzyJq38WTB-ZIs3sPo8VcvHlTzgh3HWdF2L3OD8qZST66upCdxUTLBJOUUHB6GFPpSQXt1CWEGpaOiv4EyD1vmPBGj9MbY2kLjAmRMF5kyXmg7+q8AFJwbjkXQEVsj5FyO3RAiQjjfi5CReYPck7iA8FmHAqAIBwEEAhXMf8la4VEMsWwgJuTTXNGYK0tC4rwkDvsAaSQ-qlAhIPPEkBBFvWEVycoFQhywUHGaHWH04Q7FUNBdc+x2FXwnDiSss5eJyzwFo2ucQIplBWHrcEVhuoFFirCeEuxsj8mWEOFYmZhZ2JvNOXyc4ICuLXggaxCJwG70AiUHQfIzHBKHGYDMJFhTFwWjlPKhDRKJJIRFGQcIFBsiOIHJw8ZIQ7CUGrY+9g9AnmKbDRaZSaaFRKmVNGpYqo1Uqe+FKYZan1PyElbJYjrB6C5CUJQ9DL5ROvN6Ep8MBlI3WvgkZEzGzyAbnkZK-wjan38UzeEzZtBswdADYupdjm4XoTIOwuwHB5HyGkYyRRdBBLosCIE6ZvpnFsVs24KD77lPgEFf+kzxRH32EyFYlpOzQjAZYV2ehTkfI4W4IAA */ /** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAIwB2AHTiAHAE45AZjkAmdcoaSArCoA0IAJ6JxDOdJ2SF4gCySHaqZIa2Avm8NpMuAsXJUYKSMLEggHLA8fAJhIgiiOgBssokKTpJqiXK2OsqJaoYm8eJqytKJ9hqq+eJW2h5eGNh4RKRkAGKcLb74tJRgAMbc+ANNviGCEVH8gnEKubK5ympVrrlyhabm0rZ5CtYM4hVq83ININ7NfqTSVDSQZADybGA4E2FTvDOxiGoMDNJMjo9H9lLYGDpKpsEModOJpA5XOIwakwcidOdLj1-LdqLQIGQAIIQAijHx4WDvdhcL4xUBxURqSTw3LzfYKZSSOQZWzQ5S1crMuTAiElRKuTFjFo4u74sgAJTA6FQADcwCMpRBKcxJjTorMxEzkhouftuXCGGpIdCpADmZJxfzJLY5Cp5pLydcSLj7gSqeE9d96Yh+TppKoXfkGKdlHloUyFIDUvMXBzbFl3J4LprWt6AMpgfpDLpQDWesgFovDMlXf2ffU-BBZZKuczWWylRRwvlM6Q2LIMZQ2QdHZQeq65245vqDbgPOuBunCEP88MqPQchwVNLKaHptTSYUuRI6f4npyJcfYm5Ylozobz8ShamRWkGhBmeTSQeJX+JXQ6H88jQnCMiugoELCpypQKJeWa3l6U6er0hazvOajPgGr4NsGH6WhYsHOkycglLCEJ7iclgaIosKSLGGgKFe0o3AA4lg3AABZgCQJb4KQUAAK7oK83CwBQHG4DAIwCSQJAiXxJCCcJODcAu2FBsuH55GGJ7irYHYqHpGzGKYWiWB2nK2Kcv6wWO8E5jibGcdxvH8UJIliQAInAqFDGWtY6h8i7vlkZREbYZg6JuwEmQgfxhnkHbiHYJ7yHYTGIU5XE8TgpZucponSISADuWBRLl+BdGwAncBWAkAEboDwClKSJanTEucS1Bk36DicDCpOYLoKHu-x9tGuhgoozKpBlk5ZS5FX5R50gAGpYJQnAQOxJZkBA-BgNIXQqqgADWh0qhtW3sWAeYlv0hKKe5KntW+YRFGi5RyOKDrZEaUW8pppTguGuwDRFEO-nNjnsdlrlPQVsBrVd228LlZDcSQqDemwlDsQAZtj6DSJdm2o7d91gI9rUvYFL4dYIH0RV9P0Zv9CiA3EwMAmCWgVHYKVwY0yE4oqKqcGAxV1Y1zU1uMdNYQzjbMpYcgQlFxFq66u6xXRALbkyg7-Bz0bQzcYsS1LxIEMttOYfWGldYcCWwQmNiRrU0Jq2GRxJCbHZqC6ahm96FuSwqSqquqtuqQrDudVs4iJhUyZtn9qjQokYKHjk6hSLsZhciH0hh1LACiEDNTHr04ZpJT6bIEXxcKp50YDRT-mGrrJbUgFDp3xfIFxAynbx1PPaJe0HUdOAnedJMozd4+IzXjuIJCLIQqUWjJS4MVFBByS7BzyJq38WTB-ZIs3sPo8VcvHlTzgh3HWdF2L3OD8qZST66upCdxUTLBJOUUHB6GFPpSQXt1CWEGpaOiv4EyD1vmPBGj9MbY2kLjAmRMF5kyXmg7+q8AFJwbjkXQEVsj5FyO3RAiQjjfi5CReYPck7iA8FmHAqAIBwEEAhXMf8la4VEMsWwgJuTTXNGYK0tC4rwkDvsAaSQ-qlAhIPPEkBBFvWEVycoFQhywUHGaHWH04Q7FUNBdc+x2FXwnDiSss5eJyzwFo2ucQIplBWHrcEVhuoFFirCeEuxsj8mWEOFYmZhZ2JvNOXyc4ICuLXggaxCJwG70AiUHQfIzHBKHGYDMJFhTFwWjlPKhDRKJJIRFGQcIFBsiOIHJw8ZIQ7CUGrY+9g9AnmKbDRaZSaaFRKmVNGpYqo1Uqe+FKYZan1PyElbJYjrB6C5CUJQ9DL5ROvN6Ep8MBlI3WvgkZEzGzyAbnkZK-wjan38UzeEzZtBswdADYupdjm4XoTIOwuwHB5HyGkYyRRdBBLosCIE6ZvpnFsVs24KD77lPgEFf+kzxRH32EyFYlpOzQjAZYV2ehTkfI4W4IAA */
context: { context: ({ input }) => ({
commands: [], commands: input.commands || [],
selectedCommand: undefined, selectedCommand: undefined,
currentArgument: undefined, currentArgument: undefined,
selectionRanges: { selectionRanges: {
@ -425,7 +427,7 @@ export const commandBarMachine = setup({
setCurrentMachine: () => {}, setCurrentMachine: () => {},
noMachinesReason: () => undefined, noMachinesReason: () => undefined,
}, },
}, }),
id: 'Command Bar', id: 'Command Bar',
initial: 'Closed', initial: 'Closed',
states: { states: {
@ -631,7 +633,11 @@ function sortCommands(a: Command, b: Command) {
return a.name.localeCompare(b.name) return a.name.localeCompare(b.name)
} }
export const commandBarActor = createActor(commandBarMachine).start() export const commandBarActor = createActor(commandBarMachine, {
input: {
commands: [...authCommands],
},
}).start()
/** Basic state snapshot selector */ /** Basic state snapshot selector */
const cmdBarStateSelector = (state: SnapshotFrom<typeof commandBarActor>) => const cmdBarStateSelector = (state: SnapshotFrom<typeof commandBarActor>) =>

View File

@ -0,0 +1,3 @@
export const ACTOR_IDS = {
AUTH: 'auth',
}

View File

@ -583,6 +583,13 @@ export const modelingMachine = setup({
}, },
// end guards // end guards
actions: { actions: {
toastError: ({ event }) => {
if ('output' in event && event.output instanceof Error) {
toast.error(event.output.message)
} else if ('data' in event && event.data instanceof Error) {
toast.error(event.data.message)
}
},
'assign tool in context': assign({ 'assign tool in context': assign({
currentTool: ({ event }) => currentTool: ({ event }) =>
'data' in event && event.data && 'tool' in event.data 'data' in event && event.data && 'tool' in event.data
@ -637,56 +644,6 @@ export const modelingMachine = setup({
sketchDetails: event.output, sketchDetails: event.output,
} }
}), }),
'AST extrude': ({ context: { store }, event }) => {
if (event.type !== 'Extrude') return
;(async () => {
if (!event.data) return
const { selection, distance } = event.data
let ast = kclManager.ast
if (
'variableName' in distance &&
distance.variableName &&
distance.insertIndex !== undefined
) {
const newBody = [...ast.body]
newBody.splice(
distance.insertIndex,
0,
distance.variableDeclarationAst
)
ast.body = newBody
}
const pathToNode = getNodePathFromSourceRange(
ast,
selection.graphSelections[0]?.codeRef.range
)
const extrudeSketchRes = extrudeSketch(
ast,
pathToNode,
false,
'variableName' in distance
? distance.variableIdentifierAst
: distance.valueAst
)
if (trap(extrudeSketchRes)) return
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: [pathToExtrudeArg],
zoomToFit: true,
zoomOnRangeAndType: {
range: selection.graphSelections[0]?.codeRef.range,
type: 'path',
},
})
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections)
}
})().catch(reportRejection)
},
'AST revolve': ({ context: { store }, event }) => { 'AST revolve': ({ context: { store }, event }) => {
if (event.type !== 'Revolve') return if (event.type !== 'Revolve') return
;(async () => { ;(async () => {
@ -1491,6 +1448,63 @@ export const modelingMachine = setup({
return {} as SetSelections return {} as SetSelections
} }
), ),
extrudeAstMod: fromPromise<
void,
ModelingCommandSchema['Extrude'] | undefined
>(async ({ input }) => {
if (!input) return Promise.reject('No input provided')
const { selection, distance } = input
let ast = structuredClone(kclManager.ast)
let extrudeName: string | undefined = undefined
const pathToNode = getNodePathFromSourceRange(
ast,
selection.graphSelections[0]?.codeRef.range
)
// Add an extrude statement to the AST
const extrudeSketchRes = extrudeSketch(
ast,
pathToNode,
false,
'variableName' in distance
? distance.variableIdentifierAst
: distance.valueAst
)
if (err(extrudeSketchRes)) return Promise.reject(extrudeSketchRes)
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
// Insert the distance variable if the user has provided a variable name
if (
'variableName' in distance &&
distance.variableName &&
typeof pathToExtrudeArg[1][0] === 'number'
) {
const insertIndex = Math.min(
pathToExtrudeArg[1][0],
distance.insertIndex
)
const newBody = [...modifiedAst.body]
newBody.splice(insertIndex, 0, distance.variableDeclarationAst)
modifiedAst.body = newBody
// Since we inserted a new variable, we need to update the path to the extrude argument
pathToExtrudeArg[1][0]++
}
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
focusPath: [pathToExtrudeArg],
zoomToFit: true,
zoomOnRangeAndType: {
range: selection.graphSelections[0]?.codeRef.range,
type: 'path',
},
})
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections)
}
}),
offsetPlaneAstMod: fromPromise( offsetPlaneAstMod: fromPromise(
async ({ async ({
input, input,
@ -1821,9 +1835,8 @@ export const modelingMachine = setup({
], ],
Extrude: { Extrude: {
target: 'idle', target: 'Applying extrude',
actions: ['AST extrude'], reenter: true,
reenter: false,
}, },
Revolve: { Revolve: {
@ -2548,7 +2561,7 @@ export const modelingMachine = setup({
'Delete segment': { 'Delete segment': {
reenter: false, reenter: false,
actions: ['Delete segment', 'Set sketchDetails'], actions: ['Delete segment', 'Set sketchDetails', 'reset selections'],
}, },
'code edit during sketch': '.clean slate', 'code edit during sketch': '.clean slate',
}, },
@ -2620,6 +2633,22 @@ export const modelingMachine = setup({
}, },
}, },
'Applying extrude': {
invoke: {
src: 'extrudeAstMod',
id: 'extrudeAstMod',
input: ({ event }) => {
if (event.type !== 'Extrude') return undefined
return event.data
},
onDone: ['idle'],
onError: {
target: 'idle',
actions: 'toastError',
},
},
},
'Applying offset plane': { 'Applying offset plane': {
invoke: { invoke: {
src: 'offsetPlaneAstMod', src: 'offsetPlaneAstMod',

View File

@ -1,15 +1,14 @@
import { OnboardingButtons, useDismiss, useNextClick } from '.' import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useUser } from 'machines/appMachine'
export default function UserMenu() { export default function UserMenu() {
const { auth } = useSettingsAuthContext() const user = useUser()
const dismiss = useDismiss() const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.PROJECT_MENU) const next = useNextClick(onboardingPaths.PROJECT_MENU)
const [avatarErrored, setAvatarErrored] = useState(false) const [avatarErrored, setAvatarErrored] = useState(false)
const user = auth?.context?.user
const errorOrNoImage = !user?.image || avatarErrored const errorOrNoImage = !user?.image || avatarErrored
const buttonDescription = errorOrNoImage ? 'the menu button' : 'your avatar' const buttonDescription = errorOrNoImage ? 'the menu button' : 'your avatar'

View File

@ -14,6 +14,7 @@ import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { toSync } from 'lib/utils' import { toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { authActor } from 'machines/appMachine'
const subtleBorder = const subtleBorder =
'border border-solid border-chalkboard-30 dark:border-chalkboard-80' 'border border-solid border-chalkboard-30 dark:border-chalkboard-80'
@ -22,7 +23,6 @@ const cardArea = `${subtleBorder} rounded-lg px-6 py-3 text-chalkboard-70 dark:t
const SignIn = () => { const SignIn = () => {
const [userCode, setUserCode] = useState('') const [userCode, setUserCode] = useState('')
const { const {
auth: { send },
settings: { settings: {
state: { state: {
context: { context: {
@ -70,7 +70,7 @@ const SignIn = () => {
toast.error('Error while trying to log in') toast.error('Error while trying to log in')
return return
} }
send({ type: 'Log in', token }) authActor.send({ type: 'Log in', token })
} }
return ( return (

View File

@ -1710,7 +1710,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.30" version = "0.2.33"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",
@ -1844,9 +1844,9 @@ dependencies = [
[[package]] [[package]]
name = "kittycad-modeling-cmds" name = "kittycad-modeling-cmds"
version = "0.2.92" version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5fc91d0cdacd1c2ba906f564e58ce07c70a5c471f19b0f4c0b67e754077bfdc" checksum = "67a993046541732e3c3ddd8a0364b55b7b138a9258beff353b6e7a043a41dce3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",

View File

@ -76,7 +76,7 @@ members = [
[workspace.dependencies] [workspace.dependencies]
http = "1" http = "1"
kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] } kittycad = { version = "0.3.28", default-features = false, features = ["js", "requests"] }
kittycad-modeling-cmds = { version = "0.2.92", features = [ kittycad-modeling-cmds = { version = "0.2.93", features = [
"ts-rs", "ts-rs",
"websocket", "websocket",
] } ] }

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.2.30" version = "0.2.33"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -13,9 +13,7 @@ use tower_lsp::lsp_types::{
MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation, MarkupKind, ParameterInformation, ParameterLabel, SignatureHelp, SignatureInformation,
}; };
use crate::execution::Sketch; use crate::{execution::Sketch, std::Primitive};
use crate::std::Primitive;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
#[ts(export)] #[ts(export)]

View File

@ -197,10 +197,6 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
source_range: SourceRange, source_range: SourceRange,
cmd: &ModelingCmd, cmd: &ModelingCmd,
) -> Result<(), crate::errors::KclError> { ) -> Result<(), crate::errors::KclError> {
let execution_kind = self.execution_kind();
if execution_kind.is_isolated() {
return Err(KclError::Semantic(KclErrorDetails { message: "Cannot send modeling commands while importing. Wrap your code in a function if you want to import the file.".to_owned(), source_ranges: vec![source_range] }));
}
let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq { let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
cmd: cmd.clone(), cmd: cmd.clone(),
cmd_id: id.into(), cmd_id: id.into(),

View File

@ -7,9 +7,9 @@ use crate::{
KclError, SourceRange, KclError, SourceRange,
}; };
pub(super) const SETTINGS: &str = "settings"; pub(crate) const SETTINGS: &str = "settings";
pub(super) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit"; pub(crate) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
pub(super) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit"; pub(crate) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
#[derive(Copy, Clone, Debug, Eq, PartialEq)] #[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) enum AnnotationScope { pub(super) enum AnnotationScope {

View File

@ -462,12 +462,14 @@ impl ArtifactGraph {
output.push_str("mindmap\n"); output.push_str("mindmap\n");
output.push_str(" root\n"); output.push_str(" root\n");
let mut ids_seen: fnv::FnvHashSet<ArtifactId> = Default::default();
for (_, artifact) in &self.map { for (_, artifact) in &self.map {
// Only the planes are roots. // Only the planes are roots.
let Artifact::Plane(_) = artifact else { let Artifact::Plane(_) = artifact else {
continue; continue;
}; };
self.mind_map_artifact(&mut output, artifact, " ")?; self.mind_map_artifact(&mut output, &mut ids_seen, artifact, " ")?;
} }
output.push_str("```\n"); output.push_str("```\n");
@ -475,9 +477,16 @@ impl ArtifactGraph {
Ok(output) Ok(output)
} }
fn mind_map_artifact<W: Write>(&self, output: &mut W, artifact: &Artifact, prefix: &str) -> std::fmt::Result { fn mind_map_artifact<W: Write>(
&self,
output: &mut W,
ids_seen: &mut fnv::FnvHashSet<ArtifactId>,
artifact: &Artifact,
prefix: &str,
) -> std::fmt::Result {
match artifact { match artifact {
Artifact::Plane(_plane) => { Artifact::Plane(_plane) => {
ids_seen.clear();
writeln!(output, "{prefix}Plane")?; writeln!(output, "{prefix}Plane")?;
} }
Artifact::Path(_path) => { Artifact::Path(_path) => {
@ -515,11 +524,17 @@ impl ArtifactGraph {
} }
} }
if ids_seen.contains(&artifact.id()) {
return Ok(());
}
ids_seen.insert(artifact.id());
for child_id in artifact.child_ids() { for child_id in artifact.child_ids() {
let Some(child_artifact) = self.map.get(&child_id) else { let Some(child_artifact) = self.map.get(&child_id) else {
continue; continue;
}; };
self.mind_map_artifact(output, child_artifact, &format!("{} ", prefix))?; self.mind_map_artifact(output, ids_seen, child_artifact, &format!("{} ", prefix))?;
} }
Ok(()) Ok(())

View File

@ -2,6 +2,7 @@ use std::collections::HashMap;
use async_recursion::async_recursion; use async_recursion::async_recursion;
use super::cad_op::{OpArg, Operation};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
@ -19,8 +20,6 @@ use crate::{
}, },
}; };
use super::cad_op::{OpArg, Operation};
impl BinaryPart { impl BinaryPart {
#[async_recursion] #[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {

View File

@ -0,0 +1,293 @@
use std::{ffi::OsStr, path::Path, str::FromStr};
use anyhow::Result;
use kcmc::{
coord::{Axis, AxisDirectionPair, Direction, System},
each_cmd as mcmd,
format::InputFormat,
ok_response::OkModelingCmdResponse,
shared::FileImportFormat,
units::UnitLength,
websocket::OkWebSocketResponseData,
ImportFile, ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::ExecutorContext;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, ImportedGeometry},
fs::FileSystem,
source_range::SourceRange,
};
// Zoo co-ordinate system.
//
// * Forward: -Y
// * Up: +Z
// * Handedness: Right
pub const ZOO_COORD_SYSTEM: System = System {
forward: AxisDirectionPair {
axis: Axis::Y,
direction: Direction::Negative,
},
up: AxisDirectionPair {
axis: Axis::Z,
direction: Direction::Positive,
},
};
pub async fn import_foreign(
file_path: &Path,
format: Option<InputFormat>,
exec_state: &mut ExecState,
ctxt: &ExecutorContext,
source_range: SourceRange,
) -> Result<PreImportedGeometry, KclError> {
// Make sure the file exists.
if !ctxt.fs.exists(file_path, source_range).await? {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("File `{}` does not exist.", file_path.display()),
source_ranges: vec![source_range],
}));
}
let ext_format = get_import_format_from_extension(file_path.extension().ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("No file extension found for `{}`", file_path.display()),
source_ranges: vec![source_range],
})
})?)
.map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![source_range],
})
})?;
// Get the format type from the extension of the file.
let format = if let Some(format) = format {
// Validate the given format with the extension format.
validate_extension_format(ext_format, format.clone()).map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![source_range],
})
})?;
format
} else {
ext_format
};
// Get the file contents for each file path.
let file_contents = ctxt.fs.read(file_path, source_range).await.map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![source_range],
})
})?;
// We want the file_path to be without the parent.
let file_name = std::path::Path::new(&file_path)
.file_name()
.map(|p| p.to_string_lossy().to_string())
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Could not get the file name from the path `{}`", file_path.display()),
source_ranges: vec![source_range],
})
})?;
let mut import_files = vec![kcmc::ImportFile {
path: file_name.to_string(),
data: file_contents.clone(),
}];
// In the case of a gltf importing a bin file we need to handle that! and figure out where the
// file is relative to our current file.
if let InputFormat::Gltf(..) = format {
// Check if the file is a binary gltf file, in that case we don't need to import the bin
// file.
if !file_contents.starts_with(b"glTF") {
let json = gltf_json::Root::from_slice(&file_contents).map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![source_range],
})
})?;
// Read the gltf file and check if there is a bin file.
for buffer in json.buffers.iter() {
if let Some(uri) = &buffer.uri {
if !uri.starts_with("data:") {
// We want this path relative to the file_path given.
let bin_path = std::path::Path::new(&file_path)
.parent()
.map(|p| p.join(uri))
.map(|p| p.to_string_lossy().to_string())
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!(
"Could not get the parent path of the file `{}`",
file_path.display()
),
source_ranges: vec![source_range],
})
})?;
let bin_contents = ctxt.fs.read(&bin_path, source_range).await.map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![source_range],
})
})?;
import_files.push(ImportFile {
path: uri.to_string(),
data: bin_contents,
});
}
}
}
}
}
Ok(PreImportedGeometry {
id: exec_state.next_uuid(),
source_range,
command: mcmd::ImportFiles {
files: import_files.clone(),
format,
},
})
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct PreImportedGeometry {
id: Uuid,
command: mcmd::ImportFiles,
source_range: SourceRange,
}
pub async fn send_to_engine(pre: PreImportedGeometry, ctxt: &ExecutorContext) -> Result<ImportedGeometry, KclError> {
if ctxt.is_mock() {
return Ok(ImportedGeometry {
id: pre.id,
value: pre.command.files.iter().map(|f| f.path.to_string()).collect(),
meta: vec![pre.source_range.into()],
});
}
let resp = ctxt
.engine
.send_modeling_cmd(pre.id, pre.source_range, &ModelingCmd::from(pre.command.clone()))
.await?;
let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::ImportFiles(imported_files),
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("ImportFiles response was not as expected: {:?}", resp),
source_ranges: vec![pre.source_range],
}));
};
Ok(ImportedGeometry {
id: imported_files.object_id,
value: pre.command.files.iter().map(|f| f.path.to_string()).collect(),
meta: vec![pre.source_range.into()],
})
}
/// Get the source format from the extension.
fn get_import_format_from_extension(ext: &OsStr) -> Result<InputFormat> {
let ext = ext
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid file extension: `{ext:?}`"))?;
let format = match FileImportFormat::from_str(ext) {
Ok(format) => format,
Err(_) => {
if ext == "stp" {
FileImportFormat::Step
} else if ext == "glb" {
FileImportFormat::Gltf
} else {
anyhow::bail!("unknown source format for file extension: {ext}. Try setting the `--src-format` flag explicitly or use a valid format.")
}
}
};
// Make the default units millimeters.
let ul = UnitLength::Millimeters;
// Zoo co-ordinate system.
//
// * Forward: -Y
// * Up: +Z
// * Handedness: Right
match format {
FileImportFormat::Step => Ok(InputFormat::Step(kcmc::format::step::import::Options {
split_closed_faces: false,
})),
FileImportFormat::Stl => Ok(InputFormat::Stl(kcmc::format::stl::import::Options {
coords: ZOO_COORD_SYSTEM,
units: ul,
})),
FileImportFormat::Obj => Ok(InputFormat::Obj(kcmc::format::obj::import::Options {
coords: ZOO_COORD_SYSTEM,
units: ul,
})),
FileImportFormat::Gltf => Ok(InputFormat::Gltf(kcmc::format::gltf::import::Options {})),
FileImportFormat::Ply => Ok(InputFormat::Ply(kcmc::format::ply::import::Options {
coords: ZOO_COORD_SYSTEM,
units: ul,
})),
FileImportFormat::Fbx => Ok(InputFormat::Fbx(kcmc::format::fbx::import::Options {})),
FileImportFormat::Sldprt => Ok(InputFormat::Sldprt(kcmc::format::sldprt::import::Options {
split_closed_faces: false,
})),
}
}
fn validate_extension_format(ext: InputFormat, given: InputFormat) -> Result<()> {
if let InputFormat::Stl(_) = ext {
if let InputFormat::Stl(_) = given {
return Ok(());
}
}
if let InputFormat::Obj(_) = ext {
if let InputFormat::Obj(_) = given {
return Ok(());
}
}
if let InputFormat::Ply(_) = ext {
if let InputFormat::Ply(_) = given {
return Ok(());
}
}
if ext == given {
return Ok(());
}
anyhow::bail!(
"The given format does not match the file extension. Expected: `{}`, Given: `{}`",
get_name_of_format(ext),
get_name_of_format(given)
)
}
fn get_name_of_format(type_: InputFormat) -> &'static str {
match type_ {
InputFormat::Fbx(_) => "fbx",
InputFormat::Gltf(_) => "gltf",
InputFormat::Obj(_) => "obj",
InputFormat::Ply(_) => "ply",
InputFormat::Sldprt(_) => "sldprt",
InputFormat::Step(_) => "step",
InputFormat::Stl(_) => "stl",
}
}

View File

@ -581,10 +581,11 @@ impl KclValue {
} }
// TODO called UnitLen so as not to clash with UnitLength in settings) // TODO called UnitLen so as not to clash with UnitLength in settings)
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)] #[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum UnitLen { pub enum UnitLen {
#[default]
Mm, Mm,
Cm, Cm,
M, M,
@ -593,6 +594,19 @@ pub enum UnitLen {
Yards, Yards,
} }
impl std::fmt::Display for UnitLen {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UnitLen::Mm => write!(f, "mm"),
UnitLen::Cm => write!(f, "cm"),
UnitLen::M => write!(f, "m"),
UnitLen::Inches => write!(f, "in"),
UnitLen::Feet => write!(f, "ft"),
UnitLen::Yards => write!(f, "yd"),
}
}
}
impl TryFrom<NumericSuffix> for UnitLen { impl TryFrom<NumericSuffix> for UnitLen {
type Error = (); type Error = ();
@ -644,6 +658,15 @@ pub enum UnitAngle {
Radians, Radians,
} }
impl std::fmt::Display for UnitAngle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UnitAngle::Degrees => write!(f, "deg"),
UnitAngle::Radians => write!(f, "rad"),
}
}
}
impl TryFrom<NumericSuffix> for UnitAngle { impl TryFrom<NumericSuffix> for UnitAngle {
type Error = (); type Error = ();

View File

@ -13,8 +13,7 @@ use kcmc::{
websocket::{ModelingSessionData, OkWebSocketResponseData}, websocket::{ModelingSessionData, OkWebSocketResponseData},
ImageFormat, ModelingCmd, ImageFormat, ModelingCmd,
}; };
use kittycad_modeling_cmds::length_unit::LengthUnit; use kittycad_modeling_cmds::{self as kcmc, length_unit::LengthUnit, websocket::WebSocketResponse};
use kittycad_modeling_cmds::{self as kcmc, websocket::WebSocketResponse};
use parse_display::{Display, FromStr}; use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -23,16 +22,22 @@ type Point2D = kcmc::shared::Point2d<f64>;
type Point3D = kcmc::shared::Point3d<f64>; type Point3D = kcmc::shared::Point3d<f64>;
pub use function_param::FunctionParam; pub use function_param::FunctionParam;
pub(crate) use import::{import_foreign, send_to_engine as send_import_to_engine, ZOO_COORD_SYSTEM};
pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen}; pub use kcl_value::{KclObjectFields, KclValue, UnitAngle, UnitLen};
use uuid::Uuid; use uuid::Uuid;
mod annotations; pub(crate) mod annotations;
mod artifact; mod artifact;
pub(crate) mod cache; pub(crate) mod cache;
mod cad_op; mod cad_op;
mod exec_ast; mod exec_ast;
mod function_param; mod function_param;
mod kcl_value; mod import;
pub(crate) mod kcl_value;
// Re-exports.
pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
pub use cad_op::Operation;
use crate::{ use crate::{
engine::{EngineManager, ExecutionKind}, engine::{EngineManager, ExecutionKind},
@ -40,7 +45,7 @@ use crate::{
execution::cache::{CacheInformation, CacheResult}, execution::cache::{CacheInformation, CacheResult},
fs::{FileManager, FileSystem}, fs::{FileManager, FileSystem},
parsing::ast::types::{ parsing::ast::types::{
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, NonCodeValue, BodyItem, Expr, FunctionExpression, ImportPath, ImportSelector, ItemVisibility, Node, NodeRef, NonCodeValue,
Program as AstProgram, TagDeclarator, TagNode, Program as AstProgram, TagDeclarator, TagNode,
}, },
settings::types::UnitLength, settings::types::UnitLength,
@ -50,10 +55,6 @@ use crate::{
ExecError, KclErrorWithOutputs, Program, ExecError, KclErrorWithOutputs, Program,
}; };
// Re-exports.
pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
pub use cad_op::Operation;
/// State for executing a program. /// State for executing a program.
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -129,7 +130,7 @@ pub struct ExecOutcome {
impl ExecState { impl ExecState {
pub fn new(exec_settings: &ExecutorSettings) -> Self { pub fn new(exec_settings: &ExecutorSettings) -> Self {
ExecState { ExecState {
global: GlobalState::new(), global: GlobalState::new(exec_settings),
mod_local: ModuleState::new(exec_settings), mod_local: ModuleState::new(exec_settings),
} }
} }
@ -140,7 +141,7 @@ impl ExecState {
// This is for the front end to keep track of the ids. // This is for the front end to keep track of the ids.
id_generator.next_id = 0; id_generator.next_id = 0;
let mut global = GlobalState::new(); let mut global = GlobalState::new(exec_settings);
global.id_generator = id_generator; global.id_generator = id_generator;
*self = ExecState { *self = ExecState {
@ -181,34 +182,15 @@ impl ExecState {
self.global.artifacts.insert(id, artifact); self.global.artifacts.insert(id, artifact);
} }
async fn add_module( fn add_module(&mut self, id: ModuleId, path: std::path::PathBuf, repr: ModuleRepr) -> ModuleId {
&mut self, debug_assert!(!self.global.path_to_source_id.contains_key(&path));
path: std::path::PathBuf,
ctxt: &ExecutorContext,
source_range: SourceRange,
) -> Result<ModuleId, KclError> {
// Need to avoid borrowing self in the closure.
let new_module_id = ModuleId::from_usize(self.global.path_to_source_id.len());
let mut is_new = false;
let id = *self.global.path_to_source_id.entry(path.clone()).or_insert_with(|| {
is_new = true;
new_module_id
});
if is_new { self.global.path_to_source_id.insert(path.clone(), id);
let source = ctxt.fs.read_to_string(&path, source_range).await?;
// TODO handle parsing errors properly
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?;
let module_info = ModuleInfo { let module_info = ModuleInfo { id, repr, path };
id, self.global.module_infos.insert(id, module_info);
path,
parsed: Some(parsed),
};
self.global.module_infos.insert(id, module_info);
}
Ok(id) id
} }
pub fn length_unit(&self) -> UnitLen { pub fn length_unit(&self) -> UnitLen {
@ -221,7 +203,7 @@ impl ExecState {
} }
impl GlobalState { impl GlobalState {
fn new() -> Self { fn new(settings: &ExecutorSettings) -> Self {
let mut global = GlobalState { let mut global = GlobalState {
id_generator: Default::default(), id_generator: Default::default(),
path_to_source_id: Default::default(), path_to_source_id: Default::default(),
@ -232,15 +214,14 @@ impl GlobalState {
artifact_graph: Default::default(), artifact_graph: Default::default(),
}; };
// TODO(#4434): Use the top-level file's path.
let root_path = PathBuf::new();
let root_id = ModuleId::default(); let root_id = ModuleId::default();
let root_path = settings.current_file.clone().unwrap_or_default();
global.module_infos.insert( global.module_infos.insert(
root_id, root_id,
ModuleInfo { ModuleInfo {
id: root_id, id: root_id,
path: root_path.clone(), path: root_path.clone(),
parsed: None, repr: ModuleRepr::Root,
}, },
); );
global.path_to_source_id.insert(root_path, root_id); global.path_to_source_id.insert(root_path, root_id);
@ -265,7 +246,7 @@ impl ModuleState {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MetaSettings { pub struct MetaSettings {
@ -274,7 +255,11 @@ pub struct MetaSettings {
} }
impl MetaSettings { impl MetaSettings {
fn update_from_annotation(&mut self, annotation: &NonCodeValue, source_range: SourceRange) -> Result<(), KclError> { pub fn update_from_annotation(
&mut self,
annotation: &NonCodeValue,
source_range: SourceRange,
) -> Result<(), KclError> {
let properties = annotations::expect_properties(annotations::SETTINGS, annotation, source_range)?; let properties = annotations::expect_properties(annotations::SETTINGS, annotation, source_range)?;
for p in properties { for p in properties {
@ -1253,7 +1238,15 @@ pub struct ModuleInfo {
id: ModuleId, id: ModuleId,
/// Absolute path of the module's source file. /// Absolute path of the module's source file.
path: std::path::PathBuf, path: std::path::PathBuf,
parsed: Option<Node<AstProgram>>, repr: ModuleRepr,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum ModuleRepr {
Root,
Kcl(Node<AstProgram>),
Foreign(import::PreImportedGeometry),
} }
#[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema)] #[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema)]
@ -1761,6 +1754,9 @@ pub struct ExecutorSettings {
/// The directory of the current project. This is used for resolving import /// The directory of the current project. This is used for resolving import
/// paths. If None is given, the current working directory is used. /// paths. If None is given, the current working directory is used.
pub project_directory: Option<PathBuf>, pub project_directory: Option<PathBuf>,
/// This is the path to the current file being executed.
/// We use this for preventing cyclic imports.
pub current_file: Option<PathBuf>,
} }
impl Default for ExecutorSettings { impl Default for ExecutorSettings {
@ -1772,6 +1768,7 @@ impl Default for ExecutorSettings {
show_grid: false, show_grid: false,
replay: None, replay: None,
project_directory: None, project_directory: None,
current_file: None,
} }
} }
} }
@ -1785,6 +1782,7 @@ impl From<crate::settings::types::Configuration> for ExecutorSettings {
show_grid: config.settings.modeling.show_scale_grid, show_grid: config.settings.modeling.show_scale_grid,
replay: None, replay: None,
project_directory: None, project_directory: None,
current_file: None,
} }
} }
} }
@ -1798,6 +1796,7 @@ impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSet
show_grid: config.settings.modeling.show_scale_grid, show_grid: config.settings.modeling.show_scale_grid,
replay: None, replay: None,
project_directory: None, project_directory: None,
current_file: None,
} }
} }
} }
@ -1811,6 +1810,25 @@ impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
show_grid: modeling.show_scale_grid, show_grid: modeling.show_scale_grid,
replay: None, replay: None,
project_directory: None, project_directory: None,
current_file: None,
}
}
}
impl ExecutorSettings {
/// Add the current file path to the executor settings.
pub fn with_current_file(&mut self, current_file: PathBuf) {
// We want the parent directory of the file.
if current_file.extension() == Some(std::ffi::OsStr::new("kcl")) {
self.current_file = Some(current_file.clone());
// Get the parent directory.
if let Some(parent) = current_file.parent() {
self.project_directory = Some(parent.to_path_buf());
} else {
self.project_directory = Some(std::path::PathBuf::from(""));
}
} else {
self.project_directory = Some(current_file.clone());
} }
} }
} }
@ -2028,6 +2046,7 @@ impl ExecutorContext {
show_grid: false, show_grid: false,
replay: None, replay: None,
project_directory: None, project_directory: None,
current_file: None,
}, },
None, None,
engine_addr, engine_addr,
@ -2511,33 +2530,68 @@ impl ExecutorContext {
async fn open_module( async fn open_module(
&self, &self,
path: &str, path: &ImportPath,
exec_state: &mut ExecState, exec_state: &mut ExecState,
source_range: SourceRange, source_range: SourceRange,
) -> Result<ModuleId, KclError> { ) -> Result<ModuleId, KclError> {
let resolved_path = if let Some(project_dir) = &self.settings.project_directory { match path {
project_dir.join(path) ImportPath::Kcl { filename } => {
} else { let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
std::path::PathBuf::from(&path) project_dir.join(filename)
}; } else {
std::path::PathBuf::from(filename)
};
if exec_state.mod_local.import_stack.contains(&resolved_path) { if exec_state.mod_local.import_stack.contains(&resolved_path) {
return Err(KclError::ImportCycle(KclErrorDetails { return Err(KclError::ImportCycle(KclErrorDetails {
message: format!( message: format!(
"circular import of modules is not allowed: {} -> {}", "circular import of modules is not allowed: {} -> {}",
exec_state exec_state
.mod_local .mod_local
.import_stack .import_stack
.iter() .iter()
.map(|p| p.as_path().to_string_lossy()) .map(|p| p.as_path().to_string_lossy())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" -> "), .join(" -> "),
resolved_path.to_string_lossy() resolved_path.to_string_lossy()
), ),
source_ranges: vec![source_range],
}));
}
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
return Ok(*id);
}
let source = self.fs.read_to_string(&resolved_path, source_range).await?;
let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len());
// TODO handle parsing errors properly
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?;
let repr = ModuleRepr::Kcl(parsed);
Ok(exec_state.add_module(id, resolved_path, repr))
}
ImportPath::Foreign { path } => {
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
project_dir.join(path)
} else {
std::path::PathBuf::from(path)
};
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
return Ok(*id);
}
let geom = import::import_foreign(&resolved_path, None, exec_state, self, source_range).await?;
let repr = ModuleRepr::Foreign(geom);
let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len());
Ok(exec_state.add_module(id, resolved_path, repr))
}
i => Err(KclError::Semantic(KclErrorDetails {
message: format!("Unsupported import: `{i}`"),
source_ranges: vec![source_range], source_ranges: vec![source_range],
})); })),
} }
exec_state.add_module(resolved_path.clone(), self, source_range).await
} }
async fn exec_module( async fn exec_module(
@ -2551,43 +2605,64 @@ impl ExecutorContext {
// TODO It sucks that we have to clone the whole module AST here // TODO It sucks that we have to clone the whole module AST here
let info = exec_state.global.module_infos[&module_id].clone(); let info = exec_state.global.module_infos[&module_id].clone();
let mut local_state = ModuleState { match &info.repr {
import_stack: exec_state.mod_local.import_stack.clone(), ModuleRepr::Root => Err(KclError::ImportCycle(KclErrorDetails {
..ModuleState::new(&self.settings) message: format!(
}; "circular import of modules is not allowed: {} -> {}",
local_state.import_stack.push(info.path.clone()); exec_state
std::mem::swap(&mut exec_state.mod_local, &mut local_state); .mod_local
let original_execution = self.engine.replace_execution_kind(exec_kind); .import_stack
.iter()
.map(|p| p.as_path().to_string_lossy())
.collect::<Vec<_>>()
.join(" -> "),
info.path.display()
),
source_ranges: vec![source_range],
})),
ModuleRepr::Kcl(program) => {
let mut local_state = ModuleState {
import_stack: exec_state.mod_local.import_stack.clone(),
..ModuleState::new(&self.settings)
};
local_state.import_stack.push(info.path.clone());
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
let original_execution = self.engine.replace_execution_kind(exec_kind);
// The unwrap here is safe since we only elide the AST for the top module. let result = self
let result = self .inner_execute(program, exec_state, crate::execution::BodyType::Root)
.inner_execute(&info.parsed.unwrap(), exec_state, crate::execution::BodyType::Root) .await;
.await;
let new_units = exec_state.length_unit(); let new_units = exec_state.length_unit();
std::mem::swap(&mut exec_state.mod_local, &mut local_state); std::mem::swap(&mut exec_state.mod_local, &mut local_state);
if new_units != old_units { if new_units != old_units {
self.engine.set_units(old_units.into(), Default::default()).await?; self.engine.set_units(old_units.into(), Default::default()).await?;
} }
self.engine.replace_execution_kind(original_execution); self.engine.replace_execution_kind(original_execution);
let result = result.map_err(|err| { let result = result.map_err(|err| {
if let KclError::ImportCycle(_) = err { if let KclError::ImportCycle(_) = err {
// It was an import cycle. Keep the original message. // It was an import cycle. Keep the original message.
err.override_source_ranges(vec![source_range]) err.override_source_ranges(vec![source_range])
} else { } else {
KclError::Semantic(KclErrorDetails { KclError::Semantic(KclErrorDetails {
message: format!( message: format!(
"Error loading imported file. Open it to view more details. {}: {}", "Error loading imported file. Open it to view more details. {}: {}",
info.path.display(), info.path.display(),
err.message() err.message()
), ),
source_ranges: vec![source_range], source_ranges: vec![source_range],
}) })
}
})?;
Ok((result, local_state.memory, local_state.module_exports))
} }
})?; ModuleRepr::Foreign(geom) => {
let geom = send_import_to_engine(geom.clone(), self).await?;
Ok((result, local_state.memory, local_state.module_exports)) Ok((Some(KclValue::ImportedGeometry(geom)), ProgramMemory::new(), Vec::new()))
}
}
} }
#[async_recursion] #[async_recursion]
@ -2608,15 +2683,20 @@ impl ExecutorContext {
let (result, _, _) = self let (result, _, _) = self
.exec_module(module_id, exec_state, ExecutionKind::Normal, metadata.source_range) .exec_module(module_id, exec_state, ExecutionKind::Normal, metadata.source_range)
.await?; .await?;
result.ok_or_else(|| { result.unwrap_or_else(|| {
KclError::Semantic(KclErrorDetails { // The module didn't have a return value. Currently,
message: format!( // the only way to have a return value is with the final
"Evaluating module `{}` as part of an assembly did not produce a result", // statement being an expression statement.
identifier.name //
), // TODO: Make a warning when we support them in the
source_ranges: vec![metadata.source_range, meta[0].source_range], // execution phase.
}) let mut new_meta = vec![metadata.to_owned()];
})? new_meta.extend(meta);
KclValue::KclNone {
value: Default::default(),
meta: new_meta,
}
})
} else { } else {
value value
} }
@ -3421,6 +3501,16 @@ const inInches = 1.0 * inch()"#;
assert_eq!(1.0, mem_get_json(exec_state.memory(), "inInches").as_f64().unwrap()); assert_eq!(1.0, mem_get_json(exec_state.memory(), "inInches").as_f64().unwrap());
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_unit_overriden_in() {
let ast = r#"@settings(defaultLengthUnit = in)
const inMm = 25.4 * mm()
const inInches = 2.0 * inch()"#;
let (_, _, exec_state) = parse_execute(ast).await.unwrap();
assert_eq!(1.0, mem_get_json(exec_state.memory(), "inMm").as_f64().unwrap().round());
assert_eq!(2.0, mem_get_json(exec_state.memory(), "inInches").as_f64().unwrap());
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_zero_param_fn() { async fn test_zero_param_fn() {
let ast = r#"const sigmaAllow = 35000 // psi let ast = r#"const sigmaAllow = 35000 // psi

View File

@ -84,7 +84,7 @@ pub use engine::{EngineManager, ExecutionKind};
pub use errors::{CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs}; pub use errors::{CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs};
pub use execution::{ pub use execution::{
cache::{CacheInformation, OldAstState}, cache::{CacheInformation, OldAstState},
ExecState, ExecutorContext, ExecutorSettings, Point2d, ExecState, ExecutorContext, ExecutorSettings, MetaSettings, Point2d,
}; };
pub use lsp::{ pub use lsp::{
copilot::Backend as CopilotLspBackend, copilot::Backend as CopilotLspBackend,
@ -120,6 +120,10 @@ pub mod std_utils {
pub use crate::std::utils::{get_tangential_arc_to_info, is_points_ccw_wasm, TangentialArcInfoInput}; pub use crate::std::utils::{get_tangential_arc_to_info, is_points_ccw_wasm, TangentialArcInfoInput};
} }
pub mod pretty {
pub use crate::{parsing::token::NumericSuffix, unparser::format_number};
}
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[allow(unused_imports)] #[allow(unused_imports)]
@ -157,6 +161,18 @@ impl Program {
self.ast.compute_digest() self.ast.compute_digest()
} }
/// Get the meta settings for the kcl file from the annotations.
pub fn get_meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
self.ast.get_meta_settings()
}
/// Change the meta settings for the kcl file.
pub fn change_meta_settings(&mut self, settings: crate::MetaSettings) -> Result<Self, KclError> {
Ok(Self {
ast: self.ast.change_meta_settings(settings)?,
})
}
pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> { pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> {
self.ast.lint_all() self.ast.lint_all()
} }

View File

@ -66,7 +66,8 @@ impl ImportStatement {
} }
} }
hasher.update(slf.visibility.digestable_id()); hasher.update(slf.visibility.digestable_id());
let path = slf.path.as_bytes(); let path = slf.path.to_string();
let path = path.as_bytes();
hasher.update(path.len().to_ne_bytes()); hasher.update(path.len().to_ne_bytes());
hasher.update(path); hasher.update(path);
}); });

View File

@ -6,6 +6,7 @@ use kcmc::{
}; };
use kittycad_modeling_cmds as kcmc; use kittycad_modeling_cmds as kcmc;
use super::types::LiteralValue;
use crate::{ use crate::{
engine::EngineManager, engine::EngineManager,
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
@ -18,8 +19,6 @@ use crate::{
Program, Program,
}; };
use super::types::LiteralValue;
type Point3d = kcmc::shared::Point3d<f64>; type Point3d = kcmc::shared::Point3d<f64>;
#[derive(Debug)] #[derive(Debug)]

View File

@ -17,7 +17,6 @@ use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, DocumentSymbol, FoldingRange, FoldingRangeKind, Range as LspRange, SymbolKind, CompletionItem, CompletionItemKind, DocumentSymbol, FoldingRange, FoldingRangeKind, Range as LspRange, SymbolKind,
}; };
use super::digest::Digest;
pub use crate::parsing::ast::types::{ pub use crate::parsing::ast::types::{
condition::{ElseIf, IfExpression}, condition::{ElseIf, IfExpression},
literal_value::LiteralValue, literal_value::LiteralValue,
@ -26,7 +25,8 @@ pub use crate::parsing::ast::types::{
use crate::{ use crate::{
docs::StdLibFn, docs::StdLibFn,
errors::KclError, errors::KclError,
execution::{KclValue, Metadata, TagIdentifier}, execution::{annotations, KclValue, Metadata, TagIdentifier},
parsing::ast::digest::Digest,
parsing::PIPE_OPERATOR, parsing::PIPE_OPERATOR,
source_range::{ModuleId, SourceRange}, source_range::{ModuleId, SourceRange},
}; };
@ -254,6 +254,52 @@ impl Node<Program> {
} }
Ok(findings) Ok(findings)
} }
/// Get the annotations for the meta settings from the kcl file.
pub fn get_meta_settings(&self) -> Result<Option<crate::execution::MetaSettings>, KclError> {
let annotations = self
.non_code_meta
.start_nodes
.iter()
.filter_map(|n| n.annotation().map(|result| (result, n.as_source_range())));
for (annotation, source_range) in annotations {
if annotation.annotation_name() == Some(annotations::SETTINGS) {
let mut meta_settings = crate::execution::MetaSettings::default();
meta_settings.update_from_annotation(annotation, source_range)?;
return Ok(Some(meta_settings));
}
}
Ok(None)
}
pub fn change_meta_settings(&mut self, settings: crate::execution::MetaSettings) -> Result<Self, KclError> {
let mut new_program = self.clone();
let mut found = false;
for node in &mut new_program.non_code_meta.start_nodes {
if let Some(annotation) = node.annotation() {
if annotation.annotation_name() == Some(annotations::SETTINGS) {
let annotation = NonCodeValue::new_from_meta_settings(&settings);
*node = Node::no_src(NonCodeNode {
value: annotation,
digest: None,
});
found = true;
break;
}
}
}
if !found {
let annotation = NonCodeValue::new_from_meta_settings(&settings);
new_program.non_code_meta.start_nodes.push(Node::no_src(NonCodeNode {
value: annotation,
digest: None,
}));
}
Ok(new_program)
}
} }
impl Program { impl Program {
@ -1078,6 +1124,24 @@ impl NonCodeValue {
_ => None, _ => None,
} }
} }
pub fn new_from_meta_settings(settings: &crate::execution::MetaSettings) -> NonCodeValue {
let mut properties: Vec<Node<ObjectProperty>> = vec![ObjectProperty::new(
Identifier::new(annotations::SETTINGS_UNIT_LENGTH),
Expr::Identifier(Box::new(Identifier::new(&settings.default_length_units.to_string()))),
)];
if settings.default_angle_units != Default::default() {
properties.push(ObjectProperty::new(
Identifier::new(annotations::SETTINGS_UNIT_ANGLE),
Expr::Identifier(Box::new(Identifier::new(&settings.default_angle_units.to_string()))),
));
}
NonCodeValue::Annotation {
name: Identifier::new(annotations::SETTINGS),
properties: Some(properties),
}
}
} }
#[derive(Debug, Default, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Default, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -1256,6 +1320,32 @@ impl ImportSelector {
ImportSelector::None { alias: Some(alias) } => alias.rename(old_name, new_name), ImportSelector::None { alias: Some(alias) } => alias.rename(old_name, new_name),
} }
} }
pub fn exposes_imported_name(&self) -> bool {
matches!(self, ImportSelector::None { alias: None })
}
pub fn imports_items(&self) -> bool {
!matches!(self, ImportSelector::None { .. })
}
}
#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum ImportPath {
Kcl { filename: String },
Foreign { path: String },
Std,
}
impl fmt::Display for ImportPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ImportPath::Kcl { filename: s } | ImportPath::Foreign { path: s } => write!(f, "{s}"),
ImportPath::Std => write!(f, "std"),
}
}
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -1263,7 +1353,7 @@ impl ImportSelector {
#[serde(tag = "type")] #[serde(tag = "type")]
pub struct ImportStatement { pub struct ImportStatement {
pub selector: ImportSelector, pub selector: ImportSelector,
pub path: String, pub path: ImportPath,
#[serde(default, skip_serializing_if = "ItemVisibility::is_default")] #[serde(default, skip_serializing_if = "ItemVisibility::is_default")]
pub visibility: ItemVisibility, pub visibility: ItemVisibility,
@ -1312,12 +1402,15 @@ impl ImportStatement {
return Some(alias.name.clone()); return Some(alias.name.clone());
} }
let mut parts = self.path.split('.'); let mut parts = match &self.path {
ImportPath::Kcl { filename: s } | ImportPath::Foreign { path: s } => s.split('.'),
_ => return None,
};
let name = parts.next()?; let name = parts.next()?;
let ext = parts.next()?; let _ext = parts.next()?;
let rest = parts.next(); let rest = parts.next();
if rest.is_some() || ext != "kcl" { if rest.is_some() {
return None; return None;
} }
@ -2308,6 +2401,14 @@ impl Node<ObjectProperty> {
} }
impl ObjectProperty { impl ObjectProperty {
pub fn new(key: Node<Identifier>, value: Expr) -> Node<Self> {
Node::no_src(Self {
key,
value,
digest: None,
})
}
/// Returns a hover value that includes the given character position. /// Returns a hover value that includes the given character position.
pub fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> { pub fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> {
let value_source_range: SourceRange = self.value.clone().into(); let value_source_range: SourceRange = self.value.clone().into();
@ -3727,4 +3828,98 @@ const cylinder = startSketchOn('-XZ')
assert_eq!(l.raw, "false"); assert_eq!(l.raw, "false");
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_parse_get_meta_settings_inch() {
let some_program_string = r#"@settings(defaultLengthUnit = inch)
startSketchOn('XY')"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
let result = program.get_meta_settings().unwrap();
assert!(result.is_some());
let meta_settings = result.unwrap();
assert_eq!(
meta_settings.default_length_units,
crate::execution::kcl_value::UnitLen::Inches
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_parse_get_meta_settings_inch_to_mm() {
let some_program_string = r#"@settings(defaultLengthUnit = inch)
startSketchOn('XY')"#;
let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
let result = program.get_meta_settings().unwrap();
assert!(result.is_some());
let meta_settings = result.unwrap();
assert_eq!(
meta_settings.default_length_units,
crate::execution::kcl_value::UnitLen::Inches
);
// Edit the ast.
let new_program = program
.change_meta_settings(crate::execution::MetaSettings {
default_length_units: crate::execution::kcl_value::UnitLen::Mm,
..Default::default()
})
.unwrap();
let result = new_program.get_meta_settings().unwrap();
assert!(result.is_some());
let meta_settings = result.unwrap();
assert_eq!(
meta_settings.default_length_units,
crate::execution::kcl_value::UnitLen::Mm
);
let formatted = new_program.recast(&Default::default(), 0);
assert_eq!(
formatted,
r#"@settings(defaultLengthUnit = mm)
startSketchOn('XY')
"#
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_parse_get_meta_settings_nothing_to_mm() {
let some_program_string = r#"startSketchOn('XY')"#;
let mut program = crate::parsing::top_level_parse(some_program_string).unwrap();
let result = program.get_meta_settings().unwrap();
assert!(result.is_none());
// Edit the ast.
let new_program = program
.change_meta_settings(crate::execution::MetaSettings {
default_length_units: crate::execution::kcl_value::UnitLen::Mm,
..Default::default()
})
.unwrap();
let result = new_program.get_meta_settings().unwrap();
assert!(result.is_some());
let meta_settings = result.unwrap();
assert_eq!(
meta_settings.default_length_units,
crate::execution::kcl_value::UnitLen::Mm
);
let formatted = new_program.recast(&Default::default(), 0);
assert_eq!(
formatted,
r#"@settings(defaultLengthUnit = mm)
startSketchOn('XY')
"#
);
}
} }

View File

@ -12,7 +12,10 @@ use winnow::{
token::{any, one_of, take_till}, token::{any, one_of, take_till},
}; };
use super::{ast::types::LabelledExpression, token::NumericSuffix}; use super::{
ast::types::{ImportPath, LabelledExpression},
token::NumericSuffix,
};
use crate::{ use crate::{
docs::StdLibFn, docs::StdLibFn,
errors::{CompilationError, Severity, Tag}, errors::{CompilationError, Severity, Tag},
@ -1545,33 +1548,11 @@ fn import_stmt(i: &mut TokenSlice) -> PResult<BoxNode<ImportStatement>> {
}; };
let mut end: usize = path.end; let mut end: usize = path.end;
let path_string = match path.inner.value {
LiteralValue::String(s) => s,
_ => unreachable!(),
};
if path_string.is_empty() {
return Err(ErrMode::Cut(
CompilationError::fatal(
SourceRange::new(path.start, path.end, path.module_id),
"import path cannot be empty",
)
.into(),
));
}
if path_string
.chars()
.any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-' && c != '.')
{
return Err(ErrMode::Cut(
CompilationError::fatal(
SourceRange::new(path.start, path.end, path.module_id),
"import path may only contain alphanumeric characters, underscore, hyphen, and period. Files in other directories are not yet supported.",
)
.into(),
));
}
if let ImportSelector::None { alias: ref mut a } = selector { if let ImportSelector::None {
alias: ref mut selector_alias,
} = selector
{
if let Some(alias) = opt(preceded( if let Some(alias) = opt(preceded(
(whitespace, import_as_keyword, whitespace), (whitespace, import_as_keyword, whitespace),
identifier.context(expected("an identifier to alias the import")), identifier.context(expected("an identifier to alias the import")),
@ -1579,35 +1560,40 @@ fn import_stmt(i: &mut TokenSlice) -> PResult<BoxNode<ImportStatement>> {
.parse_next(i)? .parse_next(i)?
{ {
end = alias.end; end = alias.end;
*a = Some(alias); *selector_alias = Some(alias);
} }
ParseContext::warn(CompilationError::err( ParseContext::warn(CompilationError::err(
SourceRange::new(start, path.end, path.module_id), SourceRange::new(start, path.end, path.module_id),
"Importing a whole module is experimental, likely to be buggy, and likely to change", "Importing a whole module is experimental, likely to be buggy, and likely to change",
)); ));
}
if a.is_none() let path_string = match path.inner.value {
&& (!path_string.ends_with(".kcl") LiteralValue::String(s) => s,
|| path_string.starts_with("_") _ => unreachable!(),
|| path_string.contains('-') };
|| path_string[0..path_string.len() - 4].contains('.')) let path = validate_path_string(
{ path_string,
return Err(ErrMode::Cut( selector.exposes_imported_name(),
CompilationError::fatal( SourceRange::new(path.start, path.end, path.module_id),
SourceRange::new(path.start, path.end, path.module_id), )?;
"import path is not a valid identifier and must be aliased.".to_owned(),
) if matches!(path, ImportPath::Foreign { .. }) && selector.imports_items() {
.into(), return Err(ErrMode::Cut(
)); CompilationError::fatal(
} SourceRange::new(start, end, module_id),
"individual items can only be imported from KCL files",
)
.into(),
));
} }
Ok(Node::boxed( Ok(Node::boxed(
ImportStatement { ImportStatement {
selector, selector,
visibility, visibility,
path: path_string, path,
digest: None, digest: None,
}, },
start, start,
@ -1616,6 +1602,72 @@ fn import_stmt(i: &mut TokenSlice) -> PResult<BoxNode<ImportStatement>> {
)) ))
} }
const FOREIGN_IMPORT_EXTENSIONS: [&str; 8] = ["fbx", "gltf", "glb", "obj", "ply", "sldprt", "step", "stl"];
/// Validates the path string in an `import` statement.
///
/// `var_name` is `true` if the path will be used as a variable name.
fn validate_path_string(path_string: String, var_name: bool, path_range: SourceRange) -> PResult<ImportPath> {
if path_string.is_empty() {
return Err(ErrMode::Cut(
CompilationError::fatal(path_range, "import path cannot be empty").into(),
));
}
if var_name
&& (path_string.starts_with("_")
|| path_string.contains('-')
|| path_string.chars().filter(|c| *c == '.').count() > 1)
{
return Err(ErrMode::Cut(
CompilationError::fatal(path_range, "import path is not a valid identifier and must be aliased.").into(),
));
}
let path = if path_string.ends_with(".kcl") {
if path_string
.chars()
.any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-' && c != '.')
{
return Err(ErrMode::Cut(
CompilationError::fatal(
path_range,
"import path may only contain alphanumeric characters, underscore, hyphen, and period. KCL files in other directories are not yet supported.",
)
.into(),
));
}
ImportPath::Kcl { filename: path_string }
} else if path_string.starts_with("std") {
ParseContext::warn(CompilationError::err(
path_range,
"explicit imports from the standard library are experimental, likely to be buggy, and likely to change.",
));
ImportPath::Std
} else if path_string.contains('.') {
let extn = &path_string[path_string.rfind('.').unwrap() + 1..];
if !FOREIGN_IMPORT_EXTENSIONS.contains(&extn) {
ParseContext::warn(CompilationError::err(
path_range,
format!("unsupported import path format. KCL files can be imported from the current project, CAD files with the following formats are supported: {}", FOREIGN_IMPORT_EXTENSIONS.join(", ")),
))
}
ImportPath::Foreign { path: path_string }
} else {
return Err(ErrMode::Cut(
CompilationError::fatal(
path_range,
format!("unsupported import path format. KCL files can be imported from the current project, CAD files with the following formats are supported: {}", FOREIGN_IMPORT_EXTENSIONS.join(", ")),
)
.into(),
));
};
Ok(path)
}
fn import_item(i: &mut TokenSlice) -> PResult<Node<ImportItem>> { fn import_item(i: &mut TokenSlice) -> PResult<Node<ImportItem>> {
let name = nameable_identifier let name = nameable_identifier
.context(expected("an identifier to import")) .context(expected("an identifier to import"))
@ -3611,7 +3663,11 @@ mySk1 = startSketchAt([0, 0])"#;
fn assert_err(p: &str, msg: &str, src_expected: [usize; 2]) { fn assert_err(p: &str, msg: &str, src_expected: [usize; 2]) {
let result = crate::parsing::top_level_parse(p); let result = crate::parsing::top_level_parse(p);
let err = result.unwrap_errs().next().unwrap(); let err = result.unwrap_errs().next().unwrap();
assert_eq!(err.message, msg); assert!(
err.message.starts_with(msg),
"Found `{}`, expected `{msg}`",
err.message
);
let src_actual = [err.source_range.start(), err.source_range.end()]; let src_actual = [err.source_range.start(), err.source_range.end()];
assert_eq!( assert_eq!(
src_expected, src_expected,
@ -3977,7 +4033,7 @@ e
fn bad_imports() { fn bad_imports() {
assert_err( assert_err(
r#"import cube from "../cube.kcl""#, r#"import cube from "../cube.kcl""#,
"import path may only contain alphanumeric characters, underscore, hyphen, and period. Files in other directories are not yet supported.", "import path may only contain alphanumeric characters, underscore, hyphen, and period. KCL files in other directories are not yet supported.",
[17, 30], [17, 30],
); );
assert_err( assert_err(
@ -3985,17 +4041,21 @@ e
"as is not the 'from' keyword", "as is not the 'from' keyword",
[9, 11], [9, 11],
); );
assert_err(r#"import a from "dsfs" as b"#, "Unexpected token: as", [21, 23]); assert_err(
assert_err(r#"import * from "dsfs" as b"#, "Unexpected token: as", [21, 23]); r#"import a from "dsfs" as b"#,
"unsupported import path format",
[14, 20],
);
assert_err(
r#"import * from "dsfs" as b"#,
"unsupported import path format",
[14, 20],
);
assert_err(r#"import a from b"#, "invalid string literal", [14, 15]); assert_err(r#"import a from b"#, "invalid string literal", [14, 15]);
assert_err(r#"import * "dsfs""#, "\"dsfs\" is not the 'from' keyword", [9, 15]); assert_err(r#"import * "dsfs""#, "\"dsfs\" is not the 'from' keyword", [9, 15]);
assert_err(r#"import from "dsfs""#, "\"dsfs\" is not the 'from' keyword", [12, 18]); assert_err(r#"import from "dsfs""#, "\"dsfs\" is not the 'from' keyword", [12, 18]);
assert_err(r#"import "dsfs.kcl" as *"#, "Unexpected token: as", [18, 20]); assert_err(r#"import "dsfs.kcl" as *"#, "Unexpected token: as", [18, 20]);
assert_err( assert_err(r#"import "dsfs""#, "unsupported import path format", [7, 13]);
r#"import "dsfs""#,
"import path is not a valid identifier and must be aliased.",
[7, 13],
);
assert_err( assert_err(
r#"import "foo.bar.kcl""#, r#"import "foo.bar.kcl""#,
"import path is not a valid identifier and must be aliased.", "import path is not a valid identifier and must be aliased.",
@ -4017,7 +4077,19 @@ e
fn warn_import() { fn warn_import() {
let some_program_string = r#"import "foo.kcl""#; let some_program_string = r#"import "foo.kcl""#;
let (_, errs) = assert_no_err(some_program_string); let (_, errs) = assert_no_err(some_program_string);
assert_eq!(errs.len(), 1); assert_eq!(errs.len(), 1, "{errs:#?}");
let some_program_string = r#"import "foo.obj""#;
let (_, errs) = assert_no_err(some_program_string);
assert_eq!(errs.len(), 1, "{errs:#?}");
let some_program_string = r#"import "foo.sldprt""#;
let (_, errs) = assert_no_err(some_program_string);
assert_eq!(errs.len(), 1, "{errs:#?}");
let some_program_string = r#"import "foo.bad""#;
let (_, errs) = assert_no_err(some_program_string);
assert_eq!(errs.len(), 2, "{errs:#?}");
} }
#[test] #[test]

View File

@ -62,14 +62,14 @@ impl FromStr for NumericSuffix {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"_" => Ok(NumericSuffix::Count), "_" => Ok(NumericSuffix::Count),
"mm" => Ok(NumericSuffix::Mm), "mm" | "millimeters" => Ok(NumericSuffix::Mm),
"cm" => Ok(NumericSuffix::Cm), "cm" | "centimeters" => Ok(NumericSuffix::Cm),
"m" => Ok(NumericSuffix::M), "m" | "meters" => Ok(NumericSuffix::M),
"inch" | "in" => Ok(NumericSuffix::Inch), "inch" | "in" => Ok(NumericSuffix::Inch),
"ft" => Ok(NumericSuffix::Ft), "ft" | "feet" => Ok(NumericSuffix::Ft),
"yd" => Ok(NumericSuffix::Yd), "yd" | "yards" => Ok(NumericSuffix::Yd),
"deg" => Ok(NumericSuffix::Deg), "deg" | "degrees" => Ok(NumericSuffix::Deg),
"rad" => Ok(NumericSuffix::Rad), "rad" | "radians" => Ok(NumericSuffix::Rad),
_ => Err(CompilationError::err(SourceRange::default(), "invalid unit of measure")), _ => Err(CompilationError::err(SourceRange::default(), "invalid unit of measure")),
} }
} }

View File

@ -87,7 +87,7 @@ async fn execute(test_name: &str, render_to_png: bool) {
let exec_res = crate::test_server::execute_and_snapshot_ast( let exec_res = crate::test_server::execute_and_snapshot_ast(
ast.into(), ast.into(),
crate::settings::types::UnitLength::Mm, crate::settings::types::UnitLength::Mm,
Some(Path::new("tests").join(test_name)), Some(Path::new("tests").join(test_name).join("input.kcl").to_owned()),
) )
.await; .await;
match exec_res { match exec_res {
@ -767,6 +767,27 @@ mod import_cycle1 {
super::execute(TEST_NAME, false).await super::execute(TEST_NAME, false).await
} }
} }
mod import_function_not_sketch {
const TEST_NAME: &str = "import_function_not_sketch";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, true).await
}
}
mod import_constant { mod import_constant {
const TEST_NAME: &str = "import_constant"; const TEST_NAME: &str = "import_constant";
@ -872,6 +893,27 @@ mod import_side_effect {
super::execute(TEST_NAME, false).await super::execute(TEST_NAME, false).await
} }
} }
mod import_foreign {
const TEST_NAME: &str = "import_foreign";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod array_elem_push_fail { mod array_elem_push_fail {
const TEST_NAME: &str = "array_elem_push_fail"; const TEST_NAME: &str = "array_elem_push_fail";

View File

@ -143,10 +143,8 @@ async fn inner_chamfer(
radius: LengthUnit(data.length), radius: LengthUnit(data.length),
tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future. tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future.
cut_type: CutType::Chamfer, cut_type: CutType::Chamfer,
// We pass in the command id as the face id. // We make this a none so that we can remove it in the future.
// So the resulting face of the fillet will be the same. face_id: None,
// This is because that's how most other endpoints work.
face_id: Some(id),
}), }),
) )
.await?; .await?;

View File

@ -152,10 +152,8 @@ async fn inner_fillet(
radius: LengthUnit(data.radius), radius: LengthUnit(data.radius),
tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))), tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
cut_type: CutType::Fillet, cut_type: CutType::Fillet,
// We pass in the command id as the face id. // We make this a none so that we can remove it in the future.
// So the resulting face of the fillet will be the same. face_id: None,
// This is because that's how most other endpoints work.
face_id: Some(id),
}), }),
) )
.await?; .await?;

View File

@ -1,44 +1,16 @@
//! Standard library functions involved in importing files. //! Standard library functions involved in importing files.
use std::str::FromStr;
use anyhow::Result; use anyhow::Result;
use derive_docs::stdlib; use derive_docs::stdlib;
use kcmc::{ use kcmc::{coord::System, format::InputFormat, units::UnitLength};
coord::{Axis, AxisDirectionPair, Direction, System},
each_cmd as mcmd,
format::InputFormat,
ok_response::OkModelingCmdResponse,
shared::FileImportFormat,
units::UnitLength,
websocket::OkWebSocketResponseData,
ImportFile, ModelingCmd,
};
use kittycad_modeling_cmds as kcmc; use kittycad_modeling_cmds as kcmc;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ExecState, ImportedGeometry, KclValue}, execution::{import_foreign, send_import_to_engine, ExecState, ImportedGeometry, KclValue, ZOO_COORD_SYSTEM},
fs::FileSystem,
std::Args, std::Args,
}; };
// Zoo co-ordinate system.
//
// * Forward: -Y
// * Up: +Z
// * Handedness: Right
const ZOO_COORD_SYSTEM: System = System {
forward: AxisDirectionPair {
axis: Axis::Y,
direction: Direction::Negative,
},
up: AxisDirectionPair {
axis: Axis::Z,
direction: Direction::Positive,
},
};
/// Import format specifier /// Import format specifier
#[derive(serde :: Serialize, serde :: Deserialize, PartialEq, Debug, Clone, schemars :: JsonSchema)] #[derive(serde :: Serialize, serde :: Deserialize, PartialEq, Debug, Clone, schemars :: JsonSchema)]
#[cfg_attr(feature = "tabled", derive(tabled::Tabled))] #[cfg_attr(feature = "tabled", derive(tabled::Tabled))]
@ -135,6 +107,8 @@ pub async fn import(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// Import a CAD file. /// Import a CAD file.
/// ///
/// **DEPRECATED** Prefer to use import statements.
///
/// For formats lacking unit data (such as STL, OBJ, or PLY files), the default /// For formats lacking unit data (such as STL, OBJ, or PLY files), the default
/// unit of measurement is millimeters. Alternatively you may specify the unit /// unit of measurement is millimeters. Alternatively you may specify the unit
/// by passing your desired measurement unit in the options parameter. When /// by passing your desired measurement unit in the options parameter. When
@ -144,9 +118,6 @@ pub async fn import(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// Note: The import command currently only works when using the native /// Note: The import command currently only works when using the native
/// Modeling App. /// Modeling App.
/// ///
/// For importing KCL functions using the `import` statement, see the docs on
/// [KCL modules](/docs/kcl/modules).
///
/// ```no_run /// ```no_run
/// model = import("tests/inputs/cube.obj") /// model = import("tests/inputs/cube.obj")
/// ``` /// ```
@ -178,6 +149,7 @@ pub async fn import(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
#[stdlib { #[stdlib {
name = "import", name = "import",
feature_tree_operation = true, feature_tree_operation = true,
deprecated = true,
tags = [], tags = [],
}] }]
async fn inner_import( async fn inner_import(
@ -193,232 +165,17 @@ async fn inner_import(
})); }));
} }
// Make sure the file exists. let format = options.map(InputFormat::from);
if !args.ctx.fs.exists(&file_path, args.source_range).await? { send_import_to_engine(
return Err(KclError::Semantic(KclErrorDetails { import_foreign(
message: format!("File `{}` does not exist.", file_path), std::path::Path::new(&file_path),
source_ranges: vec![args.source_range], format,
})); exec_state,
} &args.ctx,
args.source_range,
let ext_format = get_import_format_from_extension(file_path.split('.').last().ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("No file extension found for `{}`", file_path),
source_ranges: vec![args.source_range],
})
})?)
.map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![args.source_range],
})
})?;
// Get the format type from the extension of the file.
let format = if let Some(options) = options {
// Validate the given format with the extension format.
let format: InputFormat = options.into();
validate_extension_format(ext_format, format.clone()).map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![args.source_range],
})
})?;
format
} else {
ext_format
};
// Get the file contents for each file path.
let file_contents = args.ctx.fs.read(&file_path, args.source_range).await.map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![args.source_range],
})
})?;
// We want the file_path to be without the parent.
let file_name = std::path::Path::new(&file_path)
.file_name()
.map(|p| p.to_string_lossy().to_string())
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Could not get the file name from the path `{}`", file_path),
source_ranges: vec![args.source_range],
})
})?;
let mut import_files = vec![kcmc::ImportFile {
path: file_name.to_string(),
data: file_contents.clone(),
}];
// In the case of a gltf importing a bin file we need to handle that! and figure out where the
// file is relative to our current file.
if let InputFormat::Gltf(..) = format {
// Check if the file is a binary gltf file, in that case we don't need to import the bin
// file.
if !file_contents.starts_with(b"glTF") {
let json = gltf_json::Root::from_slice(&file_contents).map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![args.source_range],
})
})?;
// Read the gltf file and check if there is a bin file.
for buffer in json.buffers.iter() {
if let Some(uri) = &buffer.uri {
if !uri.starts_with("data:") {
// We want this path relative to the file_path given.
let bin_path = std::path::Path::new(&file_path)
.parent()
.map(|p| p.join(uri))
.map(|p| p.to_string_lossy().to_string())
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Could not get the parent path of the file `{}`", file_path),
source_ranges: vec![args.source_range],
})
})?;
let bin_contents = args.ctx.fs.read(&bin_path, args.source_range).await.map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: e.to_string(),
source_ranges: vec![args.source_range],
})
})?;
import_files.push(ImportFile {
path: uri.to_string(),
data: bin_contents,
});
}
}
}
}
}
if args.ctx.is_mock() {
return Ok(ImportedGeometry {
id: exec_state.next_uuid(),
value: import_files.iter().map(|f| f.path.to_string()).collect(),
meta: vec![args.source_range.into()],
});
}
let id = exec_state.next_uuid();
let resp = args
.send_modeling_cmd(
id,
ModelingCmd::from(mcmd::ImportFiles {
files: import_files.clone(),
format,
}),
) )
.await?; .await?,
&args.ctx,
let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::ImportFiles(imported_files),
} = &resp
else {
return Err(KclError::Engine(KclErrorDetails {
message: format!("ImportFiles response was not as expected: {:?}", resp),
source_ranges: vec![args.source_range],
}));
};
Ok(ImportedGeometry {
id: imported_files.object_id,
value: import_files.iter().map(|f| f.path.to_string()).collect(),
meta: vec![args.source_range.into()],
})
}
/// Get the source format from the extension.
fn get_import_format_from_extension(ext: &str) -> Result<InputFormat> {
let format = match FileImportFormat::from_str(ext) {
Ok(format) => format,
Err(_) => {
if ext == "stp" {
FileImportFormat::Step
} else if ext == "glb" {
FileImportFormat::Gltf
} else {
anyhow::bail!("unknown source format for file extension: {}. Try setting the `--src-format` flag explicitly or use a valid format.", ext)
}
}
};
// Make the default units millimeters.
let ul = UnitLength::Millimeters;
// Zoo co-ordinate system.
//
// * Forward: -Y
// * Up: +Z
// * Handedness: Right
match format {
FileImportFormat::Step => Ok(InputFormat::Step(kcmc::format::step::import::Options {
split_closed_faces: false,
})),
FileImportFormat::Stl => Ok(InputFormat::Stl(kcmc::format::stl::import::Options {
coords: ZOO_COORD_SYSTEM,
units: ul,
})),
FileImportFormat::Obj => Ok(InputFormat::Obj(kcmc::format::obj::import::Options {
coords: ZOO_COORD_SYSTEM,
units: ul,
})),
FileImportFormat::Gltf => Ok(InputFormat::Gltf(kcmc::format::gltf::import::Options {})),
FileImportFormat::Ply => Ok(InputFormat::Ply(kcmc::format::ply::import::Options {
coords: ZOO_COORD_SYSTEM,
units: ul,
})),
FileImportFormat::Fbx => Ok(InputFormat::Fbx(kcmc::format::fbx::import::Options {})),
FileImportFormat::Sldprt => Ok(InputFormat::Sldprt(kcmc::format::sldprt::import::Options {
split_closed_faces: false,
})),
}
}
fn validate_extension_format(ext: InputFormat, given: InputFormat) -> Result<()> {
if let InputFormat::Stl(_) = ext {
if let InputFormat::Stl(_) = given {
return Ok(());
}
}
if let InputFormat::Obj(_) = ext {
if let InputFormat::Obj(_) = given {
return Ok(());
}
}
if let InputFormat::Ply(_) = ext {
if let InputFormat::Ply(_) = given {
return Ok(());
}
}
if ext == given {
return Ok(());
}
anyhow::bail!(
"The given format does not match the file extension. Expected: `{}`, Given: `{}`",
get_name_of_format(ext),
get_name_of_format(given)
) )
} .await
fn get_name_of_format(type_: InputFormat) -> &'static str {
match type_ {
InputFormat::Fbx(_) => "fbx",
InputFormat::Gltf(_) => "gltf",
InputFormat::Obj(_) => "obj",
InputFormat::Ply(_) => "ply",
InputFormat::Sldprt(_) => "sldprt",
InputFormat::Step(_) => "step",
InputFormat::Stl(_) => "stl",
}
} }

View File

@ -3,14 +3,13 @@
use anyhow::Result; use anyhow::Result;
use derive_docs::stdlib; use derive_docs::stdlib;
use super::args::FromArgs;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue}, execution::{ExecState, KclValue},
std::Args, std::Args,
}; };
use super::args::FromArgs;
/// Compute the remainder after dividing `num` by `div`. /// Compute the remainder after dividing `num` by `div`.
/// If `num` is negative, the result will be too. /// If `num` is negative, the result will be too.
pub async fn rem(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> { pub async fn rem(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {

View File

@ -11,12 +11,11 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::execution::{Artifact, ArtifactId};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
BasePath, ExecState, Face, GeoMeta, KclValue, Path, Plane, Point2d, Point3d, Sketch, SketchSet, SketchSurface, Artifact, ArtifactId, BasePath, ExecState, Face, GeoMeta, KclValue, Path, Plane, Point2d, Point3d, Sketch,
Solid, TagEngineInfo, TagIdentifier, SketchSet, SketchSurface, Solid, TagEngineInfo, TagIdentifier,
}, },
parsing::ast::types::TagNode, parsing::ast::types::TagNode,
std::{ std::{
@ -2250,7 +2249,10 @@ mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use crate::{execution::TagIdentifier, std::sketch::PlaneData, std::utils::calculate_circle_center}; use crate::{
execution::TagIdentifier,
std::{sketch::PlaneData, utils::calculate_circle_center},
};
#[test] #[test]
fn test_deserialize_plane_data() { fn test_deserialize_plane_data() {

View File

@ -21,9 +21,9 @@ pub struct RequestBody {
pub async fn execute_and_snapshot( pub async fn execute_and_snapshot(
code: &str, code: &str,
units: UnitLength, units: UnitLength,
project_directory: Option<PathBuf>, current_file: Option<PathBuf>,
) -> Result<image::DynamicImage, ExecError> { ) -> Result<image::DynamicImage, ExecError> {
let ctx = new_context(units, true, project_directory).await?; let ctx = new_context(units, true, current_file).await?;
let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?; let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
let res = do_execute_and_snapshot(&ctx, program) let res = do_execute_and_snapshot(&ctx, program)
.await .await
@ -38,9 +38,9 @@ pub async fn execute_and_snapshot(
pub async fn execute_and_snapshot_ast( pub async fn execute_and_snapshot_ast(
ast: Program, ast: Program,
units: UnitLength, units: UnitLength,
project_directory: Option<PathBuf>, current_file: Option<PathBuf>,
) -> Result<(ExecState, image::DynamicImage), ExecErrorWithState> { ) -> Result<(ExecState, image::DynamicImage), ExecErrorWithState> {
let ctx = new_context(units, true, project_directory).await?; let ctx = new_context(units, true, current_file).await?;
let res = do_execute_and_snapshot(&ctx, ast).await; let res = do_execute_and_snapshot(&ctx, ast).await;
ctx.close().await; ctx.close().await;
res res
@ -49,9 +49,9 @@ pub async fn execute_and_snapshot_ast(
pub async fn execute_and_snapshot_no_auth( pub async fn execute_and_snapshot_no_auth(
code: &str, code: &str,
units: UnitLength, units: UnitLength,
project_directory: Option<PathBuf>, current_file: Option<PathBuf>,
) -> Result<image::DynamicImage, ExecError> { ) -> Result<image::DynamicImage, ExecError> {
let ctx = new_context(units, false, project_directory).await?; let ctx = new_context(units, false, current_file).await?;
let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?; let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
let res = do_execute_and_snapshot(&ctx, program) let res = do_execute_and_snapshot(&ctx, program)
.await .await
@ -88,7 +88,7 @@ async fn do_execute_and_snapshot(
pub async fn new_context( pub async fn new_context(
units: UnitLength, units: UnitLength,
with_auth: bool, with_auth: bool,
project_directory: Option<PathBuf>, current_file: Option<PathBuf>,
) -> Result<ExecutorContext, ConnectionError> { ) -> Result<ExecutorContext, ConnectionError> {
let mut client = new_zoo_client(if with_auth { None } else { Some("bad_token".to_string()) }, None) let mut client = new_zoo_client(if with_auth { None } else { Some("bad_token".to_string()) }, None)
.map_err(ConnectionError::CouldNotMakeClient)?; .map_err(ConnectionError::CouldNotMakeClient)?;
@ -99,18 +99,20 @@ pub async fn new_context(
client.set_base_url("https://api.zoo.dev".to_string()); client.set_base_url("https://api.zoo.dev".to_string());
} }
let ctx = ExecutorContext::new( let mut settings = ExecutorSettings {
&client, units,
ExecutorSettings { highlight_edges: true,
units, enable_ssao: false,
highlight_edges: true, show_grid: false,
enable_ssao: false, replay: None,
show_grid: false, project_directory: None,
replay: None, current_file: None,
project_directory, };
}, if let Some(current_file) = current_file {
) settings.with_current_file(current_file);
.await }
.map_err(ConnectionError::Establishing)?; let ctx = ExecutorContext::new(&client, settings)
.await
.map_err(ConnectionError::Establishing)?;
Ok(ctx) Ok(ctx)
} }

View File

@ -8,6 +8,7 @@ use crate::parsing::{
LiteralValue, MemberExpression, MemberObject, Node, NonCodeNode, NonCodeValue, ObjectExpression, Parameter, LiteralValue, MemberExpression, MemberObject, Node, NonCodeNode, NonCodeValue, ObjectExpression, Parameter,
PipeExpression, Program, TagDeclarator, UnaryExpression, VariableDeclaration, VariableKind, PipeExpression, Program, TagDeclarator, UnaryExpression, VariableDeclaration, VariableKind,
}, },
token::NumericSuffix,
PIPE_OPERATOR, PIPE_OPERATOR,
}; };
@ -370,6 +371,11 @@ impl VariableDeclaration {
} }
} }
// Used by TS.
pub fn format_number(value: f64, suffix: NumericSuffix) -> String {
format!("{value}{suffix}")
}
impl Literal { impl Literal {
fn recast(&self) -> String { fn recast(&self) -> String {
match self.value { match self.value {

View File

@ -661,8 +661,7 @@ snapshot_kind: text
"edge_id": "[uuid]", "edge_id": "[uuid]",
"radius": 5.0, "radius": 5.0,
"tolerance": 0.0000001, "tolerance": 0.0000001,
"cut_type": "fillet", "cut_type": "fillet"
"face_id": "[uuid]"
} }
}, },
{ {

View File

@ -50,32 +50,6 @@ mindmap
Sweep Extrusion Sweep Extrusion
Wall Wall
Wall Wall
Path
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Sweep Extrusion
Wall
Wall
Wall
Cap End
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
Solid2d
Wall Wall
Wall Wall
Cap Start Cap Start

View File

@ -72,32 +72,6 @@ mindmap
Wall Wall
Wall Wall
Wall Wall
Path
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Sweep Extrusion
Wall
Wall
Wall
Cap End
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
Solid2d
Cap End Cap End
SweepEdge Opposite SweepEdge Opposite
SweepEdge Adjacent SweepEdge Adjacent
@ -186,32 +160,6 @@ mindmap
Wall Wall
Wall Wall
Wall Wall
Path
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Sweep Extrusion
Wall
Wall
Wall
Cap End
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
Solid2d
Cap End Cap End
SweepEdge Opposite SweepEdge Opposite
SweepEdge Adjacent SweepEdge Adjacent
@ -282,32 +230,6 @@ mindmap
Wall Wall
Wall Wall
Wall Wall
Path
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Wall
SweepEdge Opposite
SweepEdge Adjacent
Segment
Sweep Extrusion
Wall
Wall
Wall
Cap End
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
SweepEdge Opposite
SweepEdge Adjacent
Solid2d
Cap End Cap End
SweepEdge Opposite SweepEdge Opposite
SweepEdge Adjacent SweepEdge Adjacent

View File

@ -654,8 +654,7 @@ snapshot_kind: text
"edge_id": "[uuid]", "edge_id": "[uuid]",
"radius": 2.0, "radius": 2.0,
"tolerance": 0.0000001, "tolerance": 0.0000001,
"cut_type": "fillet", "cut_type": "fillet"
"face_id": "[uuid]"
} }
}, },
{ {
@ -671,8 +670,7 @@ snapshot_kind: text
"edge_id": "[uuid]", "edge_id": "[uuid]",
"radius": 2.0, "radius": 2.0,
"tolerance": 0.0000001, "tolerance": 0.0000001,
"cut_type": "fillet", "cut_type": "fillet"
"face_id": "[uuid]"
} }
} }
] ]

View File

@ -654,8 +654,7 @@ snapshot_kind: text
"edge_id": "[uuid]", "edge_id": "[uuid]",
"radius": 2.0, "radius": 2.0,
"tolerance": 0.0000001, "tolerance": 0.0000001,
"cut_type": "fillet", "cut_type": "fillet"
"face_id": "[uuid]"
} }
}, },
{ {
@ -671,8 +670,7 @@ snapshot_kind: text
"edge_id": "[uuid]", "edge_id": "[uuid]",
"radius": 2.0, "radius": 2.0,
"tolerance": 0.0000001, "tolerance": 0.0000001,
"cut_type": "fillet", "cut_type": "fillet"
"face_id": "[uuid]"
} }
} }
] ]

View File

@ -654,8 +654,7 @@ snapshot_kind: text
"edge_id": "[uuid]", "edge_id": "[uuid]",
"radius": 2.0, "radius": 2.0,
"tolerance": 0.0000001, "tolerance": 0.0000001,
"cut_type": "fillet", "cut_type": "fillet"
"face_id": "[uuid]"
} }
} }
] ]

View File

@ -654,8 +654,7 @@ snapshot_kind: text
"edge_id": "[uuid]", "edge_id": "[uuid]",
"radius": 2.0, "radius": 2.0,
"tolerance": 0.0000001, "tolerance": 0.0000001,
"cut_type": "fillet", "cut_type": "fillet"
"face_id": "[uuid]"
} }
} }
] ]

View File

@ -640,8 +640,7 @@ snapshot_kind: text
"edge_id": "[uuid]", "edge_id": "[uuid]",
"radius": 2.0, "radius": 2.0,
"tolerance": 0.0000001, "tolerance": 0.0000001,
"cut_type": "fillet", "cut_type": "fillet"
"face_id": "[uuid]"
} }
}, },
{ {
@ -657,8 +656,7 @@ snapshot_kind: text
"edge_id": "[uuid]", "edge_id": "[uuid]",
"radius": 2.0, "radius": 2.0,
"tolerance": 0.0000001, "tolerance": 0.0000001,
"cut_type": "fillet", "cut_type": "fillet"
"face_id": "[uuid]"
} }
} }
] ]

View File

@ -2487,8 +2487,7 @@ snapshot_kind: text
"edge_id": "[uuid]", "edge_id": "[uuid]",
"radius": 1.0, "radius": 1.0,
"tolerance": 0.0000001, "tolerance": 0.0000001,
"cut_type": "fillet", "cut_type": "fillet"
"face_id": "[uuid]"
} }
}, },
{ {
@ -2504,8 +2503,7 @@ snapshot_kind: text
"edge_id": "[uuid]", "edge_id": "[uuid]",
"radius": 1.0, "radius": 1.0,
"tolerance": 0.0000001, "tolerance": 0.0000001,
"cut_type": "fillet", "cut_type": "fillet"
"face_id": "[uuid]"
} }
}, },
{ {
@ -2521,8 +2519,7 @@ snapshot_kind: text
"edge_id": "[uuid]", "edge_id": "[uuid]",
"radius": 1.0, "radius": 1.0,
"tolerance": 0.0000001, "tolerance": 0.0000001,
"cut_type": "fillet", "cut_type": "fillet"
"face_id": "[uuid]"
} }
}, },
{ {
@ -2538,8 +2535,7 @@ snapshot_kind: text
"edge_id": "[uuid]", "edge_id": "[uuid]",
"radius": 1.0, "radius": 1.0,
"tolerance": 0.0000001, "tolerance": 0.0000001,
"cut_type": "fillet", "cut_type": "fillet"
"face_id": "[uuid]"
} }
}, },
{ {

View File

@ -1,13 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Error from executing fillet-and-shell.kcl
snapshot_kind: text
---
KCL Engine error
× engine: Modeling command failed: websocket closed early
╭─[1:1]
1 │ rpizWidth = 30
· ▲
2 │ rpizLength = 65
╰────

View File

@ -7,7 +7,10 @@ description: Result of parsing import_constant.kcl
"body": [ "body": [
{ {
"end": 39, "end": 39,
"path": "export_constant.kcl", "path": {
"type": "Kcl",
"filename": "export_constant.kcl"
},
"selector": { "selector": {
"type": "List", "type": "List",
"items": [ "items": [

View File

@ -1,41 +1,45 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl/src/simulation_tests.rs
description: Result of parsing import_cycle1.kcl description: Result of parsing import_cycle1.kcl
snapshot_kind: text
--- ---
{ {
"Ok": { "Ok": {
"body": [ "body": [
{ {
"end": 35, "end": 69,
"path": "import_cycle2.kcl", "path": {
"type": "Kcl",
"filename": "import_cycle2.kcl"
},
"selector": { "selector": {
"type": "List", "type": "List",
"items": [ "items": [
{ {
"alias": null, "alias": null,
"end": 10, "end": 44,
"name": { "name": {
"end": 10, "end": 44,
"name": "two", "name": "two",
"start": 7, "start": 41,
"type": "Identifier" "type": "Identifier"
}, },
"start": 7, "start": 41,
"type": "ImportItem" "type": "ImportItem"
} }
] ]
}, },
"start": 0, "start": 34,
"type": "ImportStatement", "type": "ImportStatement",
"type": "ImportStatement" "type": "ImportStatement"
}, },
{ {
"declaration": { "declaration": {
"end": 75, "end": 109,
"id": { "id": {
"end": 50, "end": 84,
"name": "one", "name": "one",
"start": 47, "start": 81,
"type": "Identifier" "type": "Identifier"
}, },
"init": { "init": {
@ -43,25 +47,25 @@ description: Result of parsing import_cycle1.kcl
"body": [ "body": [
{ {
"argument": { "argument": {
"end": 73, "end": 107,
"left": { "left": {
"arguments": [], "arguments": [],
"callee": { "callee": {
"end": 67, "end": 101,
"name": "two", "name": "two",
"start": 64, "start": 98,
"type": "Identifier" "type": "Identifier"
}, },
"end": 69, "end": 103,
"start": 64, "start": 98,
"type": "CallExpression", "type": "CallExpression",
"type": "CallExpression" "type": "CallExpression"
}, },
"operator": "-", "operator": "-",
"right": { "right": {
"end": 73, "end": 107,
"raw": "1", "raw": "1",
"start": 72, "start": 106,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": { "value": {
@ -69,43 +73,43 @@ description: Result of parsing import_cycle1.kcl
"suffix": "None" "suffix": "None"
} }
}, },
"start": 64, "start": 98,
"type": "BinaryExpression", "type": "BinaryExpression",
"type": "BinaryExpression" "type": "BinaryExpression"
}, },
"end": 73, "end": 107,
"start": 57, "start": 91,
"type": "ReturnStatement", "type": "ReturnStatement",
"type": "ReturnStatement" "type": "ReturnStatement"
} }
], ],
"end": 75, "end": 109,
"start": 53 "start": 87
}, },
"end": 75, "end": 109,
"params": [], "params": [],
"start": 50, "start": 84,
"type": "FunctionExpression", "type": "FunctionExpression",
"type": "FunctionExpression" "type": "FunctionExpression"
}, },
"start": 47, "start": 81,
"type": "VariableDeclarator" "type": "VariableDeclarator"
}, },
"end": 75, "end": 109,
"kind": "fn", "kind": "fn",
"start": 37, "start": 71,
"type": "VariableDeclaration", "type": "VariableDeclaration",
"type": "VariableDeclaration", "type": "VariableDeclaration",
"visibility": "export" "visibility": "export"
} }
], ],
"end": 76, "end": 110,
"nonCodeMeta": { "nonCodeMeta": {
"nonCodeNodes": { "nonCodeNodes": {
"0": [ "0": [
{ {
"end": 37, "end": 71,
"start": 35, "start": 69,
"type": "NonCodeNode", "type": "NonCodeNode",
"value": { "value": {
"type": "newLine" "type": "newLine"
@ -113,7 +117,42 @@ description: Result of parsing import_cycle1.kcl
} }
] ]
}, },
"startNodes": [] "startNodes": [
{
"end": 33,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "annotation",
"name": {
"end": 9,
"name": "settings",
"start": 1,
"type": "Identifier"
},
"properties": [
{
"end": 32,
"key": {
"end": 27,
"name": "defaultLengthUnit",
"start": 10,
"type": "Identifier"
},
"start": 10,
"type": "ObjectProperty",
"value": {
"end": 32,
"name": "in",
"start": 30,
"type": "Identifier",
"type": "Identifier"
}
}
]
}
}
]
}, },
"start": 0 "start": 0
} }

View File

@ -1,14 +1,16 @@
--- ---
source: kcl/src/simulation_tests.rs source: kcl/src/simulation_tests.rs
description: Error from executing import_cycle1.kcl description: Error from executing import_cycle1.kcl
snapshot_kind: text
--- ---
KCL ImportCycle error KCL ImportCycle error
× import cycle: circular import of modules is not allowed: tests/ × import cycle: circular import of modules is not allowed: tests/
│ import_cycle1/import_cycle2.kcl -> tests/import_cycle1/import_cycle3.kcl │ import_cycle1/import_cycle2.kcl -> tests/import_cycle1/import_cycle3.kcl
│ -> tests/import_cycle1/input.kcl -> tests/import_cycle1/import_cycle2.kcl │ -> tests/import_cycle1/input.kcl
╭─[1:1] ╭─[2:1]
1 │ import two from "import_cycle2.kcl" 1 │ @settings(defaultLengthUnit = in)
2 │ import two from "import_cycle2.kcl"
· ─────────────────────────────────── · ───────────────────────────────────
2 3
╰──── ╰────

View File

@ -1,3 +1,4 @@
@settings(defaultLengthUnit = mm)
import three from "import_cycle3.kcl" import three from "import_cycle3.kcl"
export fn two = () => { return three() - 1 } export fn two = () => { return three() - 1 }

View File

@ -1,3 +1,4 @@
@settings(defaultLengthUnit = in)
import one from "input.kcl" import one from "input.kcl"
export fn three = () => { return one() + one() + one() } export fn three = () => { return one() + one() + one() }

View File

@ -1,3 +1,4 @@
@settings(defaultLengthUnit = in)
import two from "import_cycle2.kcl" import two from "import_cycle2.kcl"
export fn one() { export fn one() {

View File

@ -7,7 +7,10 @@ description: Result of parsing import_export.kcl
"body": [ "body": [
{ {
"end": 32, "end": 32,
"path": "export_1.kcl", "path": {
"type": "Kcl",
"filename": "export_1.kcl"
},
"selector": { "selector": {
"type": "List", "type": "List",
"items": [ "items": [

File diff suppressed because it is too large Load Diff

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