Compare commits

..

19 Commits

Author SHA1 Message Date
3dfc2c86e1 Revert "Fix isomorphic-copy to work on Windows (#5417)" (#5424)
This reverts commit 18d87b99bd.
2025-02-19 11:31:14 -05:00
71647ede29 Release KCL 38 (#5421) 2025-02-19 10:26:02 -05:00
cd679f4be3 Fix units not getting set properly with whole module imports (#5418)
* Add test for whole modules with non-default units

* Update output files since adding test

* Fix to not drop batched commands due to isolated mode

* Update output after fix

* Update other sim test outputs
2025-02-19 16:11:11 +11:00
18d87b99bd Fix isomorphic-copy to work on Windows (#5417)
The current command uses the proper `copy` command but does not use
`\`'s, so it fails on my Windows machine.
2025-02-18 20:25:12 -05:00
70a2202877 change everything to rwlocks for thread safety (#5416)
* make everything in engine a rwlock and cleanup repetitive code

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>

* updates

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>

* docs

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-02-18 21:50:13 +00:00
f5c9f84ae9 test equipping tools mid tool use removes have baked expression (#5403)
test equiping tools mid tool use removes have baked expression
2025-02-19 07:45:44 +11:00
71f701dec7 Don't auto-hide menu bar on Windows and Linux (#5415)
This auto-hide behavior causes the bar to toggle visible and hidden when the user hits the <kbd>Alt</kbd> key, which can be quite often especially with Trackpad Friendly controls enabled.
2025-02-19 07:45:29 +11:00
bffbed1d42 Release KCL 37 (#5409)
* Use kcl-samples 'next' branch

* Release KCL 37
2025-02-18 12:29:34 -06:00
9f60ed8e75 Fix yarn lock after yarn install (#5408) 2025-02-18 12:38:28 -05:00
57b366b2d0 Unpin electron and electron-builder dependencies (#5230)
* Bump and unpin electron-builder and -updater version forward
Fixes #4505

* notarize: true

* Remove signingHashAlgorithms from win

* Fix signtoolOptions props after migration

* Disable branch build

* Bump more

* Add back CSC_FOR_PULL_REQUEST

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

* Another CSC_FOR_PULL_REQUEST

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

* Revert force prod changes

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-17 11:03:02 -05:00
6f3f5dbda9 Multi-second blank screen on second instance of the app (#5377)
* Multi-second blank screen on second instance of the app
Fixes #5346

* Add !IS_PLAYWRIGHT

* 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-02-15 09:28:58 -05:00
8dd25715fb Fix: E2E playwright test is permantely broken on localhost runtime on ubuntu. (#5391)
* fix: e2e test passes now

* fix: trying to unflake this test

* 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-02-14 15:49:06 -05:00
834f7133d8 Allow multiple profiles in the same sketch (#5196)
* Revert "Revert multi-profile (#4812)"

This reverts commit efe8089b08.

* fix poor 1000ms wait UX

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

* trigger CI

* Add Rust side artifacts for startSketchOn face or plane (#4834)

* Add Rust side artifacts for startSketchOn face or plane

* move ast digging

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>

* lint

* lint

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

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

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

* trigger CI

* chore: disabled file watcher which prevents faster file write (#4835)

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

* partial fixes

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

* Trigger CI

* Fix up all the tests

* Fix partial execution

* wip

* WIP

* wip

* rust changes to make three point confrom to same as others since we're not ready with name params yet

* most of the fix for 3 point circle

* get overlays working for circle three point

* fmt

* fix types

* cargo fmt

* add face codef ref for walls and caps

* fix sketch on face after updates to rust side artifact graph

* some things needed for multi-profile tests

* bad attempts at fixing rust

* more

* more

* fix rust

* more rust fixes

* overlay fix

* remove duplicate test

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

* lint and typing

* maybe fix a unit test

* small thing

* fix circ dep

* fix unit test

* fix some tests

* fix sweep point-and-click test

* fix more tests and add a fix me

* fix more tests

* fix electron specific test

* tsc

* more test tweaks

* update docs

* commint snaps?

* is clippy happy now?

* clippy again

* test works now without me changing anything big-fixed-itself

* small bug

* make three point have cross hair to make it consistent with othe rtools

* fix up state diagram

* fmt

* add draft point for first click of three point circ

* 1 test for three point circle

* 2 test for three point circle

* clean up

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

* remove bad doc comment

* remove test skip

* remove onboarding test changes

* Update src/lang/modifyAst.ts

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

* Update output from simulation tests

* Fix to use correct source ranges

This also reduces cloning.

* Change back to skipping face cap none and both

* Update output after changing back to skipping none and both

* Fix clippy warning

* fix profile start snap bug

* add path ids to cap

* fix going into edit sketch

* make other startSketchOn's work

* fix snapshot test

* explain function name

* Update src/lib/rectangleTool.ts

Co-authored-by: Frank Noirot <frank@zoo.dev>

* rename error

* remove file tree from diff

* Update src/clientSideScene/segments.ts

Co-authored-by: Frank Noirot <frank@zoo.dev>

* nit

* Prevent double write to KCL code on revolve

* Update output after adding cap-to-path graph edge

* Fix edit/select sketch-on-cap via feature tree

* clean up for face codeRef

* fix changing tools part way through circle/rect tools

* fix delete of circle profile

* fix close profiles

* fix closing profile bug (tangentArcTo being ignored)

* remove stale comment

* Delete paths associated with sketch when the sketch plane is deleted

* Add support for deleting sketches on caps (not walls)

* get delet working for walls

* make delet of extrusions work for multi profile

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

* Delete the sketch statement too on the cap and wall cases

* Don't write to file in `split-sketch-pipe-if-needed` unless necessary

* Don't wait for file write to complete within `updateEditorWithAstAndWriteToFile`
It is already debounced internally. If we await it, we will have to wait for a debounced timeout

* docs

* fix circ dep

* tsc

* fix selection enter sketch weirdness

* test fixes

* comment out and fixme for delete related tests

* add skip wins

* try and get last test to pass

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Kevin Nadro <nadr0@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
Co-authored-by: 49lf <ircsurfer33@gmail.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
2025-02-14 08:57:04 -05:00
8c5662e458 Add type to KclValue::Number (#5380)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-14 00:03:23 +00:00
f37fc357af KCL: Allow comments in CallExpressionKw (#5381)
Before, this would not parse:

```
line(
  end = [44.09, 306.95],
  // tag this for later
  tag = $hello
)
```

Now it does.
2025-02-13 23:18:54 +00:00
e27e9ecc63 test: Add SSI pattern simulation test (#5379)
* Add new SSI pattern test

* Update output since adding new test
2025-02-13 15:04:12 -06:00
78b42ea191 offsetPlane kwargs (#5367)
Previously: `offsetPlane('XY', 75)`
Now: `offsetPlane('XY', offset = 75)`

Pairs with this KCL-samples PR: https://github.com/KittyCAD/kcl-samples/pull/163
2025-02-13 14:37:02 -05:00
5d02a27122 CM KCL: add annotations (#5374)
* CM KCL: add annotations

* Make AnnotationName a token that includes the @

* The text of AnnotationName is now optional (#5324)

---------

Co-authored-by: Matt Mundell <matt@mundell.me>
2025-02-13 19:28:19 +00:00
49d52ce94b Remove KclValue::Int (#5369)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-02-14 08:28:00 +13:00
382 changed files with 33104 additions and 4422 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -59,23 +59,7 @@ Any KCL value.
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `Number`| | No | | `type` |enum: `Number`| | No |
| `value` |`number`| | No | | `value` |`number`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `ty` |[`NumericType`](/docs/kcl/types/NumericType)| Any KCL value. | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Int`| | No |
| `value` |`integer`| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -0,0 +1,250 @@
---
title: "NumericType"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Count`| | No |
----
**Type:** `object`
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Mm`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Cm`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `M`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Inches`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Feet`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Yards`| | No |
----
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Length`| | No |
----
**Type:** `object`
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Degrees`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Radians`| | No |
----
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Angle`| | No |
----
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Known`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Default`| | No |
| `len` |[`UnitLen`](/docs/kcl/types/UnitLen)| | No |
| `angle` |[`UnitAngle`](/docs/kcl/types/UnitAngle)| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Unknown`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Any`| | No |
----

View File

@ -0,0 +1,47 @@
---
title: "UnitAngle"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Degrees`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Radians`| | No |
----

186
docs/kcl/types/UnitType.md Normal file
View File

@ -0,0 +1,186 @@
---
title: "UnitType"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Count`| | No |
----
**Type:** `object`
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Mm`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Cm`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `M`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Inches`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Feet`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Yards`| | No |
----
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Length`| | No |
----
**Type:** `object`
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Degrees`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Radians`| | No |
----
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Angle`| | No |
----

File diff suppressed because one or more lines are too long

View File

@ -24,7 +24,7 @@ sketch001 = startSketchOn('XZ')
revolve001 = revolve({ axis = "X" }, sketch001) revolve001 = revolve({ axis = "X" }, sketch001)
triangle() triangle()
|> extrude(length = 30) |> extrude(length = 30)
plane001 = offsetPlane('XY', 10) plane001 = offsetPlane('XY', offset = 10)
sketch002 = startSketchOn(plane001) sketch002 = startSketchOn(plane001)
|> startProfileAt([-20, 0], %) |> startProfileAt([-20, 0], %)
|> line(end = [5, -15]) |> line(end = [5, -15])
@ -35,7 +35,7 @@ sketch002 = startSketchOn(plane001)
extrude001 = extrude(sketch002, length = 10) extrude001 = extrude(sketch002, length = 10)
` `
const FEAUTRE_TREE_SKETCH_CODE = `sketch001 = startSketchOn('XZ') const FEATURE_TREE_SKETCH_CODE = `sketch001 = startSketchOn('XZ')
|> startProfileAt([0, 0], %) |> startProfileAt([0, 0], %)
|> angledLine([0, 4], %, $rectangleSegmentA001) |> angledLine([0, 4], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
@ -54,7 +54,7 @@ sketch002 = startSketchOn(extrude001, rectangleSegmentB001)
center = [-1, 2], center = [-1, 2],
radius = .5 radius = .5
}, %) }, %)
plane001 = offsetPlane('XZ', -5) plane001 = offsetPlane('XZ', offset = -5)
sketch003 = startSketchOn(plane001) sketch003 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 5 }, %) |> circle({ center = [0, 0], radius = 5 }, %)
` `
@ -116,7 +116,7 @@ test.describe('Feature Tree pane', () => {
await testViewSource({ await testViewSource({
operationName: 'Offset Plane', operationName: 'Offset Plane',
operationIndex: 0, operationIndex: 0,
expectedActiveLine: "plane001 = offsetPlane('XY', 10)", expectedActiveLine: "plane001 = offsetPlane('XY', offset = 10)",
}) })
await testViewSource({ await testViewSource({
operationName: 'Extrude', operationName: 'Extrude',
@ -153,33 +153,16 @@ test.describe('Feature Tree pane', () => {
`User can edit sketch (but not on offset plane yet) from the feature tree`, `User can edit sketch (but not on offset plane yet) from the feature tree`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, homePage, scene, editor, toolbar, page }) => { async ({ context, homePage, scene, editor, toolbar, page }) => {
const unavailableToastMessage = page.getByText( await context.addInitScript((initialCode) => {
'Editing sketches on faces or offset planes through the feature tree is not yet supported' localStorage.setItem('persistCode', initialCode)
) }, FEATURE_TREE_SKETCH_CODE)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await context.folderSetupFn(async (dir) => { await test.step('force re-exe', async () => {
const bracketDir = join(dir, 'test-sample') await page.waitForTimeout(1000)
await fsp.mkdir(bracketDir, { recursive: true }) await editor.replaceCode('90', '91')
await fsp.writeFile( await page.waitForTimeout(1500)
join(bracketDir, 'main.kcl'),
FEAUTRE_TREE_SKETCH_CODE,
'utf-8'
)
})
await test.step('setup test', async () => {
await homePage.expectState({
projectCards: [
{
title: 'test-sample',
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
await homePage.openProject('test-sample')
await scene.waitForExecutionDone()
await toolbar.openFeatureTreePane()
}) })
await test.step('On a default plane should work', async () => { await test.step('On a default plane should work', async () => {
@ -199,24 +182,23 @@ test.describe('Feature Tree pane', () => {
await test.step('On an extrude face should *not* work', async () => { await test.step('On an extrude face should *not* work', async () => {
// Tooltip is getting in the way of clicking, so I'm first closing the pane // Tooltip is getting in the way of clicking, so I'm first closing the pane
await toolbar.closeFeatureTreePane() await toolbar.closeFeatureTreePane()
await page.waitForTimeout(1000)
await editor.replaceCode('91', '90')
await page.waitForTimeout(2000)
await (await toolbar.getFeatureTreeOperation('Sketch', 1)).dblclick() await (await toolbar.getFeatureTreeOperation('Sketch', 1)).dblclick()
await expect( await expect(
unavailableToastMessage, toolbar.exitSketchBtn,
'We should see a toast message about this' 'We should be in sketch mode now'
).toBeVisible() ).toBeVisible()
await unavailableToastMessage.waitFor({ state: 'detached' }) await editor.expectState({
// TODO - turn on once we update the artifactGraph in Rust highlightedCode: '',
// to include the proper source location for the extrude face diagnostics: [],
// await expect( activeLines: [
// toolbar.exitSketchBtn, 'sketch002=startSketchOn(extrude001,rectangleSegmentB001)',
// 'We should be in sketch mode now' ],
// ).toBeVisible() })
// await editor.expectState({ await toolbar.exitSketchBtn.click()
// highlightedCode: '',
// diagnostics: [],
// activeLines: ['|>circle({center=[-1,2],radius=.5},%)'],
// })
// await toolbar.exitSketchBtn.click()
}) })
await test.step('On an offset plane should *not* work', async () => { await test.step('On an offset plane should *not* work', async () => {
@ -226,7 +208,7 @@ test.describe('Feature Tree pane', () => {
await editor.expectState({ await editor.expectState({
highlightedCode: '', highlightedCode: '',
diagnostics: [], diagnostics: [],
activeLines: ['|>circle({center=[0,0],radius=5},%)'], activeLines: ['sketch003=startSketchOn(plane001)'],
}) })
await expect( await expect(
toolbar.exitSketchBtn, toolbar.exitSketchBtn,
@ -342,7 +324,8 @@ test.describe('Feature Tree pane', () => {
toolbar, toolbar,
cmdBar, cmdBar,
}) => { }) => {
const testCode = (value: string) => `p = offsetPlane('XY', ${value})` const testCode = (value: string) =>
`p = offsetPlane('XY', offset = ${value})`
const initialInput = '10' const initialInput = '10'
const initialCode = testCode(initialInput) const initialCode = testCode(initialInput)
const newInput = '5 + 10' const newInput = '5 + 10'

View File

@ -1028,7 +1028,7 @@ openSketch = startSketchOn('XY')
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 150 } const testPoint = { x: 700, y: 150 }
const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y) const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const expectedOutput = `plane001 = offsetPlane('XZ', 5)` const expectedOutput = `plane001 = offsetPlane('XZ', offset = 5)`
await homePage.goToModelingScene() await homePage.goToModelingScene()
// FIXME: Since there is no KCL code loaded. We need to wait for the scene to load before we continue. // FIXME: Since there is no KCL code loaded. We need to wait for the scene to load before we continue.
@ -1164,7 +1164,7 @@ openSketch = startSketchOn('XY')
}) => { }) => {
const initialCode = `sketch001 = startSketchOn('XZ') const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %) |> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50) plane001 = offsetPlane('XZ', offset = 50)
sketch002 = startSketchOn(plane001) sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 20 }, %) |> circle({ center = [0, 0], radius = 20 }, %)
` `
@ -1250,7 +1250,7 @@ openSketch = startSketchOn('XY')
}) => { }) => {
const initialCode = `sketch001 = startSketchOn('XZ') const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %) |> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50) plane001 = offsetPlane('XZ', offset = 50)
sketch002 = startSketchOn(plane001) sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 20 }, %) |> circle({ center = [0, 0], radius = 20 }, %)
loft001 = loft([sketch001, sketch002]) loft001 = loft([sketch001, sketch002])
@ -1297,7 +1297,7 @@ loft001 = loft([sketch001, sketch002])
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
await clickOnSketch2() await clickOnSketch2()
await expect(page.locator('.cm-activeLine')).toHaveText(` await expect(page.locator('.cm-activeLine')).toHaveText(`
plane001 = offsetPlane('XZ', 50) plane001 = offsetPlane('XZ', offset = 50)
`) `)
await page.keyboard.press('Backspace') await page.keyboard.press('Backspace')
// Check for sketch 1 // Check for sketch 1
@ -2448,19 +2448,18 @@ extrude002 = extrude(sketch002, length = 50)
await context.addInitScript((initialCode) => { await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode) localStorage.setItem('persistCode', initialCode)
}, initialCode) }, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone() await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value // One dumb hardcoded screen pixel value
const testPoint = { x: 550, y: 295 } const testPoint = { x: 580, y: 320 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y) const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellTarget = hasExtrudesInPipe ? 'sketch002' : 'extrude002' const shellTarget = hasExtrudesInPipe ? 'sketch002' : 'extrude002'
const shellDeclaration = `shell001 = shell(${shellTarget}, faces = ['end'], thickness = 5)` const shellDeclaration = `shell001 = shell(${shellTarget}, faces = ['end'], thickness = 5)`
await test.step(`Look for the grey of the shape`, async () => { await test.step(`Look for the grey of the shape`, async () => {
await toolbar.closePane('code') await scene.expectPixelColor([113, 113, 113], testPoint, 15)
await scene.expectPixelColor([128, 128, 128], testPoint, 15)
}) })
await test.step(`Go through the command bar flow, selecting a cap and keeping default thickness`, async () => { await test.step(`Go through the command bar flow, selecting a cap and keeping default thickness`, async () => {

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

@ -390,43 +390,47 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
`rev = revolve({ axis: 'y' }, part009)` `rev = revolve({ axis: 'y' }, part009)`
) )
// DELETE PARENT EXTRUDE // FIXME (commented section below), this test would select a wall that had a sketch on it, and delete the underlying extrude
await page.mouse.click(parentExtrude.x, parentExtrude.y) // and replace the sketch on face with a hard coded custom plane, but since there was a sketch on that plane maybe it
await page.waitForTimeout(100) // should have delete the sketch? it's broken atm, but not sure if worth fixing since desired behaviour is a little
await expect(page.locator('.cm-activeLine')).toHaveText( // vague
'|> line(end = [170.36, -121.61], tag = $seg01)' // // DELETE PARENT EXTRUDE
) // await page.mouse.click(parentExtrude.x, parentExtrude.y)
await u.clearCommandLogs() // await page.waitForTimeout(100)
await page.keyboard.press('Backspace') // await expect(page.locator('.cm-activeLine')).toHaveText(
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) // '|> line(end = [170.36, -121.61], tag = $seg01)'
await page.waitForTimeout(200) // )
await expect(u.codeLocator).not.toContainText( // await u.clearCommandLogs()
`extrude001 = extrude(sketch001, length = 50)` // await page.keyboard.press('Backspace')
) // await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({ // await page.waitForTimeout(200)
plane = { // await expect(u.codeLocator).not.toContainText(
origin = { x = 0, y = -50, z = 0 }, // `extrude001 = extrude(sketch001, length = 50)`
xAxis = { x = 1, y = 0, z = 0 }, // )
yAxis = { x = 0, y = 0, z = 1 }, // await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({
zAxis = { x = 0, y = -1, z = 0 } // plane = {
} // origin = { x = 0, y = -50, z = 0 },
})`) // xAxis = { x = 1, y = 0, z = 0 },
await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({ // yAxis = { x = 0, y = 0, z = 1 },
plane = { // zAxis = { x = 0, y = -1, z = 0 }
origin = { x = 116.53, y = 0, z = 163.25 }, // }
xAxis = { x = -0.81, y = 0, z = 0.58 }, // })`)
yAxis = { x = 0, y = -1, z = 0 }, // await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({
zAxis = { x = 0.58, y = 0, z = 0.81 } // plane = {
} // origin = { x = 116.53, y = 0, z = 163.25 },
})`) // xAxis = { x = -0.81, y = 0, z = 0.58 },
await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({ // yAxis = { x = 0, y = -1, z = 0 },
plane = { // zAxis = { x = 0.58, y = 0, z = 0.81 }
origin = { x = -91.74, y = 0, z = 80.89 }, // }
xAxis = { x = -0.66, y = 0, z = -0.75 }, // })`)
yAxis = { x = 0, y = -1, z = 0 }, // await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({
zAxis = { x = -0.75, y = 0, z = 0.66 } // plane = {
} // origin = { x = -91.74, y = 0, z = 80.89 },
})`) // xAxis = { x = -0.66, y = 0, z = -0.75 },
// yAxis = { x = 0, y = -1, z = 0 },
// zAxis = { x = -0.75, y = 0, z = 0.66 }
// }
// })`)
// DELETE SOLID 2D // DELETE SOLID 2D
await page.mouse.click(solid2d.x, solid2d.y) await page.mouse.click(solid2d.x, solid2d.y)
@ -454,15 +458,14 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet) await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet)
}) })
test("Deleting solid that the AST mod can't handle results in a toast message", async ({ test.fixme(
page, "Deleting solid that the AST mod can't handle results in a toast message",
homePage, async ({ page, homePage }) => {
}) => { const u = await getUtils(page)
const u = await getUtils(page) await page.addInitScript(async () => {
await page.addInitScript(async () => { localStorage.setItem(
localStorage.setItem( 'persistCode',
'persistCode', `sketch001 = startSketchOn('XZ')
`sketch001 = startSketchOn('XZ')
|> startProfileAt([-79.26, 95.04], %) |> startProfileAt([-79.26, 95.04], %)
|> line(end = [112.54, 127.64], tag = $seg02) |> line(end = [112.54, 127.64], tag = $seg02)
|> line(end = [170.36, -121.61], tag = $seg01) |> line(end = [170.36, -121.61], tag = $seg01)
@ -477,48 +480,49 @@ profile003 = startProfileAt([40.16, -120.48], sketch006)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
` `
)
}, KCL_DEFAULT_LENGTH)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await u.closeDebugPanel()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 1139.49, y: -7053, z: 8597.31 },
center: { x: -2206.68, y: -1298.36, z: 60 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
// attempt delete
await page.mouse.click(930, 139)
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toHaveText(
'|> line(end = [170.36, -121.61], tag = $seg01)'
) )
}, KCL_DEFAULT_LENGTH) await u.clearCommandLogs()
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.keyboard.press('Backspace')
await homePage.goToModelingScene() await expect(page.getByText('Unable to delete selection')).toBeVisible()
}
await u.openDebugPanel() )
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
await u.closeDebugPanel()
await u.openAndClearDebugPanel()
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
vantage: { x: 1139.49, y: -7053, z: 8597.31 },
center: { x: -2206.68, y: -1298.36, z: 60 },
up: { x: 0, y: 0, z: 1 },
},
})
await page.waitForTimeout(100)
await u.sendCustomCmd({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
await page.waitForTimeout(100)
// attempt delete
await page.mouse.click(930, 139)
await page.waitForTimeout(100)
await expect(page.locator('.cm-activeLine')).toHaveText(
'|> line(end = [170.36, -121.61], tag = $seg01)'
)
await u.clearCommandLogs()
await page.keyboard.press('Backspace')
await expect(page.getByText('Unable to delete selection')).toBeVisible()
})
test('Hovering over 3d features highlights code, clicking puts the cursor in the right place and sends selection id to engine', async ({ test('Hovering over 3d features highlights code, clicking puts the cursor in the right place and sends selection id to engine', async ({
page, page,
homePage, homePage,

View File

@ -16,8 +16,7 @@ mac:
arch: arch:
- x64 - x64
- arm64 - arm64
notarize: notarize: true
teamId: 92H8YB3B95
fileAssociations: fileAssociations:
- ext: kcl - ext: kcl
name: kcl name: kcl
@ -32,10 +31,11 @@ win:
arch: arch:
- x64 - x64
- arm64 - arm64
signingHashAlgorithms: signtoolOptions:
- sha256 sign: "./scripts/sign-win.js"
sign: "./scripts/sign-win.js" signingHashAlgorithms:
publisherName: "KittyCAD Inc" # needs to be exactly like on Digicert - sha256
publisherName: "KittyCAD Inc" # needs to be exactly like on Digicert
icon: "assets/icon.ico" icon: "assets/icon.ico"
fileAssociations: fileAssociations:
- ext: kcl - ext: kcl

View File

@ -40,7 +40,7 @@
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"diff": "^7.0.0", "diff": "^7.0.0",
"electron-updater": "6.3.0", "electron-updater": "^6.5.0",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
@ -85,7 +85,7 @@
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages", "fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages", "fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
"fetch:wasm": "./get-latest-wasm-bundle.sh", "fetch:wasm": "./get-latest-wasm-bundle.sh",
"fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/achalmers/kw-pattern-transform2/manifest.json", "fetch:samples": "echo \"Fetching latest KCL samples...\" && curl -o public/kcl-samples-manifest-fallback.json https://raw.githubusercontent.com/KittyCAD/kcl-samples/next/manifest.json",
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)", "isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
"build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm-dev": "yarn wasm-prep && (cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt", "build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
@ -145,10 +145,11 @@
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.25.4", "@babel/preset-env": "^7.25.4",
"@electron-forge/cli": "7.4.0", "@electron-forge/cli": "^7.6.1",
"@electron-forge/plugin-fuses": "7.4.0", "@electron-forge/plugin-fuses": "^7.6.1",
"@electron-forge/plugin-vite": "7.4.0", "@electron-forge/plugin-vite": "^7.6.1",
"@electron/fuses": "1.8.0", "@electron/fuses": "^1.8.0",
"@electron/notarize": "^2.5.0",
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
"@lezer/generator": "^1.7.2", "@lezer/generator": "^1.7.2",
"@nabla/vite-plugin-eslint": "^2.0.5", "@nabla/vite-plugin-eslint": "^2.0.5",
@ -175,9 +176,8 @@
"@vitest/web-worker": "^1.5.0", "@vitest/web-worker": "^1.5.0",
"@xstate/cli": "^0.5.17", "@xstate/cli": "^0.5.17",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"electron": "32.1.2", "electron": "^34.1.1",
"electron-builder": "24.13.3", "electron-builder": "^26.0.6",
"electron-notarize": "1.2.2",
"eslint": "^8.0.1", "eslint": "^8.0.1",
"eslint-plugin-css-modules": "^2.12.0", "eslint-plugin-css-modules": "^2.12.0",
"eslint-plugin-import": "^2.30.0", "eslint-plugin-import": "^2.30.0",

View File

@ -1,4 +1,5 @@
@precedence { @precedence {
annotation
member member
call call
exp @left exp @left
@ -20,9 +21,12 @@ statement[@isGroup=Statement] {
FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals? ParamList Arrow? Body } | FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals? ParamList Arrow? Body } |
VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } | VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
ReturnStatement { kw<"return"> expression } | ReturnStatement { kw<"return"> expression } |
ExpressionStatement { expression } ExpressionStatement { expression } |
Annotation { AnnotationName AnnotationList? }
} }
AnnotationList { !annotation "(" commaSep<AnnotationProperty> ")" }
ParamList { "(" commaSep<Parameter { VariableDefinition "?"? (":" type)? }> ")" } ParamList { "(" commaSep<Parameter { VariableDefinition "?"? (":" type)? }> ")" }
Body { "{" statement* "}" } Body { "{" statement* "}" }
@ -59,6 +63,12 @@ UnaryOp { AddOp | BangOp }
ObjectProperty { PropertyName (":" | Equals) expression } ObjectProperty { PropertyName (":" | Equals) expression }
AnnotationProperty {
PropertyName
( AddOp | MultOp | ExpOp | LogicOp | BangOp | CompOp | Equals | Arrow | PipeOperator | PipeSubstitution )
expression
}
LabeledArgument { ArgumentLabel Equals expression } LabeledArgument { ArgumentLabel Equals expression }
ArgumentList { "(" commaSep<LabeledArgument | expression> ")" } ArgumentList { "(" commaSep<LabeledArgument | expression> ")" }
@ -105,6 +115,7 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
PipeSubstitution { "%" } PipeSubstitution { "%" }
identifier { (@asciiLetter | "_") (@asciiLetter | @digit | "_")* } identifier { (@asciiLetter | "_") (@asciiLetter | @digit | "_")* }
AnnotationName { "@" identifier? }
PropertyName { identifier } PropertyName { identifier }
TagDeclarator { "$" identifier } TagDeclarator { "$" identifier }

View File

@ -0,0 +1,153 @@
# alone
@a
==>
Program(Annotation(AnnotationName))
# alone and anonymous
@
==>
Program(Annotation(AnnotationName))
# empty
@ann()
==>
Program(Annotation(AnnotationName,
AnnotationList))
# empty and anonymous
@()
==>
Program(Annotation(AnnotationName,
AnnotationList))
# equals
@setting(a=1)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
Number))))
# operator
@ann(a*1)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
MultOp,
Number))))
# anonymous
@(a=1)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
Number))))
# complex expr
@ann(a=(1+2+f('yes')))
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
ParenthesizedExpression(BinaryExpression(BinaryExpression(Number,
AddOp,
Number),
AddOp,
CallExpression(VariableName,
ArgumentList(String))))))))
# many args
@ann(a=1, b=2)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
Number),
AnnotationProperty(PropertyName,
Equals,
Number))))
# space around op
@ann(a / 1)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
MultOp,
Number))))
# space around sep
@ann(a/1 , b/2)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
MultOp,
Number),
AnnotationProperty(PropertyName,
MultOp,
Number))))
# trailing sep
@ann(a=1,)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
Number))))
# lone sep
@ann(,)
==>
Program(Annotation(AnnotationName,
AnnotationList))
# inside fn
fn f() {
@anno(b=2)
}
==>
Program(FunctionDeclaration(fn,
VariableDefinition,
ParamList,
Body(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
Number))))))
# laxer with space than the language parser is
@anno (b=2)
==>
Program(Annotation(AnnotationName,
AnnotationList(AnnotationProperty(PropertyName,
Equals,
Number))))

View File

@ -69,6 +69,7 @@ import {
startSketchOnDefault, startSketchOnDefault,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { import {
KclValue,
PathToNode, PathToNode,
Program, Program,
VariableDeclaration, VariableDeclaration,
@ -804,12 +805,33 @@ export const ModelingMachineProvider = ({
engineCommandManager.artifactGraph engineCommandManager.artifactGraph
) )
if (err(plane)) return Promise.reject(plane) if (err(plane)) return Promise.reject(plane)
let sketch: KclValue | null = null
const sketch = Object.values(kclManager.execState.variables).find( for (const variable of Object.values(
(variable) => kclManager.execState.variables
)) {
// find programMemory that matches path artifact
if (
variable?.type === 'Sketch' && variable?.type === 'Sketch' &&
variable.value.artifactId === plane.pathIds[0] variable.value.artifactId === plane.pathIds[0]
) ) {
sketch = variable
break
}
if (
// if the variable is an sweep, check if the underlying sketch matches the artifact
variable?.type === 'Solid' &&
variable.value.sketch.on.type === 'plane' &&
variable.value.sketch.artifactId === plane.pathIds[0]
) {
sketch = {
type: 'Sketch',
value: variable.value.sketch,
}
break
}
}
if (!sketch || sketch.type !== 'Sketch')
return Promise.reject(new Error('No sketch'))
if (!sketch || sketch.type !== 'Sketch') if (!sketch || sketch.type !== 'Sketch')
return Promise.reject(new Error('No sketch')) return Promise.reject(new Error('No sketch'))
const info = await getSketchOrientationDetails(sketch.value) const info = await getSketchOrientationDetails(sketch.value)
@ -1551,8 +1573,11 @@ export const ModelingMachineProvider = ({
? pathToProfile ? pathToProfile
: updatedSketchNodePaths[0] : updatedSketchNodePaths[0]
} }
await kclManager.executeAstMock(moddedAst)
await codeManager.updateEditorWithAstAndWriteToFile(moddedAst) if (doesNeedSplitting || indexToDelete >= 0) {
await kclManager.executeAstMock(moddedAst)
await codeManager.updateEditorWithAstAndWriteToFile(moddedAst)
}
return { return {
updatedEntryNodePath: pathToProfile, updatedEntryNodePath: pathToProfile,
updatedSketchNodePaths: updatedSketchNodePaths, updatedSketchNodePaths: updatedSketchNodePaths,

View File

@ -8,7 +8,7 @@ import { editorManager } from 'lib/singletons'
import { Annotation, Transaction } from '@codemirror/state' import { Annotation, Transaction } from '@codemirror/state'
import { EditorView, KeyBinding } from '@codemirror/view' import { EditorView, KeyBinding } from '@codemirror/view'
import { recast, Program } from 'lang/wasm' import { recast, Program } from 'lang/wasm'
import { err } from 'lib/trap' import { err, reportRejection } from 'lib/trap'
import { Compartment } from '@codemirror/state' import { Compartment } from '@codemirror/state'
import { history } from '@codemirror/commands' import { history } from '@codemirror/commands'
@ -168,7 +168,7 @@ export default class CodeManager {
const newCode = recast(ast) const newCode = recast(ast)
if (err(newCode)) return if (err(newCode)) return
this.updateCodeStateEditor(newCode) this.updateCodeStateEditor(newCode)
await this.writeToFile() this.writeToFile().catch(reportRejection)
} }
} }

View File

@ -239,6 +239,7 @@ const newVar = myVar + 1`
expect(mem['three']).toEqual({ expect(mem['three']).toEqual({
type: 'Number', type: 'Number',
value: 3, value: 3,
ty: expect.any(Object),
__meta: [ __meta: [
{ {
sourceRange: [14, 15, 0], sourceRange: [14, 15, 0],
@ -248,12 +249,23 @@ const newVar = myVar + 1`
expect(mem['yo']).toEqual({ expect(mem['yo']).toEqual({
type: 'Array', type: 'Array',
value: [ value: [
{ type: 'Number', value: 1, __meta: [{ sourceRange: [28, 29, 0] }] }, {
type: 'Number',
value: 1,
ty: expect.any(Object),
__meta: [{ sourceRange: [28, 29, 0] }],
},
{ type: 'String', value: '2', __meta: [{ sourceRange: [31, 34, 0] }] }, { type: 'String', value: '2', __meta: [{ sourceRange: [31, 34, 0] }] },
{ type: 'Number', value: 3, __meta: [{ sourceRange: [14, 15, 0] }] }, {
type: 'Number',
value: 3,
ty: expect.any(Object),
__meta: [{ sourceRange: [14, 15, 0] }],
},
{ {
type: 'Number', type: 'Number',
value: 9, value: 9,
ty: expect.any(Object),
__meta: [{ sourceRange: [43, 44, 0] }, { sourceRange: [47, 48, 0] }], __meta: [{ sourceRange: [43, 44, 0] }, { sourceRange: [47, 48, 0] }],
}, },
], ],
@ -281,16 +293,19 @@ const newVar = myVar + 1`
anum: { anum: {
type: 'Number', type: 'Number',
value: 2, value: 2,
ty: expect.any(Object),
__meta: [{ sourceRange: [47, 48, 0] }], __meta: [{ sourceRange: [47, 48, 0] }],
}, },
identifier: { identifier: {
type: 'Number', type: 'Number',
value: 3, value: 3,
ty: expect.any(Object),
__meta: [{ sourceRange: [14, 15, 0] }], __meta: [{ sourceRange: [14, 15, 0] }],
}, },
binExp: { binExp: {
type: 'Number', type: 'Number',
value: 9, value: 9,
ty: expect.any(Object),
__meta: [{ sourceRange: [77, 78, 0] }, { sourceRange: [81, 82, 0] }], __meta: [{ sourceRange: [77, 78, 0] }, { sourceRange: [81, 82, 0] }],
}, },
}, },
@ -404,6 +419,7 @@ describe('testing math operators', () => {
], ],
type: 'Number', type: 'Number',
value: 1, value: 1,
ty: expect.any(Object),
}, },
{ {
__meta: [ __meta: [
@ -413,6 +429,7 @@ describe('testing math operators', () => {
], ],
type: 'Number', type: 'Number',
value: -3, value: -3,
ty: expect.any(Object),
}, },
]) ])
}) })

View File

@ -32,7 +32,7 @@ child_process.spawnSync('git', [
'clone', 'clone',
'--single-branch', '--single-branch',
'--branch', '--branch',
'achalmers/kw-pattern-transform2', 'next',
URL_GIT_KCL_SAMPLES, URL_GIT_KCL_SAMPLES,
DIR_KCL_SAMPLES, DIR_KCL_SAMPLES,
]) ])

View File

@ -821,144 +821,146 @@ sketch003 = startSketchOn('XZ')
type: 'segment', type: 'segment',
}, },
], ],
[ // TODO FIXME, similar to fix me in e2e/playwright/testing-selections.spec.ts
'delete extrude', // also related to deleting, deleting in general probably is due for a refactor
{ // [
codeBefore: `sketch001 = startSketchOn('XZ') // 'delete extrude',
|> startProfileAt([3.29, 7.86], %) // {
|> line(end = [2.48, 2.44]) // codeBefore: `sketch001 = startSketchOn('XZ')
|> line(end = [2.66, 1.17]) // |> startProfileAt([3.29, 7.86], %)
|> line(end = [3.75, 0.46]) // |> line(end = [2.48, 2.44])
|> line(end = [4.99, -0.46], tag = $seg01) // |> line(end = [2.66, 1.17])
|> line(end = [-3.86, -2.73]) // |> line(end = [3.75, 0.46])
|> line(end = [-17.67, 0.85]) // |> line(end = [4.99, -0.46], tag = $seg01)
|> close() // |> line(end = [-3.86, -2.73])
const extrude001 = extrude(sketch001, length = 10)`, // |> line(end = [-17.67, 0.85])
codeAfter: `sketch001 = startSketchOn('XZ') // |> close()
|> startProfileAt([3.29, 7.86], %) // const extrude001 = extrude(sketch001, length = 10)`,
|> line(end = [2.48, 2.44]) // codeAfter: `sketch001 = startSketchOn('XZ')
|> line(end = [2.66, 1.17]) // |> startProfileAt([3.29, 7.86], %)
|> line(end = [3.75, 0.46]) // |> line(end = [2.48, 2.44])
|> line(end = [4.99, -0.46], tag = $seg01) // |> line(end = [2.66, 1.17])
|> line(end = [-3.86, -2.73]) // |> line(end = [3.75, 0.46])
|> line(end = [-17.67, 0.85]) // |> line(end = [4.99, -0.46], tag = $seg01)
|> close()\n`, // |> line(end = [-3.86, -2.73])
lineOfInterest: 'line(end = [2.66, 1.17])', // |> line(end = [-17.67, 0.85])
type: 'wall', // |> close()\n`,
}, // lineOfInterest: 'line(end = [2.66, 1.17])',
], // type: 'wall',
[ // },
'delete extrude with sketch on it', // ],
{ // [
codeBefore: `myVar = 5 // 'delete extrude with sketch on it',
sketch001 = startSketchOn('XZ') // {
|> startProfileAt([4.46, 5.12], %, $tag) // codeBefore: `myVar = 5
|> line(end = [0.08, myVar]) // sketch001 = startSketchOn('XZ')
|> line(end = [13.03, 2.02], tag = $seg01) // |> startProfileAt([4.46, 5.12], %, $tag)
|> line(end = [3.9, -7.6]) // |> line(end = [0.08, myVar])
|> line(end = [-11.18, -2.15]) // |> line(end = [13.03, 2.02], tag = $seg01)
|> line(end = [5.41, -9.61]) // |> line(end = [3.9, -7.6])
|> line(end = [-8.54, -2.51]) // |> line(end = [-11.18, -2.15])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [5.41, -9.61])
|> close() // |> line(end = [-8.54, -2.51])
const extrude001 = extrude(sketch001, length = 5) // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
sketch002 = startSketchOn(extrude001, seg01) // |> close()
|> startProfileAt([-12.55, 2.89], %) // const extrude001 = extrude(sketch001, length = 5)
|> line(end = [3.02, 1.9]) // sketch002 = startSketchOn(extrude001, seg01)
|> line(end = [1.82, -1.49], tag = $seg02) // |> startProfileAt([-12.55, 2.89], %)
|> angledLine([-86, segLen(seg02)], %) // |> line(end = [3.02, 1.9])
|> line(end = [-3.97, -0.53]) // |> line(end = [1.82, -1.49], tag = $seg02)
|> line(end = [0.3, 0.84]) // |> angledLine([-86, segLen(seg02)], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [-3.97, -0.53])
|> close()`, // |> line(end = [0.3, 0.84])
codeAfter: `myVar = 5 // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
sketch001 = startSketchOn('XZ') // |> close()`,
|> startProfileAt([4.46, 5.12], %, $tag) // codeAfter: `myVar = 5
|> line(end = [0.08, myVar]) // sketch001 = startSketchOn('XZ')
|> line(end = [13.03, 2.02], tag = $seg01) // |> startProfileAt([4.46, 5.12], %, $tag)
|> line(end = [3.9, -7.6]) // |> line(end = [0.08, myVar])
|> line(end = [-11.18, -2.15]) // |> line(end = [13.03, 2.02], tag = $seg01)
|> line(end = [5.41, -9.61]) // |> line(end = [3.9, -7.6])
|> line(end = [-8.54, -2.51]) // |> line(end = [-11.18, -2.15])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [5.41, -9.61])
|> close() // |> line(end = [-8.54, -2.51])
sketch002 = startSketchOn({ // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
plane = { // |> close()
origin = { x = 1, y = 2, z = 3 }, // sketch002 = startSketchOn({
xAxis = { x = 4, y = 5, z = 6 }, // plane = {
yAxis = { x = 7, y = 8, z = 9 }, // origin = { x = 1, y = 2, z = 3 },
zAxis = { x = 10, y = 11, z = 12 } // xAxis = { x = 4, y = 5, z = 6 },
} // yAxis = { x = 7, y = 8, z = 9 },
}) // zAxis = { x = 10, y = 11, z = 12 }
|> startProfileAt([-12.55, 2.89], %) // }
|> line(end = [3.02, 1.9]) // })
|> line(end = [1.82, -1.49], tag = $seg02) // |> startProfileAt([-12.55, 2.89], %)
|> angledLine([-86, segLen(seg02)], %) // |> line(end = [3.02, 1.9])
|> line(end = [-3.97, -0.53]) // |> line(end = [1.82, -1.49], tag = $seg02)
|> line(end = [0.3, 0.84]) // |> angledLine([-86, segLen(seg02)], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [-3.97, -0.53])
|> close() // |> line(end = [0.3, 0.84])
`, // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
lineOfInterest: 'line(end = [-11.18, -2.15])', // |> close()
type: 'wall', // `,
}, // lineOfInterest: 'line(end = [-11.18, -2.15])',
], // type: 'wall',
[ // },
'delete extrude with sketch on it 2', // ],
{ // [
codeBefore: `myVar = 5 // 'delete extrude with sketch on it 2',
sketch001 = startSketchOn('XZ') // {
|> startProfileAt([4.46, 5.12], %, $tag) // codeBefore: `myVar = 5
|> line(end = [0.08, myVar]) // sketch001 = startSketchOn('XZ')
|> line(end = [13.03, 2.02], tag = $seg01) // |> startProfileAt([4.46, 5.12], %, $tag)
|> line(end = [3.9, -7.6]) // |> line(end = [0.08, myVar])
|> line(end = [-11.18, -2.15]) // |> line(end = [13.03, 2.02], tag = $seg01)
|> line(end = [5.41, -9.61]) // |> line(end = [3.9, -7.6])
|> line(end = [-8.54, -2.51]) // |> line(end = [-11.18, -2.15])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [5.41, -9.61])
|> close() // |> line(end = [-8.54, -2.51])
const extrude001 = extrude(sketch001, length = 5) // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
sketch002 = startSketchOn(extrude001, seg01) // |> close()
|> startProfileAt([-12.55, 2.89], %) // const extrude001 = extrude(sketch001, length = 5)
|> line(end = [3.02, 1.9]) // sketch002 = startSketchOn(extrude001, seg01)
|> line(end = [1.82, -1.49], tag = $seg02) // |> startProfileAt([-12.55, 2.89], %)
|> angledLine([-86, segLen(seg02)], %) // |> line(end = [3.02, 1.9])
|> line(end = [-3.97, -0.53]) // |> line(end = [1.82, -1.49], tag = $seg02)
|> line(end = [0.3, 0.84]) // |> angledLine([-86, segLen(seg02)], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [-3.97, -0.53])
|> close()`, // |> line(end = [0.3, 0.84])
codeAfter: `myVar = 5 // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
sketch001 = startSketchOn('XZ') // |> close()`,
|> startProfileAt([4.46, 5.12], %, $tag) // codeAfter: `myVar = 5
|> line(end = [0.08, myVar]) // sketch001 = startSketchOn('XZ')
|> line(end = [13.03, 2.02], tag = $seg01) // |> startProfileAt([4.46, 5.12], %, $tag)
|> line(end = [3.9, -7.6]) // |> line(end = [0.08, myVar])
|> line(end = [-11.18, -2.15]) // |> line(end = [13.03, 2.02], tag = $seg01)
|> line(end = [5.41, -9.61]) // |> line(end = [3.9, -7.6])
|> line(end = [-8.54, -2.51]) // |> line(end = [-11.18, -2.15])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [5.41, -9.61])
|> close() // |> line(end = [-8.54, -2.51])
sketch002 = startSketchOn({ // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
plane = { // |> close()
origin = { x = 1, y = 2, z = 3 }, // sketch002 = startSketchOn({
xAxis = { x = 4, y = 5, z = 6 }, // plane = {
yAxis = { x = 7, y = 8, z = 9 }, // origin = { x = 1, y = 2, z = 3 },
zAxis = { x = 10, y = 11, z = 12 } // xAxis = { x = 4, y = 5, z = 6 },
} // yAxis = { x = 7, y = 8, z = 9 },
}) // zAxis = { x = 10, y = 11, z = 12 }
|> startProfileAt([-12.55, 2.89], %) // }
|> line(end = [3.02, 1.9]) // })
|> line(end = [1.82, -1.49], tag = $seg02) // |> startProfileAt([-12.55, 2.89], %)
|> angledLine([-86, segLen(seg02)], %) // |> line(end = [3.02, 1.9])
|> line(end = [-3.97, -0.53]) // |> line(end = [1.82, -1.49], tag = $seg02)
|> line(end = [0.3, 0.84]) // |> angledLine([-86, segLen(seg02)], %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) // |> line(end = [-3.97, -0.53])
|> close() // |> line(end = [0.3, 0.84])
`, // |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
lineOfInterest: 'startProfileAt([4.46, 5.12], %, $tag)', // |> close()
type: 'cap', // `,
}, // lineOfInterest: 'startProfileAt([4.46, 5.12], %, $tag)',
], // type: 'cap',
// },
// ],
] as const ] as const
test.each(cases)( test.each(cases)(
'%s', '%s',
@ -980,6 +982,7 @@ sketch002 = startSketchOn({
artifact, artifact,
}, },
execState.variables, execState.variables,
execState.artifactGraph,
async () => { async () => {
await new Promise((resolve) => setTimeout(resolve, 100)) await new Promise((resolve) => setTimeout(resolve, 100))
return { return {

View File

@ -38,6 +38,7 @@ import {
isCallExprWithName, isCallExprWithName,
ARG_INDEX_FIELD, ARG_INDEX_FIELD,
LABELED_ARG_FIELD, LABELED_ARG_FIELD,
UNLABELED_ARG,
} from './queryAst' } from './queryAst'
import { import {
addTagForSketchOnFace, addTagForSketchOnFace,
@ -68,12 +69,12 @@ import {
expandWall, expandWall,
getArtifactOfTypes, getArtifactOfTypes,
getArtifactsOfTypes, getArtifactsOfTypes,
getFaceCodeRef,
getPathsFromArtifact, getPathsFromArtifact,
} from './std/artifactGraph' } from './std/artifactGraph'
import { BodyItem } from 'wasm-lib/kcl/bindings/BodyItem' import { BodyItem } from 'wasm-lib/kcl/bindings/BodyItem'
import { findKwArg } from './util' import { findKwArg } from './util'
import { deleteEdgeTreatment } from './modifyAst/addEdgeTreatment' import { deleteEdgeTreatment } from './modifyAst/addEdgeTreatment'
import { engineCommandManager } from 'lib/singletons'
export function startSketchOnDefault( export function startSketchOnDefault(
node: Node<Program>, node: Node<Program>,
@ -676,10 +677,11 @@ export function addOffsetPlane({
const newPlane = createVariableDeclaration( const newPlane = createVariableDeclaration(
newPlaneName, newPlaneName,
createCallExpressionStdLib('offsetPlane', [ createCallExpressionStdLibKw(
'offsetPlane',
createLiteral(defaultPlane.toUpperCase()), createLiteral(defaultPlane.toUpperCase()),
offset, [createLabeledArg('offset', offset)]
]) )
) )
const insertAt = const insertAt =
@ -697,8 +699,7 @@ export function addOffsetPlane({
[insertAt, 'index'], [insertAt, 'index'],
['declaration', 'VariableDeclaration'], ['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['unlabeled', UNLABELED_ARG],
[0, 'index'],
] ]
return { return {
modifiedAst, modifiedAst,
@ -983,6 +984,7 @@ export function createCallExpressionStdLibKw(
end: 0, end: 0,
moduleId: 0, moduleId: 0,
outerAttrs: [], outerAttrs: [],
nonCodeMeta: nonCodeMetaEmpty(),
callee: { callee: {
type: 'Identifier', type: 'Identifier',
start: 0, start: 0,
@ -1390,6 +1392,7 @@ export async function deleteFromSelection(
ast: Node<Program>, ast: Node<Program>,
selection: Selection, selection: Selection,
variables: VariableMap, variables: VariableMap,
artifactGraph: ArtifactGraph,
getFaceDetails: (id: string) => Promise<Models['FaceIsPlanar_type']> = () => getFaceDetails: (id: string) => Promise<Models['FaceIsPlanar_type']> = () =>
({} as any) ({} as any)
): Promise<Node<Program> | Error> { ): Promise<Node<Program> | Error> {
@ -1402,12 +1405,12 @@ export async function deleteFromSelection(
) { ) {
const plane = const plane =
selection.artifact.type === 'plane' selection.artifact.type === 'plane'
? expandPlane(selection.artifact, engineCommandManager.artifactGraph) ? expandPlane(selection.artifact, artifactGraph)
: selection.artifact.type === 'wall' : selection.artifact.type === 'wall'
? expandWall(selection.artifact, engineCommandManager.artifactGraph) ? expandWall(selection.artifact, artifactGraph)
: expandCap(selection.artifact, engineCommandManager.artifactGraph) : expandCap(selection.artifact, artifactGraph)
for (const path of plane.paths.sort( for (const path of plane.paths.sort(
(a, b) => b.codeRef.range[0] - a.codeRef.range[0] (a, b) => b.codeRef.range?.[0] - a.codeRef.range?.[0]
)) { )) {
const varDec = getNodeFromPath<VariableDeclarator>( const varDec = getNodeFromPath<VariableDeclarator>(
ast, ast,
@ -1424,6 +1427,15 @@ export async function deleteFromSelection(
selection.artifact.type === 'cap' || selection.artifact.type === 'cap' ||
selection.artifact.type === 'wall' selection.artifact.type === 'wall'
) { ) {
// Delete the sketch node, which would not work if
// we continued down the traditional code path below.
// faceCodeRef's pathToNode is empty for some reason
// so using source range instead
const codeRef = getFaceCodeRef(selection.artifact)
if (!codeRef) return new Error('Could not find face code ref')
const sketchVarDec = getNodePathFromSourceRange(astClone, codeRef.range)
const sketchBodyIndex = Number(sketchVarDec[1][0])
astClone.body.splice(sketchBodyIndex, 1)
return astClone return astClone
} }
} }
@ -1523,20 +1535,20 @@ export async function deleteFromSelection(
selection.artifact.surfaceId selection.artifact.surfaceId
? getArtifactOfTypes( ? getArtifactOfTypes(
{ key: selection.artifact.surfaceId, types: ['wall'] }, { key: selection.artifact.surfaceId, types: ['wall'] },
engineCommandManager.artifactGraph artifactGraph
) )
: null : null
if (err(wallArtifact)) return if (err(wallArtifact)) return
if (wallArtifact) { if (wallArtifact) {
const sweep = getArtifactOfTypes( const sweep = getArtifactOfTypes(
{ key: wallArtifact.sweepId, types: ['sweep'] }, { key: wallArtifact.sweepId, types: ['sweep'] },
engineCommandManager.artifactGraph artifactGraph
) )
if (err(sweep)) return if (err(sweep)) return
const wallsWithDependencies = Array.from( const wallsWithDependencies = Array.from(
getArtifactsOfTypes( getArtifactsOfTypes(
{ keys: sweep.surfaceIds, types: ['wall', 'cap'] }, { keys: sweep.surfaceIds, types: ['wall', 'cap'] },
engineCommandManager.artifactGraph artifactGraph
).values() ).values()
).filter((wall) => wall?.pathIds?.length) ).filter((wall) => wall?.pathIds?.length)
const wallIds = wallsWithDependencies.map((wall) => wall.id) const wallIds = wallsWithDependencies.map((wall) => wall.id)
@ -1714,7 +1726,7 @@ export async function deleteFromSelection(
return new Error('Selection not recognised, could not delete') return new Error('Selection not recognised, could not delete')
} }
const nonCodeMetaEmpty = () => { export const nonCodeMetaEmpty = () => {
return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 } return { nonCodeNodes: {}, startNodes: [], start: 0, end: 0 }
} }

View File

@ -582,7 +582,7 @@ sketch002 = startSketchOn(extrude001, $seg01)
it('finds sketch001 and sketch002 pipes to be lofted', async () => { it('finds sketch001 and sketch002 pipes to be lofted', async () => {
const exampleCode = `sketch001 = startSketchOn('XZ') const exampleCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 1 }, %) |> circle({ center = [0, 0], radius = 1 }, %)
plane001 = offsetPlane('XZ', 2) plane001 = offsetPlane('XZ', offset = 2)
sketch002 = startSketchOn(plane001) sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 3 }, %) |> circle({ center = [0, 0], radius = 3 }, %)
` `

View File

@ -45,6 +45,7 @@ import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
import { KclSettingsAnnotation } from 'lib/settings/settingsTypes' import { KclSettingsAnnotation } from 'lib/settings/settingsTypes'
export const LABELED_ARG_FIELD = 'LabeledArg -> Arg' export const LABELED_ARG_FIELD = 'LabeledArg -> Arg'
export const UNLABELED_ARG = 'unlabeled first arg'
export const ARG_INDEX_FIELD = 'arg index' export const ARG_INDEX_FIELD = 'arg index'
/** /**

View File

@ -532,6 +532,32 @@ function getPlaneFromSolid2D(
if (err(path)) return path if (err(path)) return path
return getPlaneFromPath(path, graph) return getPlaneFromPath(path, graph)
} }
function getPlaneFromCap(
cap: CapArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const sweep = getArtifactOfTypes(
{ key: cap.sweepId, types: ['sweep'] },
graph
)
if (err(sweep)) return sweep
const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromWall(
wall: WallArtifact,
graph: ArtifactGraph
): PlaneArtifact | WallArtifact | CapArtifact | Error {
const sweep = getArtifactOfTypes(
{ key: wall.sweepId, types: ['sweep'] },
graph
)
if (err(sweep)) return sweep
const path = getArtifactOfTypes({ key: sweep.pathId, types: ['path'] }, graph)
if (err(path)) return path
return getPlaneFromPath(path, graph)
}
function getPlaneFromSweepEdge(edge: SweepEdge, graph: ArtifactGraph) { function getPlaneFromSweepEdge(edge: SweepEdge, graph: ArtifactGraph) {
const sweep = getArtifactOfTypes( const sweep = getArtifactOfTypes(
{ key: edge.sweepId, types: ['sweep'] }, { key: edge.sweepId, types: ['sweep'] },
@ -552,7 +578,15 @@ export function getPlaneFromArtifact(
if (artifact.type === 'path') return getPlaneFromPath(artifact, graph) if (artifact.type === 'path') return getPlaneFromPath(artifact, graph)
if (artifact.type === 'segment') return getPlaneFromSegment(artifact, graph) if (artifact.type === 'segment') return getPlaneFromSegment(artifact, graph)
if (artifact.type === 'solid2d') return getPlaneFromSolid2D(artifact, graph) if (artifact.type === 'solid2d') return getPlaneFromSolid2D(artifact, graph)
if (artifact.type === 'wall' || artifact.type === 'cap') return artifact if (
// if the user selects a face with sketch on it (pathIds.length), they probably wanted to edit that sketch,
// not the sketch for the underlying sweep sketch
(artifact.type === 'wall' || artifact.type === 'cap') &&
artifact?.pathIds?.length
)
return artifact
if (artifact.type === 'cap') return getPlaneFromCap(artifact, graph)
if (artifact.type === 'wall') return getPlaneFromWall(artifact, graph)
if (artifact.type === 'sweepEdge') if (artifact.type === 'sweepEdge')
return getPlaneFromSweepEdge(artifact, graph) return getPlaneFromSweepEdge(artifact, graph)
return new Error(`Artifact type ${artifact.type} does not have a plane`) return new Error(`Artifact type ${artifact.type} does not have a plane`)

View File

@ -30,6 +30,7 @@ import { toolTips, ToolTip } from 'lang/langHelpers'
import { import {
createPipeExpression, createPipeExpression,
mutateKwArg, mutateKwArg,
nonCodeMetaEmpty,
splitPathAtPipeExpression, splitPathAtPipeExpression,
} from '../modifyAst' } from '../modifyAst'
@ -2829,6 +2830,7 @@ function addTagKw(): addTagFn {
unlabeled: callExpr.node.arguments.length unlabeled: callExpr.node.arguments.length
? callExpr.node.arguments[0] ? callExpr.node.arguments[0]
: null, : null,
nonCodeMeta: nonCodeMetaEmpty(),
arguments: [], arguments: [],
} }
const tagArg = findKwArg(ARG_TAG, primaryCallExp) const tagArg = findKwArg(ARG_TAG, primaryCallExp)

View File

@ -14,6 +14,7 @@ describe('KCL expression calculations', () => {
variables['x'] = { variables['x'] = {
type: 'Number', type: 'Number',
value: 2, value: 2,
ty: { type: 'Any' },
__meta: [], __meta: [],
} }
const actual = await getCalculatedKclExpressionValue('1 + x', variables) const actual = await getCalculatedKclExpressionValue('1 + x', variables)
@ -32,6 +33,7 @@ describe('KCL expression calculations', () => {
variables['y'] = { variables['y'] = {
type: 'Number', type: 'Number',
value: 2, value: 2,
ty: { type: 'Any' },
__meta: [], __meta: [],
} }
const actual = await getCalculatedKclExpressionValue('1 + x', variables) const actual = await getCalculatedKclExpressionValue('1 + x', variables)
@ -44,6 +46,7 @@ describe('KCL expression calculations', () => {
variables['x'] = { variables['x'] = {
type: 'Number', type: 'Number',
value: 2, value: 2,
ty: { type: 'Any' },
__meta: [], __meta: [],
} }
const actual = await getCalculatedKclExpressionValue( const actual = await getCalculatedKclExpressionValue(

View File

@ -126,8 +126,7 @@ const prepareToEditOffsetPlane: PrepareToEditCallback = async ({
if ( if (
operation.type !== 'StdLibCall' || operation.type !== 'StdLibCall' ||
!operation.labeledArgs || !operation.labeledArgs ||
!('std_plane' in operation.labeledArgs) || !operation.unlabeledArg ||
!operation.labeledArgs.std_plane ||
!('offset' in operation.labeledArgs) || !('offset' in operation.labeledArgs) ||
!operation.labeledArgs.offset !operation.labeledArgs.offset
) { ) {
@ -135,11 +134,9 @@ const prepareToEditOffsetPlane: PrepareToEditCallback = async ({
} }
// TODO: Implement conversion to arbitrary plane selection // TODO: Implement conversion to arbitrary plane selection
// once the Offset Plane command supports it. // once the Offset Plane command supports it.
const stdPlane = operation.unlabeledArg
const planeName = codeManager.code const planeName = codeManager.code
.slice( .slice(stdPlane.sourceRange[0], stdPlane.sourceRange[1])
operation.labeledArgs.std_plane.sourceRange[0],
operation.labeledArgs.std_plane.sourceRange[1]
)
.replaceAll(`'`, ``) .replaceAll(`'`, ``)
if (!isDefaultPlaneStr(planeName)) { if (!isDefaultPlaneStr(planeName)) {

View File

@ -375,14 +375,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
}, },
icon: 'line', icon: 'line',
status: 'available', status: 'available',
disabled: (state) => disabled: (state) => state.matches('Sketch no face'),
state.matches('Sketch no face') ||
state.matches({
Sketch: { 'Rectangle tool': 'Awaiting second corner' },
}) ||
state.matches({
Sketch: { 'Circle tool': 'Awaiting Radius' },
}),
title: 'Line', title: 'Line',
hotkey: (state) => hotkey: (state) =>
state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L', state.matches({ Sketch: 'Line tool' }) ? ['Esc', 'L'] : 'L',

File diff suppressed because one or more lines are too long

View File

@ -53,6 +53,7 @@ process.env.VITE_KC_CONNECTION_TIMEOUT_MS ??=
console.log('process.env', process.env) console.log('process.env', process.env)
/// Register our application to handle all "zoo-studio:" protocols. /// Register our application to handle all "zoo-studio:" protocols.
const singleInstanceLock = app.requestSingleInstanceLock()
if (process.defaultApp) { if (process.defaultApp) {
if (process.argv.length >= 2) { if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [ app.setAsDefaultProtocolClient(ZOO_STUDIO_PROTOCOL, process.execPath, [
@ -65,7 +66,13 @@ if (process.defaultApp) {
// Global app listeners // Global app listeners
// Must be done before ready event. // Must be done before ready event.
registerStartupListeners() // Checking against this lock is needed for Windows and Linux, see
// https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app#windows-and-linux-code
if (!singleInstanceLock && !process.env.IS_PLAYWRIGHT) {
app.quit()
} else {
registerStartupListeners()
}
const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => { const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
let newWindow let newWindow
@ -75,7 +82,7 @@ const createWindow = (pathToOpen?: string, reuse?: boolean): BrowserWindow => {
} }
if (!newWindow) { if (!newWindow) {
newWindow = new BrowserWindow({ newWindow = new BrowserWindow({
autoHideMenuBar: true, autoHideMenuBar: false,
show: false, show: false,
width: 1800, width: 1800,
height: 1200, height: 1200,

View File

@ -730,7 +730,7 @@ dependencies = [
[[package]] [[package]]
name = "derive-docs" name = "derive-docs"
version = "0.1.36" version = "0.1.38"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"anyhow", "anyhow",
@ -1712,7 +1712,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.35" version = "0.2.38"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",
@ -1779,7 +1779,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-test-server" name = "kcl-test-server"
version = "0.1.21" version = "0.1.38"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hyper 0.14.32", "hyper 0.14.32",

View File

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

View File

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

View File

@ -1,7 +1,4 @@
use std::{ use std::{collections::HashMap, sync::Arc};
collections::HashMap,
sync::{Arc, Mutex},
};
use anyhow::Result; use anyhow::Result;
use indexmap::IndexMap; use indexmap::IndexMap;
@ -24,22 +21,20 @@ const NEED_PLANES: bool = true;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct EngineConnection { pub struct EngineConnection {
batch: Arc<Mutex<Vec<(WebSocketRequest, kcl_lib::SourceRange)>>>, batch: Arc<RwLock<Vec<(WebSocketRequest, kcl_lib::SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::SourceRange)>>>, batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::SourceRange)>>>,
core_test: Arc<Mutex<String>>, core_test: Arc<RwLock<String>>,
default_planes: Arc<RwLock<Option<DefaultPlanes>>>, default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
execution_kind: Arc<Mutex<ExecutionKind>>, execution_kind: Arc<RwLock<ExecutionKind>>,
} }
impl EngineConnection { impl EngineConnection {
pub async fn new(result: Arc<Mutex<String>>) -> Result<EngineConnection> { pub async fn new(result: Arc<RwLock<String>>) -> Result<EngineConnection> {
if let Ok(mut code) = result.lock() { result.write().await.push_str(CPP_PREFIX);
code.push_str(CPP_PREFIX);
}
Ok(EngineConnection { Ok(EngineConnection {
batch: Arc::new(Mutex::new(Vec::new())), batch: Arc::new(RwLock::new(Vec::new())),
batch_end: Arc::new(Mutex::new(IndexMap::new())), batch_end: Arc::new(RwLock::new(IndexMap::new())),
core_test: result, core_test: result,
default_planes: Default::default(), default_planes: Default::default(),
execution_kind: Default::default(), execution_kind: Default::default(),
@ -362,29 +357,29 @@ fn codegen_cpp_repl_uuid_setters(reps_id: &str, entity_ids: &[uuid::Uuid]) -> St
#[async_trait::async_trait] #[async_trait::async_trait]
impl kcl_lib::EngineManager for EngineConnection { impl kcl_lib::EngineManager for EngineConnection {
fn batch(&self) -> Arc<Mutex<Vec<(WebSocketRequest, kcl_lib::SourceRange)>>> { fn batch(&self) -> Arc<RwLock<Vec<(WebSocketRequest, kcl_lib::SourceRange)>>> {
self.batch.clone() self.batch.clone()
} }
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::SourceRange)>>> { fn batch_end(&self) -> Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::SourceRange)>>> {
self.batch_end.clone() self.batch_end.clone()
} }
fn responses(&self) -> IndexMap<Uuid, WebSocketResponse> { fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>> {
IndexMap::new() Arc::new(RwLock::new(IndexMap::new()))
} }
fn take_artifact_commands(&self) -> Vec<ArtifactCommand> { fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>> {
Vec::new() Arc::new(RwLock::new(Vec::new()))
} }
fn execution_kind(&self) -> ExecutionKind { async fn execution_kind(&self) -> ExecutionKind {
let guard = self.execution_kind.lock().unwrap(); let guard = self.execution_kind.read().await;
*guard *guard
} }
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind { async fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
let mut guard = self.execution_kind.lock().unwrap(); let mut guard = self.execution_kind.write().await;
let original = *guard; let original = *guard;
*guard = execution_kind; *guard = execution_kind;
original original
@ -435,24 +430,18 @@ impl kcl_lib::EngineManager for EngineConnection {
}) => { }) => {
let mut responses = HashMap::new(); let mut responses = HashMap::new();
for request in requests { for request in requests {
let (new_code, this_response); let (new_code, this_response) = self.handle_command(&request.cmd_id, &request.cmd);
if let Ok(mut test_code) = self.core_test.lock() { if !new_code.is_empty() {
(new_code, this_response) = self.handle_command(&request.cmd_id, &request.cmd); let new_code = new_code
.trim()
if !new_code.is_empty() { .split(' ')
let new_code = new_code .filter(|s| !s.is_empty())
.trim() .collect::<Vec<_>>()
.split(' ') .join(" ")
.filter(|s| !s.is_empty()) + "\n";
.collect::<Vec<_>>() //println!("{new_code}");
.join(" ") self.core_test.write().await.push_str(&new_code);
+ "\n";
//println!("{new_code}");
test_code.push_str(&new_code);
}
} else {
this_response = OkModelingCmdResponse::Empty {};
} }
responses.insert( responses.insert(
@ -470,24 +459,18 @@ impl kcl_lib::EngineManager for EngineConnection {
} }
WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd, cmd_id }) => { WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd, cmd_id }) => {
//also handle unbatched requests inline //also handle unbatched requests inline
let (new_code, this_response); let (new_code, this_response) = self.handle_command(&cmd_id, &cmd);
if let Ok(mut test_code) = self.core_test.lock() { if !new_code.is_empty() {
(new_code, this_response) = self.handle_command(&cmd_id, &cmd); let new_code = new_code
.trim()
if !new_code.is_empty() { .split(' ')
let new_code = new_code .filter(|s| !s.is_empty())
.trim() .collect::<Vec<_>>()
.split(' ') .join(" ")
.filter(|s| !s.is_empty()) + "\n";
.collect::<Vec<_>>() //println!("{new_code}");
.join(" ") self.core_test.write().await.push_str(&new_code);
+ "\n";
//println!("{new_code}");
test_code.push_str(&new_code);
}
} else {
this_response = OkModelingCmdResponse::Empty {};
} }
Ok(WebSocketResponse::Success(kcmc::websocket::SuccessWebSocketResponse { Ok(WebSocketResponse::Success(kcmc::websocket::SuccessWebSocketResponse {

View File

@ -1,7 +1,8 @@
use std::sync::{Arc, Mutex}; use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use kcl_lib::{ExecState, ExecutorContext}; use kcl_lib::{ExecState, ExecutorContext};
use tokio::sync::RwLock;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
mod conn_mock_core; mod conn_mock_core;
@ -10,7 +11,7 @@ mod conn_mock_core;
pub async fn kcl_to_engine_core(code: &str) -> Result<String> { pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
let program = kcl_lib::Program::parse_no_errs(code)?; let program = kcl_lib::Program::parse_no_errs(code)?;
let result = Arc::new(Mutex::new("".into())); let result = Arc::new(RwLock::new("".into()));
let ref_result = Arc::clone(&result); let ref_result = Arc::clone(&result);
let ctx = ExecutorContext::new_forwarded_mock(Arc::new(Box::new( let ctx = ExecutorContext::new_forwarded_mock(Arc::new(Box::new(
@ -18,6 +19,6 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
))); )));
ctx.run(&program, &mut ExecState::new(&ctx.settings)).await?; ctx.run(&program, &mut ExecState::new(&ctx.settings)).await?;
let result = result.lock().expect("mutex lock").clone(); let result = result.read().await.clone();
Ok(result) Ok(result)
} }

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.35" version = "0.2.38"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -1,13 +1,9 @@
//! Functions for setting up our WebSocket and WebRTC connections for communications with the //! Functions for setting up our WebSocket and WebRTC connections for communications with the
//! engine. //! engine.
use std::{ use std::{collections::HashMap, sync::Arc};
collections::HashMap,
sync::{Arc, Mutex},
};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use dashmap::DashMap;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use indexmap::IndexMap; use indexmap::IndexMap;
use kcmc::{ use kcmc::{
@ -17,9 +13,7 @@ use kcmc::{
}, },
ModelingCmd, ModelingCmd,
}; };
use kittycad_modeling_cmds::{ use kittycad_modeling_cmds::{self as kcmc};
self as kcmc, id::ModelingCmdId, ok_response::OkModelingCmdResponse, websocket::ModelingBatch,
};
use tokio::sync::{mpsc, oneshot, RwLock}; use tokio::sync::{mpsc, oneshot, RwLock};
use tokio_tungstenite::tungstenite::Message as WsMsg; use tokio_tungstenite::tungstenite::Message as WsMsg;
use uuid::Uuid; use uuid::Uuid;
@ -43,21 +37,21 @@ type WebSocketTcpWrite = futures::stream::SplitSink<tokio_tungstenite::WebSocket
pub struct EngineConnection { pub struct EngineConnection {
engine_req_tx: mpsc::Sender<ToEngineReq>, engine_req_tx: mpsc::Sender<ToEngineReq>,
shutdown_tx: mpsc::Sender<()>, shutdown_tx: mpsc::Sender<()>,
responses: Arc<DashMap<uuid::Uuid, WebSocketResponse>>, responses: Arc<RwLock<IndexMap<uuid::Uuid, WebSocketResponse>>>,
pending_errors: Arc<Mutex<Vec<String>>>, pending_errors: Arc<RwLock<Vec<String>>>,
#[allow(dead_code)] #[allow(dead_code)]
tcp_read_handle: Arc<TcpReadHandle>, tcp_read_handle: Arc<TcpReadHandle>,
socket_health: Arc<Mutex<SocketHealth>>, socket_health: Arc<RwLock<SocketHealth>>,
batch: Arc<Mutex<Vec<(WebSocketRequest, SourceRange)>>>, batch: Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>, batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>,
artifact_commands: Arc<Mutex<Vec<ArtifactCommand>>>, artifact_commands: Arc<RwLock<Vec<ArtifactCommand>>>,
/// The default planes for the scene. /// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>, default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
/// If the server sends session data, it'll be copied to here. /// If the server sends session data, it'll be copied to here.
session_data: Arc<Mutex<Option<ModelingSessionData>>>, session_data: Arc<RwLock<Option<ModelingSessionData>>>,
execution_kind: Arc<Mutex<ExecutionKind>>, execution_kind: Arc<RwLock<ExecutionKind>>,
} }
pub struct TcpRead { pub struct TcpRead {
@ -230,12 +224,12 @@ impl EngineConnection {
let mut tcp_read = TcpRead { stream: tcp_read }; let mut tcp_read = TcpRead { stream: tcp_read };
let session_data: Arc<Mutex<Option<ModelingSessionData>>> = Arc::new(Mutex::new(None)); let session_data: Arc<RwLock<Option<ModelingSessionData>>> = Arc::new(RwLock::new(None));
let session_data2 = session_data.clone(); let session_data2 = session_data.clone();
let responses: Arc<DashMap<uuid::Uuid, WebSocketResponse>> = Arc::new(DashMap::new()); let responses: Arc<RwLock<IndexMap<uuid::Uuid, WebSocketResponse>>> = Arc::new(RwLock::new(IndexMap::new()));
let responses_clone = responses.clone(); let responses_clone = responses.clone();
let socket_health = Arc::new(Mutex::new(SocketHealth::Active)); let socket_health = Arc::new(RwLock::new(SocketHealth::Active));
let pending_errors = Arc::new(Mutex::new(Vec::new())); let pending_errors = Arc::new(RwLock::new(Vec::new()));
let pending_errors_clone = pending_errors.clone(); let pending_errors_clone = pending_errors.clone();
let socket_health_tcp_read = socket_health.clone(); let socket_health_tcp_read = socket_health.clone();
@ -260,7 +254,7 @@ impl EngineConnection {
let id: uuid::Uuid = (*resp_id).into(); let id: uuid::Uuid = (*resp_id).into();
match batch_response { match batch_response {
BatchResponse::Success { response } => { BatchResponse::Success { response } => {
responses_clone.insert( responses_clone.write().await.insert(
id, id,
WebSocketResponse::Success(SuccessWebSocketResponse { WebSocketResponse::Success(SuccessWebSocketResponse {
success: true, success: true,
@ -272,7 +266,7 @@ impl EngineConnection {
); );
} }
BatchResponse::Failure { errors } => { BatchResponse::Failure { errors } => {
responses_clone.insert( responses_clone.write().await.insert(
id, id,
WebSocketResponse::Failure(FailureWebSocketResponse { WebSocketResponse::Failure(FailureWebSocketResponse {
success: false, success: false,
@ -288,7 +282,7 @@ impl EngineConnection {
resp: OkWebSocketResponseData::ModelingSessionData { session }, resp: OkWebSocketResponseData::ModelingSessionData { session },
.. ..
}) => { }) => {
let mut sd = session_data2.lock().unwrap(); let mut sd = session_data2.write().await;
sd.replace(session.clone()); sd.replace(session.clone());
} }
WebSocketResponse::Failure(FailureWebSocketResponse { WebSocketResponse::Failure(FailureWebSocketResponse {
@ -297,7 +291,7 @@ impl EngineConnection {
errors, errors,
}) => { }) => {
if let Some(id) = request_id { if let Some(id) = request_id {
responses_clone.insert( responses_clone.write().await.insert(
*id, *id,
WebSocketResponse::Failure(FailureWebSocketResponse { WebSocketResponse::Failure(FailureWebSocketResponse {
success: false, success: false,
@ -307,19 +301,20 @@ impl EngineConnection {
); );
} else { } else {
// Add it to our pending errors. // Add it to our pending errors.
let mut pe = pending_errors_clone.lock().unwrap(); let mut pe = pending_errors_clone.write().await;
for error in errors { for error in errors {
if !pe.contains(&error.message) { if !pe.contains(&error.message) {
pe.push(error.message.clone()); pe.push(error.message.clone());
} }
} }
drop(pe);
} }
} }
_ => {} _ => {}
} }
if let Some(id) = id { if let Some(id) = id {
responses_clone.insert(id, ws_resp.clone()); responses_clone.write().await.insert(id, ws_resp.clone());
} }
} }
Err(e) => { Err(e) => {
@ -327,7 +322,7 @@ impl EngineConnection {
WebSocketReadError::Read(e) => crate::logln!("could not read from WS: {:?}", e), WebSocketReadError::Read(e) => crate::logln!("could not read from WS: {:?}", e),
WebSocketReadError::Deser(e) => crate::logln!("could not deserialize msg from WS: {:?}", e), WebSocketReadError::Deser(e) => crate::logln!("could not deserialize msg from WS: {:?}", e),
} }
*socket_health_tcp_read.lock().unwrap() = SocketHealth::Inactive; *socket_health_tcp_read.write().await = SocketHealth::Inactive;
return Err(e); return Err(e);
} }
} }
@ -343,70 +338,41 @@ impl EngineConnection {
responses, responses,
pending_errors, pending_errors,
socket_health, socket_health,
batch: Arc::new(Mutex::new(Vec::new())), batch: Arc::new(RwLock::new(Vec::new())),
batch_end: Arc::new(Mutex::new(IndexMap::new())), batch_end: Arc::new(RwLock::new(IndexMap::new())),
artifact_commands: Arc::new(Mutex::new(Vec::new())), artifact_commands: Arc::new(RwLock::new(Vec::new())),
default_planes: Default::default(), default_planes: Default::default(),
session_data, session_data,
execution_kind: Default::default(), execution_kind: Default::default(),
}) })
} }
fn handle_command(
&self,
cmd: &ModelingCmd,
cmd_id: ModelingCmdId,
id_to_source_range: &HashMap<Uuid, SourceRange>,
) -> Result<(), KclError> {
let cmd_id = *cmd_id.as_ref();
let range = id_to_source_range
.get(&cmd_id)
.copied()
.ok_or_else(|| KclError::internal(format!("Failed to get source range for command ID: {:?}", cmd_id)))?;
// Add artifact command.
let mut artifact_commands = self.artifact_commands.lock().unwrap();
artifact_commands.push(ArtifactCommand {
cmd_id,
range,
command: cmd.clone(),
});
Ok(())
}
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl EngineManager for EngineConnection { impl EngineManager for EngineConnection {
fn batch(&self) -> Arc<Mutex<Vec<(WebSocketRequest, SourceRange)>>> { fn batch(&self) -> Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>> {
self.batch.clone() self.batch.clone()
} }
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>> { fn batch_end(&self) -> Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>> {
self.batch_end.clone() self.batch_end.clone()
} }
fn responses(&self) -> IndexMap<Uuid, WebSocketResponse> { fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>> {
self.responses self.responses.clone()
.iter()
.map(|entry| {
let (k, v) = entry.pair();
(*k, v.clone())
})
.collect()
} }
fn take_artifact_commands(&self) -> Vec<ArtifactCommand> { fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>> {
let mut artifact_commands = self.artifact_commands.lock().unwrap(); self.artifact_commands.clone()
std::mem::take(&mut *artifact_commands)
} }
fn execution_kind(&self) -> ExecutionKind { async fn execution_kind(&self) -> ExecutionKind {
let guard = self.execution_kind.lock().unwrap(); let guard = self.execution_kind.read().await;
*guard *guard
} }
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind { async fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
let mut guard = self.execution_kind.lock().unwrap(); let mut guard = self.execution_kind.write().await;
let original = *guard; let original = *guard;
*guard = execution_kind; *guard = execution_kind;
original original
@ -447,49 +413,8 @@ impl EngineManager for EngineConnection {
id: uuid::Uuid, id: uuid::Uuid,
source_range: SourceRange, source_range: SourceRange,
cmd: WebSocketRequest, cmd: WebSocketRequest,
id_to_source_range: HashMap<Uuid, SourceRange>, _id_to_source_range: HashMap<Uuid, SourceRange>,
) -> Result<WebSocketResponse, KclError> { ) -> Result<WebSocketResponse, KclError> {
match &cmd {
WebSocketRequest::ModelingCmdBatchReq(ModelingBatch { requests, .. }) => {
for request in requests {
self.handle_command(&request.cmd, request.cmd_id, &id_to_source_range)?;
}
}
WebSocketRequest::ModelingCmdReq(request) => {
self.handle_command(&request.cmd, request.cmd_id, &id_to_source_range)?;
}
_ => {}
}
// In isolated mode, we don't send the command to the engine.
if self.execution_kind().is_isolated() {
return match &cmd {
WebSocketRequest::ModelingCmdBatchReq(ModelingBatch { requests, .. }) => {
let mut responses = HashMap::with_capacity(requests.len());
for request in requests {
responses.insert(
request.cmd_id,
BatchResponse::Success {
response: OkModelingCmdResponse::Empty {},
},
);
}
Ok(WebSocketResponse::Success(SuccessWebSocketResponse {
request_id: Some(id),
resp: OkWebSocketResponseData::ModelingBatch { responses },
success: true,
}))
}
_ => Ok(WebSocketResponse::Success(SuccessWebSocketResponse {
request_id: Some(id),
resp: OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::Empty {},
},
success: true,
})),
};
}
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
// Send the request to the engine, via the actor. // Send the request to the engine, via the actor.
@ -524,25 +449,24 @@ impl EngineManager for EngineConnection {
// Wait for the response. // Wait for the response.
let current_time = std::time::Instant::now(); let current_time = std::time::Instant::now();
while current_time.elapsed().as_secs() < 60 { while current_time.elapsed().as_secs() < 60 {
if let Ok(guard) = self.socket_health.lock() { let guard = self.socket_health.read().await;
if *guard == SocketHealth::Inactive { if *guard == SocketHealth::Inactive {
// Check if we have any pending errors. // Check if we have any pending errors.
let pe = self.pending_errors.lock().unwrap(); let pe = self.pending_errors.read().await;
if !pe.is_empty() { if !pe.is_empty() {
return Err(KclError::Engine(KclErrorDetails { return Err(KclError::Engine(KclErrorDetails {
message: pe.join(", ").to_string(), message: pe.join(", ").to_string(),
source_ranges: vec![source_range], source_ranges: vec![source_range],
})); }));
} else { } else {
return Err(KclError::Engine(KclErrorDetails { return Err(KclError::Engine(KclErrorDetails {
message: "Modeling command failed: websocket closed early".to_string(), message: "Modeling command failed: websocket closed early".to_string(),
source_ranges: vec![source_range], source_ranges: vec![source_range],
})); }));
}
} }
} }
// We pop off the responses to cleanup our mappings. // We pop off the responses to cleanup our mappings.
if let Some((_, resp)) = self.responses.remove(&id) { if let Some(resp) = self.responses.write().await.shift_remove(&id) {
return Ok(resp); return Ok(resp);
} }
} }
@ -553,17 +477,16 @@ impl EngineManager for EngineConnection {
})) }))
} }
fn get_session_data(&self) -> Option<ModelingSessionData> { async fn get_session_data(&self) -> Option<ModelingSessionData> {
self.session_data.lock().unwrap().clone() self.session_data.read().await.clone()
} }
async fn close(&self) { async fn close(&self) {
let _ = self.shutdown_tx.send(()).await; let _ = self.shutdown_tx.send(()).await;
loop { loop {
if let Ok(guard) = self.socket_health.lock() { let guard = self.socket_health.read().await;
if *guard == SocketHealth::Inactive { if *guard == SocketHealth::Inactive {
return; return;
}
} }
} }
} }

View File

@ -1,10 +1,7 @@
//! Functions for setting up our WebSocket and WebRTC connections for communications with the //! Functions for setting up our WebSocket and WebRTC connections for communications with the
//! engine. //! engine.
use std::{ use std::{collections::HashMap, sync::Arc};
collections::HashMap,
sync::{Arc, Mutex},
};
use anyhow::Result; use anyhow::Result;
use indexmap::IndexMap; use indexmap::IndexMap;
@ -15,7 +12,8 @@ use kcmc::{
WebSocketResponse, WebSocketResponse,
}, },
}; };
use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId, ModelingCmd}; use kittycad_modeling_cmds::{self as kcmc};
use tokio::sync::RwLock;
use uuid::Uuid; use uuid::Uuid;
use super::ExecutionKind; use super::ExecutionKind;
@ -28,71 +26,48 @@ use crate::{
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct EngineConnection { pub struct EngineConnection {
batch: Arc<Mutex<Vec<(WebSocketRequest, SourceRange)>>>, batch: Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>, batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>,
artifact_commands: Arc<Mutex<Vec<ArtifactCommand>>>, artifact_commands: Arc<RwLock<Vec<ArtifactCommand>>>,
execution_kind: Arc<Mutex<ExecutionKind>>, execution_kind: Arc<RwLock<ExecutionKind>>,
} }
impl EngineConnection { impl EngineConnection {
pub async fn new() -> Result<EngineConnection> { pub async fn new() -> Result<EngineConnection> {
Ok(EngineConnection { Ok(EngineConnection {
batch: Arc::new(Mutex::new(Vec::new())), batch: Arc::new(RwLock::new(Vec::new())),
batch_end: Arc::new(Mutex::new(IndexMap::new())), batch_end: Arc::new(RwLock::new(IndexMap::new())),
artifact_commands: Arc::new(Mutex::new(Vec::new())), artifact_commands: Arc::new(RwLock::new(Vec::new())),
execution_kind: Default::default(), execution_kind: Default::default(),
}) })
} }
fn handle_command(
&self,
cmd: &ModelingCmd,
cmd_id: ModelingCmdId,
id_to_source_range: &HashMap<Uuid, SourceRange>,
) -> Result<(), KclError> {
let cmd_id = *cmd_id.as_ref();
let range = id_to_source_range
.get(&cmd_id)
.copied()
.ok_or_else(|| KclError::internal(format!("Failed to get source range for command ID: {:?}", cmd_id)))?;
// Add artifact command.
let mut artifact_commands = self.artifact_commands.lock().unwrap();
artifact_commands.push(ArtifactCommand {
cmd_id,
range,
command: cmd.clone(),
});
Ok(())
}
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl crate::engine::EngineManager for EngineConnection { impl crate::engine::EngineManager for EngineConnection {
fn batch(&self) -> Arc<Mutex<Vec<(WebSocketRequest, SourceRange)>>> { fn batch(&self) -> Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>> {
self.batch.clone() self.batch.clone()
} }
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>> { fn batch_end(&self) -> Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>> {
self.batch_end.clone() self.batch_end.clone()
} }
fn responses(&self) -> IndexMap<Uuid, WebSocketResponse> { fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>> {
IndexMap::new() Arc::new(RwLock::new(IndexMap::new()))
} }
fn take_artifact_commands(&self) -> Vec<ArtifactCommand> { fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>> {
let mut artifact_commands = self.artifact_commands.lock().unwrap(); self.artifact_commands.clone()
std::mem::take(&mut *artifact_commands)
} }
fn execution_kind(&self) -> ExecutionKind { async fn execution_kind(&self) -> ExecutionKind {
let guard = self.execution_kind.lock().unwrap(); let guard = self.execution_kind.read().await;
*guard *guard
} }
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind { async fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
let mut guard = self.execution_kind.lock().unwrap(); let mut guard = self.execution_kind.write().await;
let original = *guard; let original = *guard;
*guard = execution_kind; *guard = execution_kind;
original original
@ -119,7 +94,7 @@ impl crate::engine::EngineManager for EngineConnection {
id: uuid::Uuid, id: uuid::Uuid,
_source_range: SourceRange, _source_range: SourceRange,
cmd: WebSocketRequest, cmd: WebSocketRequest,
id_to_source_range: HashMap<Uuid, SourceRange>, _id_to_source_range: HashMap<Uuid, SourceRange>,
) -> Result<WebSocketResponse, KclError> { ) -> Result<WebSocketResponse, KclError> {
match cmd { match cmd {
WebSocketRequest::ModelingCmdBatchReq(ModelingBatch { WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
@ -130,7 +105,6 @@ impl crate::engine::EngineManager for EngineConnection {
// Create the empty responses. // Create the empty responses.
let mut responses = HashMap::with_capacity(requests.len()); let mut responses = HashMap::with_capacity(requests.len());
for request in requests { for request in requests {
self.handle_command(&request.cmd, request.cmd_id, &id_to_source_range)?;
responses.insert( responses.insert(
request.cmd_id, request.cmd_id,
BatchResponse::Success { BatchResponse::Success {
@ -144,17 +118,13 @@ impl crate::engine::EngineManager for EngineConnection {
success: true, success: true,
})) }))
} }
WebSocketRequest::ModelingCmdReq(request) => { WebSocketRequest::ModelingCmdReq(_) => Ok(WebSocketResponse::Success(SuccessWebSocketResponse {
self.handle_command(&request.cmd, request.cmd_id, &id_to_source_range)?; request_id: Some(id),
resp: OkWebSocketResponseData::Modeling {
Ok(WebSocketResponse::Success(SuccessWebSocketResponse { modeling_response: OkModelingCmdResponse::Empty {},
request_id: Some(id), },
resp: OkWebSocketResponseData::Modeling { success: true,
modeling_response: OkModelingCmdResponse::Empty {}, })),
},
success: true,
}))
}
_ => Ok(WebSocketResponse::Success(SuccessWebSocketResponse { _ => Ok(WebSocketResponse::Success(SuccessWebSocketResponse {
request_id: Some(id), request_id: Some(id),
resp: OkWebSocketResponseData::Modeling { resp: OkWebSocketResponseData::Modeling {

View File

@ -1,22 +1,12 @@
//! Functions for setting up our WebSocket and WebRTC connections for communications with the //! Functions for setting up our WebSocket and WebRTC connections for communications with the
//! engine. //! engine.
use std::{ use std::{collections::HashMap, sync::Arc};
collections::HashMap,
sync::{Arc, Mutex},
};
use anyhow::Result; use anyhow::Result;
use indexmap::IndexMap; use indexmap::IndexMap;
use kcmc::{ use kcmc::websocket::{WebSocketRequest, WebSocketResponse};
id::ModelingCmdId,
ok_response::OkModelingCmdResponse,
websocket::{
BatchResponse, ModelingBatch, OkWebSocketResponseData, SuccessWebSocketResponse, WebSocketRequest,
WebSocketResponse,
},
ModelingCmd,
};
use kittycad_modeling_cmds as kcmc; use kittycad_modeling_cmds as kcmc;
use tokio::sync::RwLock;
use uuid::Uuid; use uuid::Uuid;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@ -54,11 +44,11 @@ extern "C" {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct EngineConnection { pub struct EngineConnection {
manager: Arc<EngineCommandManager>, manager: Arc<EngineCommandManager>,
batch: Arc<Mutex<Vec<(WebSocketRequest, SourceRange)>>>, batch: Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>, batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>,
responses: Arc<Mutex<IndexMap<Uuid, WebSocketResponse>>>, responses: Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>,
artifact_commands: Arc<Mutex<Vec<ArtifactCommand>>>, artifact_commands: Arc<RwLock<Vec<ArtifactCommand>>>,
execution_kind: Arc<Mutex<ExecutionKind>>, execution_kind: Arc<RwLock<ExecutionKind>>,
} }
// Safety: WebAssembly will only ever run in a single-threaded context. // Safety: WebAssembly will only ever run in a single-threaded context.
@ -70,66 +60,101 @@ impl EngineConnection {
#[allow(clippy::arc_with_non_send_sync)] #[allow(clippy::arc_with_non_send_sync)]
Ok(EngineConnection { Ok(EngineConnection {
manager: Arc::new(manager), manager: Arc::new(manager),
batch: Arc::new(Mutex::new(Vec::new())), batch: Arc::new(RwLock::new(Vec::new())),
batch_end: Arc::new(Mutex::new(IndexMap::new())), batch_end: Arc::new(RwLock::new(IndexMap::new())),
responses: Arc::new(Mutex::new(IndexMap::new())), responses: Arc::new(RwLock::new(IndexMap::new())),
artifact_commands: Arc::new(Mutex::new(Vec::new())), artifact_commands: Arc::new(RwLock::new(Vec::new())),
execution_kind: Default::default(), execution_kind: Default::default(),
}) })
} }
}
impl EngineConnection { async fn do_send_modeling_cmd(
fn handle_command(
&self, &self,
cmd: &ModelingCmd, id: uuid::Uuid,
cmd_id: ModelingCmdId, source_range: SourceRange,
id_to_source_range: &HashMap<Uuid, SourceRange>, cmd: WebSocketRequest,
) -> Result<(), KclError> { id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
let cmd_id = *cmd_id.as_ref(); ) -> Result<WebSocketResponse, KclError> {
let range = id_to_source_range let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
.get(&cmd_id) KclError::Engine(KclErrorDetails {
.copied() message: format!("Failed to serialize source range: {:?}", e),
.ok_or_else(|| KclError::internal(format!("Failed to get source range for command ID: {:?}", cmd_id)))?; source_ranges: vec![source_range],
})
})?;
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize modeling command: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize id to source range: {:?}", e),
source_ranges: vec![source_range],
})
})?;
// Add artifact command. let promise = self
let mut artifact_commands = self.artifact_commands.lock().unwrap(); .manager
artifact_commands.push(ArtifactCommand { .send_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
cmd_id, .map_err(|e| {
range, KclError::Engine(KclErrorDetails {
command: cmd.clone(), message: e.to_string().into(),
}); source_ranges: vec![source_range],
Ok(()) })
})?;
let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from engine: {:?}", e),
source_ranges: vec![source_range],
})
})?;
// Parse the value as a string.
let s = value.as_string().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to get string from response from engine: `{:?}`", value),
source_ranges: vec![source_range],
})
})?;
let ws_result: WebSocketResponse = serde_json::from_str(&s).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to deserialize response from engine: {:?}", e),
source_ranges: vec![source_range],
})
})?;
Ok(ws_result)
} }
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl crate::engine::EngineManager for EngineConnection { impl crate::engine::EngineManager for EngineConnection {
fn batch(&self) -> Arc<Mutex<Vec<(WebSocketRequest, SourceRange)>>> { fn batch(&self) -> Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>> {
self.batch.clone() self.batch.clone()
} }
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>> { fn batch_end(&self) -> Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>> {
self.batch_end.clone() self.batch_end.clone()
} }
fn responses(&self) -> IndexMap<Uuid, WebSocketResponse> { fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>> {
let responses = self.responses.lock().unwrap(); self.responses.clone()
responses.clone()
} }
fn take_artifact_commands(&self) -> Vec<ArtifactCommand> { fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>> {
let mut artifact_commands = self.artifact_commands.lock().unwrap(); self.artifact_commands.clone()
std::mem::take(&mut *artifact_commands)
} }
fn execution_kind(&self) -> ExecutionKind { async fn execution_kind(&self) -> ExecutionKind {
let guard = self.execution_kind.lock().unwrap(); let guard = self.execution_kind.read().await;
*guard *guard
} }
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind { async fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
let mut guard = self.execution_kind.lock().unwrap(); let mut guard = self.execution_kind.write().await;
let original = *guard; let original = *guard;
*guard = execution_kind; *guard = execution_kind;
original original
@ -214,100 +239,18 @@ impl crate::engine::EngineManager for EngineConnection {
cmd: WebSocketRequest, cmd: WebSocketRequest,
id_to_source_range: HashMap<uuid::Uuid, SourceRange>, id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
) -> Result<WebSocketResponse, KclError> { ) -> Result<WebSocketResponse, KclError> {
match &cmd { let ws_result = self
WebSocketRequest::ModelingCmdBatchReq(ModelingBatch { requests, .. }) => { .do_send_modeling_cmd(id, source_range, cmd, id_to_source_range)
for request in requests { .await?;
self.handle_command(&request.cmd, request.cmd_id, &id_to_source_range)?;
} // In isolated mode, we don't save the response.
} if self.execution_kind().await.is_isolated() {
WebSocketRequest::ModelingCmdReq(request) => { return Ok(ws_result);
self.handle_command(&request.cmd, request.cmd_id, &id_to_source_range)?;
}
_ => {}
} }
// In isolated mode, we don't send the command to the engine. let mut responses = self.responses.write().await;
if self.execution_kind().is_isolated() {
return match &cmd {
WebSocketRequest::ModelingCmdBatchReq(ModelingBatch { requests, .. }) => {
let mut responses = HashMap::with_capacity(requests.len());
for request in requests {
responses.insert(
request.cmd_id,
BatchResponse::Success {
response: OkModelingCmdResponse::Empty {},
},
);
}
Ok(WebSocketResponse::Success(SuccessWebSocketResponse {
request_id: Some(id),
resp: OkWebSocketResponseData::ModelingBatch { responses },
success: true,
}))
}
_ => Ok(WebSocketResponse::Success(SuccessWebSocketResponse {
request_id: Some(id),
resp: OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::Empty {},
},
success: true,
})),
};
}
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize source range: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize modeling command: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize id to source range: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let promise = self
.manager
.send_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from engine: {:?}", e),
source_ranges: vec![source_range],
})
})?;
// Parse the value as a string.
let s = value.as_string().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to get string from response from engine: `{:?}`", value),
source_ranges: vec![source_range],
})
})?;
let ws_result: WebSocketResponse = serde_json::from_str(&s).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to deserialize response from engine: {:?}", e),
source_ranges: vec![source_range],
})
})?;
let mut responses = self.responses.lock().unwrap();
responses.insert(id, ws_result.clone()); responses.insert(id, ws_result.clone());
drop(responses);
Ok(ws_result) Ok(ws_result)
} }

View File

@ -8,14 +8,12 @@ pub mod conn_mock;
#[cfg(feature = "engine")] #[cfg(feature = "engine")]
pub mod conn_wasm; pub mod conn_wasm;
use std::{ use std::{collections::HashMap, sync::Arc};
collections::HashMap,
sync::{Arc, Mutex},
};
use indexmap::IndexMap; use indexmap::IndexMap;
use kcmc::{ use kcmc::{
each_cmd as mcmd, each_cmd as mcmd,
id::ModelingCmdId,
length_unit::LengthUnit, length_unit::LengthUnit,
ok_response::OkModelingCmdResponse, ok_response::OkModelingCmdResponse,
shared::Color, shared::Color,
@ -28,6 +26,7 @@ use kcmc::{
use kittycad_modeling_cmds as kcmc; use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
@ -62,28 +61,38 @@ impl ExecutionKind {
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static { pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Get the batch of commands to be sent to the engine. /// Get the batch of commands to be sent to the engine.
fn batch(&self) -> Arc<Mutex<Vec<(WebSocketRequest, SourceRange)>>>; fn batch(&self) -> Arc<RwLock<Vec<(WebSocketRequest, SourceRange)>>>;
/// Get the batch of end commands to be sent to the engine. /// Get the batch of end commands to be sent to the engine.
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>; fn batch_end(&self) -> Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>;
/// Get the command responses from the engine. /// Get the command responses from the engine.
fn responses(&self) -> IndexMap<Uuid, WebSocketResponse>; fn responses(&self) -> Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>;
/// Take the artifact commands generated up to this point and clear them. /// Get the artifact commands that have accumulated so far.
fn take_artifact_commands(&self) -> Vec<ArtifactCommand>; fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>>;
/// Clear all artifact commands that have accumulated so far. /// Clear all artifact commands that have accumulated so far.
fn clear_artifact_commands(&self) { async fn clear_artifact_commands(&self) {
self.take_artifact_commands(); self.artifact_commands().write().await.clear();
}
/// Take the artifact commands that have accumulated so far and clear them.
async fn take_artifact_commands(&self) -> Vec<ArtifactCommand> {
std::mem::take(&mut *self.artifact_commands().write().await)
}
/// Take the responses that have accumulated so far and clear them.
async fn take_responses(&self) -> IndexMap<Uuid, WebSocketResponse> {
std::mem::take(&mut *self.responses().write().await)
} }
/// Get the current execution kind. /// Get the current execution kind.
fn execution_kind(&self) -> ExecutionKind; async fn execution_kind(&self) -> ExecutionKind;
/// Replace the current execution kind with a new value and return the /// Replace the current execution kind with a new value and return the
/// existing value. /// existing value.
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind; async fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind;
/// Get the default planes. /// Get the default planes.
async fn default_planes( async fn default_planes(
@ -127,7 +136,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// Ensure artifact commands are cleared so that we don't accumulate them // Ensure artifact commands are cleared so that we don't accumulate them
// across runs. // across runs.
self.clear_artifact_commands(); self.clear_artifact_commands().await;
// Do the after clear scene hook. // Do the after clear scene hook.
self.clear_scene_post_hook(id_generator, source_range).await?; self.clear_scene_post_hook(id_generator, source_range).await?;
@ -151,6 +160,27 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
Ok(()) Ok(())
} }
async fn handle_artifact_command(
&self,
cmd: &ModelingCmd,
cmd_id: ModelingCmdId,
id_to_source_range: &HashMap<Uuid, SourceRange>,
) -> Result<(), KclError> {
let cmd_id = *cmd_id.as_ref();
let range = id_to_source_range
.get(&cmd_id)
.copied()
.ok_or_else(|| KclError::internal(format!("Failed to get source range for command ID: {:?}", cmd_id)))?;
// Add artifact command.
self.artifact_commands().write().await.push(ArtifactCommand {
cmd_id,
range,
command: cmd.clone(),
});
Ok(())
}
async fn set_units( async fn set_units(
&self, &self,
units: crate::UnitLength, units: crate::UnitLength,
@ -197,13 +227,18 @@ 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> {
// In isolated mode, we don't send the command to the engine.
if self.execution_kind().await.is_isolated() {
return Ok(());
}
let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq { let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
cmd: cmd.clone(), cmd: cmd.clone(),
cmd_id: id.into(), cmd_id: id.into(),
}); });
// Add cmd to the batch. // Add cmd to the batch.
self.batch().lock().unwrap().push((req, source_range)); self.batch().write().await.push((req, source_range));
Ok(()) Ok(())
} }
@ -217,13 +252,18 @@ 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> {
// In isolated mode, we don't send the command to the engine.
if self.execution_kind().await.is_isolated() {
return Ok(());
}
let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq { let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
cmd: cmd.clone(), cmd: cmd.clone(),
cmd_id: id.into(), cmd_id: id.into(),
}); });
// Add cmd to the batch end. // Add cmd to the batch end.
self.batch_end().lock().unwrap().insert(id, (req, source_range)); self.batch_end().write().await.insert(id, (req, source_range));
Ok(()) Ok(())
} }
@ -249,11 +289,11 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
source_range: SourceRange, source_range: SourceRange,
) -> Result<OkWebSocketResponseData, crate::errors::KclError> { ) -> Result<OkWebSocketResponseData, crate::errors::KclError> {
let all_requests = if batch_end { let all_requests = if batch_end {
let mut requests = self.batch().lock().unwrap().clone(); let mut requests = self.batch().read().await.clone();
requests.extend(self.batch_end().lock().unwrap().values().cloned()); requests.extend(self.batch_end().read().await.values().cloned());
requests requests
} else { } else {
self.batch().lock().unwrap().clone() self.batch().read().await.clone()
}; };
// Return early if we have no commands to send. // Return early if we have no commands to send.
@ -304,10 +344,27 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
} }
} }
// Do the artifact commands.
for (req, _) in all_requests.iter() {
match &req {
WebSocketRequest::ModelingCmdBatchReq(ModelingBatch { requests, .. }) => {
for request in requests {
self.handle_artifact_command(&request.cmd, request.cmd_id, &id_to_source_range)
.await?;
}
}
WebSocketRequest::ModelingCmdReq(request) => {
self.handle_artifact_command(&request.cmd, request.cmd_id, &id_to_source_range)
.await?;
}
_ => {}
}
}
// Throw away the old batch queue. // Throw away the old batch queue.
self.batch().lock().unwrap().clear(); self.batch().write().await.clear();
if batch_end { if batch_end {
self.batch_end().lock().unwrap().clear(); self.batch_end().write().await.clear();
} }
// We pop off the responses to cleanup our mappings. // We pop off the responses to cleanup our mappings.
@ -596,7 +653,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Get session data, if it has been received. /// Get session data, if it has been received.
/// Returns None if the server never sent it. /// Returns None if the server never sent it.
fn get_session_data(&self) -> Option<ModelingSessionData> { async fn get_session_data(&self) -> Option<ModelingSessionData> {
None None
} }

View File

@ -9,8 +9,8 @@ use crate::{
execution::{ execution::{
annotations, annotations,
cad_op::{OpArg, Operation}, cad_op::{OpArg, Operation},
kcl_value::NumericType,
memory, memory,
memory::ProgramMemory,
state::ModuleState, state::ModuleState,
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, TagEngineInfo, BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, TagEngineInfo,
TagIdentifier, TagIdentifier,
@ -48,7 +48,7 @@ impl ExecutorContext {
let old_units = exec_state.length_unit(); let old_units = exec_state.length_unit();
exec_state.mod_local.settings.update_from_annotation(annotation)?; exec_state.mod_local.settings.update_from_annotation(annotation)?;
let new_units = exec_state.length_unit(); let new_units = exec_state.length_unit();
if !self.engine.execution_kind().is_isolated() && old_units != new_units { if !self.engine.execution_kind().await.is_isolated() && old_units != new_units {
self.engine self.engine
.set_units(new_units.into(), annotation.as_source_range()) .set_units(new_units.into(), annotation.as_source_range())
.await?; .await?;
@ -393,7 +393,7 @@ impl ExecutorContext {
exec_state.global.mod_loader.enter_module(path); exec_state.global.mod_loader.enter_module(path);
std::mem::swap(&mut exec_state.mod_local, &mut local_state); std::mem::swap(&mut exec_state.mod_local, &mut local_state);
exec_state.mut_memory().push_new_root_env(); exec_state.mut_memory().push_new_root_env();
let original_execution = self.engine.replace_execution_kind(exec_kind); let original_execution = self.engine.replace_execution_kind(exec_kind).await;
let result = self let result = self
.exec_program(program, exec_state, crate::execution::BodyType::Root) .exec_program(program, exec_state, crate::execution::BodyType::Root)
@ -406,7 +406,7 @@ impl ExecutorContext {
if !exec_kind.is_isolated() && new_units != old_units { if !exec_kind.is_isolated() && 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).await;
result result
.map_err(|err| { .map_err(|err| {
@ -437,7 +437,7 @@ impl ExecutorContext {
) -> Result<KclValue, KclError> { ) -> Result<KclValue, KclError> {
let item = match init { let item = match init {
Expr::None(none) => KclValue::from(none), Expr::None(none) => KclValue::from(none),
Expr::Literal(literal) => KclValue::from(literal), Expr::Literal(literal) => KclValue::from_literal((**literal).clone(), &exec_state.mod_local.settings),
Expr::TagDeclarator(tag) => tag.execute(exec_state).await?, Expr::TagDeclarator(tag) => tag.execute(exec_state).await?,
Expr::Identifier(identifier) => { Expr::Identifier(identifier) => {
let value = exec_state.memory().get(&identifier.name, identifier.into())?.clone(); let value = exec_state.memory().get(&identifier.name, identifier.into())?.clone();
@ -518,7 +518,10 @@ 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> {
match self { match self {
BinaryPart::Literal(literal) => Ok(literal.into()), BinaryPart::Literal(literal) => Ok(KclValue::from_literal(
(**literal).clone(),
&exec_state.mod_local.settings,
)),
BinaryPart::Identifier(identifier) => { BinaryPart::Identifier(identifier) => {
let value = exec_state.memory().get(&identifier.name, identifier.into())?; let value = exec_state.memory().get(&identifier.name, identifier.into())?;
Ok(value.clone()) Ok(value.clone())
@ -704,26 +707,32 @@ impl Node<BinaryExpression> {
BinaryOperator::Add => KclValue::Number { BinaryOperator::Add => KclValue::Number {
value: left + right, value: left + right,
meta, meta,
ty: NumericType::Unknown,
}, },
BinaryOperator::Sub => KclValue::Number { BinaryOperator::Sub => KclValue::Number {
value: left - right, value: left - right,
meta, meta,
ty: NumericType::Unknown,
}, },
BinaryOperator::Mul => KclValue::Number { BinaryOperator::Mul => KclValue::Number {
value: left * right, value: left * right,
meta, meta,
ty: NumericType::Unknown,
}, },
BinaryOperator::Div => KclValue::Number { BinaryOperator::Div => KclValue::Number {
value: left / right, value: left / right,
meta, meta,
ty: NumericType::Unknown,
}, },
BinaryOperator::Mod => KclValue::Number { BinaryOperator::Mod => KclValue::Number {
value: left % right, value: left % right,
meta, meta,
ty: NumericType::Unknown,
}, },
BinaryOperator::Pow => KclValue::Number { BinaryOperator::Pow => KclValue::Number {
value: left.powf(right), value: left.powf(right),
meta, meta,
ty: NumericType::Unknown,
}, },
BinaryOperator::Neq => KclValue::Bool { BinaryOperator::Neq => KclValue::Bool {
value: left != right, value: left != right,
@ -786,19 +795,14 @@ impl Node<UnaryExpression> {
let value = &self.argument.get_result(exec_state, ctx).await?; let value = &self.argument.get_result(exec_state, ctx).await?;
match value { match value {
KclValue::Number { value, meta: _ } => { KclValue::Number { value, ty, .. } => {
let meta = vec![Metadata {
source_range: self.into(),
}];
Ok(KclValue::Number { value: -value, meta })
}
KclValue::Int { value, meta: _ } => {
let meta = vec![Metadata { let meta = vec![Metadata {
source_range: self.into(), source_range: self.into(),
}]; }];
Ok(KclValue::Number { Ok(KclValue::Number {
value: (-value) as f64, value: -value,
meta, meta,
ty: ty.clone(),
}) })
} }
_ => Err(KclError::Semantic(KclErrorDetails { _ => Err(KclError::Semantic(KclErrorDetails {
@ -1299,8 +1303,9 @@ impl Node<ArrayRangeExpression> {
Ok(KclValue::Array { Ok(KclValue::Array {
value: range value: range
.into_iter() .into_iter()
.map(|num| KclValue::Int { .map(|num| KclValue::Number {
value: num, value: num as f64,
ty: NumericType::Unknown,
meta: meta.clone(), meta: meta.clone(),
}) })
.collect(), .collect(),
@ -1342,8 +1347,6 @@ fn article_for(s: &str) -> &'static str {
pub fn parse_number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<f64, KclError> { pub fn parse_number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<f64, KclError> {
if let KclValue::Number { value: n, .. } = &v { if let KclValue::Number { value: n, .. } = &v {
Ok(*n) Ok(*n)
} else if let KclValue::Int { value: n, .. } = &v {
Ok(*n as f64)
} else { } else {
let actual_type = v.human_friendly_type(); let actual_type = v.human_friendly_type();
let article = if actual_type.starts_with(['a', 'e', 'i', 'o', 'u']) { let article = if actual_type.starts_with(['a', 'e', 'i', 'o', 'u']) {
@ -1460,16 +1463,7 @@ fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -
})) }))
}; };
match value { match value {
KclValue::Int { value:num, meta: _ } => { KclValue::Number{value: num, .. } => {
let maybe_int: Result<usize, _> = (*num).try_into();
if let Ok(uint) = maybe_int {
Ok(Property::UInt(uint))
}
else {
make_err(format!("'{num}' is negative, so you can't index an array with it"))
}
}
KclValue::Number{value: num, meta:_} => {
let num = *num; let num = *num;
if num < 0.0 { if num < 0.0 {
return make_err(format!("'{num}' is negative, so you can't index an array with it")) return make_err(format!("'{num}' is negative, so you can't index an array with it"))
@ -1510,7 +1504,7 @@ impl Node<PipeExpression> {
fn assign_args_to_params( fn assign_args_to_params(
function_expression: NodeRef<'_, FunctionExpression>, function_expression: NodeRef<'_, FunctionExpression>,
args: Vec<Arg>, args: Vec<Arg>,
fn_memory: &mut ProgramMemory, exec_state: &mut ExecState,
) -> Result<(), KclError> { ) -> Result<(), KclError> {
let num_args = function_expression.number_of_args(); let num_args = function_expression.number_of_args();
let (min_params, max_params) = num_args.into_inner(); let (min_params, max_params) = num_args.into_inner();
@ -1530,12 +1524,15 @@ fn assign_args_to_params(
return Err(err_wrong_number_args); return Err(err_wrong_number_args);
} }
let mem = &mut exec_state.global.memory;
let settings = &exec_state.mod_local.settings;
// Add the arguments to the memory. A new call frame should have already // Add the arguments to the memory. A new call frame should have already
// been created. // been created.
for (index, param) in function_expression.params.iter().enumerate() { for (index, param) in function_expression.params.iter().enumerate() {
if let Some(arg) = args.get(index) { if let Some(arg) = args.get(index) {
// Argument was provided. // Argument was provided.
fn_memory.add( mem.add(
param.identifier.name.clone(), param.identifier.name.clone(),
arg.value.clone(), arg.value.clone(),
(&param.identifier).into(), (&param.identifier).into(),
@ -1545,9 +1542,9 @@ fn assign_args_to_params(
if let Some(ref default_val) = param.default_value { if let Some(ref default_val) = param.default_value {
// If the corresponding parameter is optional, // If the corresponding parameter is optional,
// then it's fine, the user doesn't need to supply it. // then it's fine, the user doesn't need to supply it.
fn_memory.add( mem.add(
param.identifier.name.clone(), param.identifier.name.clone(),
default_val.clone().into(), KclValue::from_default_param(default_val.clone(), settings),
(&param.identifier).into(), (&param.identifier).into(),
)?; )?;
} else { } else {
@ -1563,18 +1560,21 @@ fn assign_args_to_params(
fn assign_args_to_params_kw( fn assign_args_to_params_kw(
function_expression: NodeRef<'_, FunctionExpression>, function_expression: NodeRef<'_, FunctionExpression>,
mut args: crate::std::args::KwArgs, mut args: crate::std::args::KwArgs,
fn_memory: &mut ProgramMemory, exec_state: &mut ExecState,
) -> Result<(), KclError> { ) -> Result<(), KclError> {
// Add the arguments to the memory. A new call frame should have already // Add the arguments to the memory. A new call frame should have already
// been created. // been created.
let source_ranges = vec![function_expression.into()]; let source_ranges = vec![function_expression.into()];
let mem = &mut exec_state.global.memory;
let settings = &exec_state.mod_local.settings;
for param in function_expression.params.iter() { for param in function_expression.params.iter() {
if param.labeled { if param.labeled {
let arg = args.labeled.get(&param.identifier.name); let arg = args.labeled.get(&param.identifier.name);
let arg_val = match arg { let arg_val = match arg {
Some(arg) => arg.value.clone(), Some(arg) => arg.value.clone(),
None => match param.default_value { None => match param.default_value {
Some(ref default_val) => KclValue::from(default_val.clone()), Some(ref default_val) => KclValue::from_default_param(default_val.clone(), settings),
None => { None => {
return Err(KclError::Semantic(KclErrorDetails { return Err(KclError::Semantic(KclErrorDetails {
source_ranges, source_ranges,
@ -1586,7 +1586,7 @@ fn assign_args_to_params_kw(
} }
}, },
}; };
fn_memory.add(param.identifier.name.clone(), arg_val, (&param.identifier).into())?; mem.add(param.identifier.name.clone(), arg_val, (&param.identifier).into())?;
} else { } else {
let Some(unlabeled) = args.unlabeled.take() else { let Some(unlabeled) = args.unlabeled.take() else {
let param_name = &param.identifier.name; let param_name = &param.identifier.name;
@ -1603,7 +1603,7 @@ fn assign_args_to_params_kw(
}) })
}); });
}; };
fn_memory.add( mem.add(
param.identifier.name.clone(), param.identifier.name.clone(),
unlabeled.value.clone(), unlabeled.value.clone(),
(&param.identifier).into(), (&param.identifier).into(),
@ -1624,7 +1624,7 @@ pub(crate) async fn call_user_defined_function(
// variables shadow variables in the parent scope. The new environment's // variables shadow variables in the parent scope. The new environment's
// parent should be the environment of the closure. // parent should be the environment of the closure.
exec_state.mut_memory().push_new_env_for_call(memory); exec_state.mut_memory().push_new_env_for_call(memory);
if let Err(e) = assign_args_to_params(function_expression, args, exec_state.mut_memory()) { if let Err(e) = assign_args_to_params(function_expression, args, exec_state) {
exec_state.mut_memory().pop_env(); exec_state.mut_memory().pop_env();
return Err(e); return Err(e);
} }
@ -1657,7 +1657,7 @@ pub(crate) async fn call_user_defined_function_kw(
// variables shadow variables in the parent scope. The new environment's // variables shadow variables in the parent scope. The new environment's
// parent should be the environment of the closure. // parent should be the environment of the closure.
exec_state.mut_memory().push_new_env_for_call(memory); exec_state.mut_memory().push_new_env_for_call(memory);
if let Err(e) = assign_args_to_params_kw(function_expression, args, exec_state.mut_memory()) { if let Err(e) = assign_args_to_params_kw(function_expression, args, exec_state) {
exec_state.mut_memory().pop_env(); exec_state.mut_memory().pop_env();
return Err(e); return Err(e);
} }
@ -1720,19 +1720,19 @@ impl JsonSchema for FunctionParam<'_> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*;
use crate::{ use crate::{
execution::parse_execute, execution::{memory::ProgramMemory, parse_execute},
parsing::ast::types::{DefaultParamVal, Identifier, Parameter}, parsing::ast::types::{DefaultParamVal, Identifier, Parameter},
}; };
use super::*;
#[test] #[test]
fn test_assign_args_to_params() { fn test_assign_args_to_params() {
// Set up a little framework for this test. // Set up a little framework for this test.
fn mem(number: usize) -> KclValue { fn mem(number: usize) -> KclValue {
KclValue::Int { KclValue::Number {
value: number as i64, value: number as f64,
ty: NumericType::count(),
meta: Default::default(), meta: Default::default(),
} }
} }
@ -1838,8 +1838,8 @@ mod test {
digest: None, digest: None,
}); });
let args = args.into_iter().map(Arg::synthetic).collect(); let args = args.into_iter().map(Arg::synthetic).collect();
let mut actual = ProgramMemory::new(); let mut exec_state = ExecState::new(&Default::default());
let actual = assign_args_to_params(func_expr, args, &mut actual).map(|_| actual); let actual = assign_args_to_params(func_expr, args, &mut exec_state).map(|_| exec_state.global.memory);
assert_eq!( assert_eq!(
actual, expected, actual, expected,
"failed test '{test_name}':\ngot {actual:?}\nbut expected\n{expected:?}" "failed test '{test_name}':\ngot {actual:?}\nbut expected\n{expected:?}"

View File

@ -289,7 +289,7 @@ pub struct PreImportedGeometry {
} }
pub async fn send_to_engine(pre: PreImportedGeometry, ctxt: &ExecutorContext) -> Result<ImportedGeometry, KclError> { pub async fn send_to_engine(pre: PreImportedGeometry, ctxt: &ExecutorContext) -> Result<ImportedGeometry, KclError> {
if ctxt.no_engine_commands() { if ctxt.no_engine_commands().await {
return Ok(ImportedGeometry { return Ok(ImportedGeometry {
id: pre.id, id: pre.id,
value: pre.command.files.iter().map(|f| f.path.to_string()).collect(), value: pre.command.files.iter().map(|f| f.path.to_string()).collect(),

View File

@ -12,14 +12,16 @@ use crate::{
TagIdentifier, TagIdentifier,
}, },
parsing::{ parsing::{
ast::types::{FunctionExpression, KclNone, LiteralValue, TagDeclarator, TagNode}, ast::types::{
DefaultParamVal, FunctionExpression, KclNone, Literal, LiteralValue, Node, TagDeclarator, TagNode,
},
token::NumericSuffix, token::NumericSuffix,
}, },
std::{args::Arg, FnAsArg}, std::{args::Arg, FnAsArg},
ExecutorContext, KclError, ModuleId, SourceRange, ExecutorContext, KclError, ModuleId, SourceRange,
}; };
use super::memory::EnvironmentRef; use super::{memory::EnvironmentRef, MetaSettings};
pub type KclObjectFields = HashMap<String, KclValue>; pub type KclObjectFields = HashMap<String, KclValue>;
@ -40,11 +42,7 @@ pub enum KclValue {
}, },
Number { Number {
value: f64, value: f64,
#[serde(rename = "__meta")] ty: NumericType,
meta: Vec<Metadata>,
},
Int {
value: i64,
#[serde(rename = "__meta")] #[serde(rename = "__meta")]
meta: Vec<Metadata>, meta: Vec<Metadata>,
}, },
@ -168,7 +166,6 @@ impl From<KclValue> for Vec<SourceRange> {
KclValue::Face { value } => to_vec_sr(&value.meta), KclValue::Face { value } => to_vec_sr(&value.meta),
KclValue::Bool { meta, .. } => to_vec_sr(&meta), KclValue::Bool { meta, .. } => to_vec_sr(&meta),
KclValue::Number { meta, .. } => to_vec_sr(&meta), KclValue::Number { meta, .. } => to_vec_sr(&meta),
KclValue::Int { meta, .. } => to_vec_sr(&meta),
KclValue::String { meta, .. } => to_vec_sr(&meta), KclValue::String { meta, .. } => to_vec_sr(&meta),
KclValue::Array { meta, .. } => to_vec_sr(&meta), KclValue::Array { meta, .. } => to_vec_sr(&meta),
KclValue::Object { meta, .. } => to_vec_sr(&meta), KclValue::Object { meta, .. } => to_vec_sr(&meta),
@ -200,7 +197,6 @@ impl From<&KclValue> for Vec<SourceRange> {
KclValue::Face { value } => to_vec_sr(&value.meta), KclValue::Face { value } => to_vec_sr(&value.meta),
KclValue::Bool { meta, .. } => to_vec_sr(meta), KclValue::Bool { meta, .. } => to_vec_sr(meta),
KclValue::Number { meta, .. } => to_vec_sr(meta), KclValue::Number { meta, .. } => to_vec_sr(meta),
KclValue::Int { meta, .. } => to_vec_sr(meta),
KclValue::String { meta, .. } => to_vec_sr(meta), KclValue::String { meta, .. } => to_vec_sr(meta),
KclValue::Uuid { meta, .. } => to_vec_sr(meta), KclValue::Uuid { meta, .. } => to_vec_sr(meta),
KclValue::Array { meta, .. } => to_vec_sr(meta), KclValue::Array { meta, .. } => to_vec_sr(meta),
@ -217,8 +213,7 @@ impl KclValue {
match self { match self {
KclValue::Uuid { value: _, meta } => meta.clone(), KclValue::Uuid { value: _, meta } => meta.clone(),
KclValue::Bool { value: _, meta } => meta.clone(), KclValue::Bool { value: _, meta } => meta.clone(),
KclValue::Number { value: _, meta } => meta.clone(), KclValue::Number { meta, .. } => meta.clone(),
KclValue::Int { value: _, meta } => meta.clone(),
KclValue::String { value: _, meta } => meta.clone(), KclValue::String { value: _, meta } => meta.clone(),
KclValue::Array { value: _, meta } => meta.clone(), KclValue::Array { value: _, meta } => meta.clone(),
KclValue::Object { value: _, meta } => meta.clone(), KclValue::Object { value: _, meta } => meta.clone(),
@ -297,7 +292,6 @@ impl KclValue {
KclValue::Face { .. } => "Face", KclValue::Face { .. } => "Face",
KclValue::Bool { .. } => "boolean (true/false value)", KclValue::Bool { .. } => "boolean (true/false value)",
KclValue::Number { .. } => "number", KclValue::Number { .. } => "number",
KclValue::Int { .. } => "integer",
KclValue::String { .. } => "string (text)", KclValue::String { .. } => "string (text)",
KclValue::Array { .. } => "array (list)", KclValue::Array { .. } => "array (list)",
KclValue::Object { .. } => "object", KclValue::Object { .. } => "object",
@ -307,14 +301,29 @@ impl KclValue {
} }
} }
pub(crate) fn from_literal(literal: LiteralValue, meta: Vec<Metadata>) -> Self { pub(crate) fn from_literal(literal: Node<Literal>, settings: &MetaSettings) -> Self {
match literal { let meta = vec![literal.metadata()];
LiteralValue::Number { value, .. } => KclValue::Number { value, meta }, match literal.inner.value {
LiteralValue::Number { value, suffix } => KclValue::Number {
value,
meta,
ty: NumericType::from_parsed(suffix, settings),
},
LiteralValue::String(value) => KclValue::String { value, meta }, LiteralValue::String(value) => KclValue::String { value, meta },
LiteralValue::Bool(value) => KclValue::Bool { value, meta }, LiteralValue::Bool(value) => KclValue::Bool { value, meta },
} }
} }
pub(crate) fn from_default_param(param: DefaultParamVal, settings: &MetaSettings) -> Self {
match param {
DefaultParamVal::Literal(lit) => Self::from_literal(lit, settings),
DefaultParamVal::KclNone(none) => KclValue::KclNone {
value: none,
meta: Default::default(),
},
}
}
pub(crate) fn map_env_ref(&self, env_map: &HashMap<EnvironmentRef, EnvironmentRef>) -> Self { pub(crate) fn map_env_ref(&self, env_map: &HashMap<EnvironmentRef, EnvironmentRef>) -> Self {
let mut result = self.clone(); let mut result = self.clone();
if let KclValue::Function { ref mut memory, .. } = result { if let KclValue::Function { ref mut memory, .. } = result {
@ -327,20 +336,30 @@ impl KclValue {
/// Put the number into a KCL value. /// Put the number into a KCL value.
pub const fn from_number(f: f64, meta: Vec<Metadata>) -> Self { pub const fn from_number(f: f64, meta: Vec<Metadata>) -> Self {
Self::Number { value: f, meta } Self::Number {
value: f,
meta,
ty: NumericType::Unknown,
}
}
pub const fn from_number_with_type(f: f64, ty: NumericType, meta: Vec<Metadata>) -> Self {
Self::Number { value: f, meta, ty }
} }
/// Put the point into a KCL value. /// Put the point into a KCL value.
pub fn from_point2d(p: [f64; 2], meta: Vec<Metadata>) -> Self { pub fn from_point2d(p: [f64; 2], ty: NumericType, meta: Vec<Metadata>) -> Self {
Self::Array { Self::Array {
value: vec![ value: vec![
Self::Number { Self::Number {
value: p[0], value: p[0],
meta: meta.clone(), meta: meta.clone(),
ty: ty.clone(),
}, },
Self::Number { Self::Number {
value: p[1], value: p[1],
meta: meta.clone(), meta: meta.clone(),
ty,
}, },
], ],
meta, meta,
@ -349,7 +368,6 @@ impl KclValue {
pub(crate) fn as_usize(&self) -> Option<usize> { pub(crate) fn as_usize(&self) -> Option<usize> {
match self { match self {
KclValue::Int { value, .. } if *value > 0 => Some(*value as usize),
KclValue::Number { value, .. } => crate::try_f64_to_usize(*value), KclValue::Number { value, .. } => crate::try_f64_to_usize(*value),
_ => None, _ => None,
} }
@ -357,7 +375,6 @@ impl KclValue {
pub fn as_int(&self) -> Option<i64> { pub fn as_int(&self) -> Option<i64> {
match self { match self {
KclValue::Int { value, .. } => Some(*value),
KclValue::Number { value, .. } => crate::try_f64_to_i64(*value), KclValue::Number { value, .. } => crate::try_f64_to_i64(*value),
_ => None, _ => None,
} }
@ -438,10 +455,8 @@ impl KclValue {
} }
pub fn as_f64(&self) -> Option<f64> { pub fn as_f64(&self) -> Option<f64> {
if let KclValue::Number { value, meta: _ } = &self { if let KclValue::Number { value, .. } = &self {
Some(*value) Some(*value)
} else if let KclValue::Int { value, meta: _ } = &self {
Some(*value as f64)
} else { } else {
None None
} }
@ -606,6 +621,73 @@ impl KclValue {
} }
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum NumericType {
// Specified by the user (directly or indirectly)
Known(UnitType),
// Unspecified, using defaults
Default { len: UnitLen, angle: UnitAngle },
// Exceeded the ability of the type system to track.
Unknown,
// Type info has been explicitly cast away.
Any,
}
impl NumericType {
pub fn count() -> Self {
NumericType::Known(UnitType::Count)
}
pub fn combine(self, other: &NumericType) -> NumericType {
if &self == other {
self
} else {
NumericType::Unknown
}
}
pub fn from_parsed(suffix: NumericSuffix, settings: &super::MetaSettings) -> Self {
match suffix {
NumericSuffix::None => NumericType::Default {
len: settings.default_length_units,
angle: settings.default_angle_units,
},
NumericSuffix::Count => NumericType::Known(UnitType::Count),
NumericSuffix::Mm => NumericType::Known(UnitType::Length(UnitLen::Mm)),
NumericSuffix::Cm => NumericType::Known(UnitType::Length(UnitLen::Cm)),
NumericSuffix::M => NumericType::Known(UnitType::Length(UnitLen::M)),
NumericSuffix::Inch => NumericType::Known(UnitType::Length(UnitLen::Inches)),
NumericSuffix::Ft => NumericType::Known(UnitType::Length(UnitLen::Feet)),
NumericSuffix::Yd => NumericType::Known(UnitType::Length(UnitLen::Yards)),
NumericSuffix::Deg => NumericType::Known(UnitType::Angle(UnitAngle::Degrees)),
NumericSuffix::Rad => NumericType::Known(UnitType::Angle(UnitAngle::Radians)),
}
}
}
impl From<UnitLen> for NumericType {
fn from(value: UnitLen) -> Self {
NumericType::Known(UnitType::Length(value))
}
}
impl From<UnitAngle> for NumericType {
fn from(value: UnitAngle) -> Self {
NumericType::Known(UnitType::Angle(value))
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
pub enum UnitType {
Count,
Length(UnitLen),
Angle(UnitAngle),
}
// 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, Default, 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)]

View File

@ -845,6 +845,8 @@ mod env {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::execution::kcl_value::NumericType;
use super::*; use super::*;
fn sr() -> SourceRange { fn sr() -> SourceRange {
@ -852,8 +854,9 @@ mod test {
} }
fn val(value: i64) -> KclValue { fn val(value: i64) -> KclValue {
KclValue::Int { KclValue::Number {
value, value: value as f64,
ty: NumericType::count(),
meta: Vec::new(), meta: Vec::new(),
} }
} }
@ -861,14 +864,14 @@ mod test {
#[track_caller] #[track_caller]
fn assert_get(mem: &ProgramMemory, key: &str, n: i64) { fn assert_get(mem: &ProgramMemory, key: &str, n: i64) {
match mem.get(key, sr()).unwrap() { match mem.get(key, sr()).unwrap() {
KclValue::Int { value, .. } => assert_eq!(*value, n), KclValue::Number { value, .. } => assert_eq!(*value as i64, n),
_ => unreachable!(), _ => unreachable!(),
} }
} }
fn expect_int(value: &KclValue) -> Option<i64> { fn expect_small_number(value: &KclValue) -> Option<i64> {
match value { match value {
KclValue::Int { value, .. } => Some(*value), KclValue::Number { value, .. } if value > &0.0 && value < &10.0 => Some(*value as i64),
_ => None, _ => None,
} }
} }
@ -876,7 +879,7 @@ mod test {
#[track_caller] #[track_caller]
fn assert_get_from(mem: &ProgramMemory, key: &str, n: i64, snapshot: EnvironmentRef) { fn assert_get_from(mem: &ProgramMemory, key: &str, n: i64, snapshot: EnvironmentRef) {
match mem.get_from(key, snapshot, sr()).unwrap() { match mem.get_from(key, snapshot, sr()).unwrap() {
KclValue::Int { value, .. } => assert_eq!(*value, n), KclValue::Number { value, .. } => assert_eq!(*value as i64, n),
_ => unreachable!(), _ => unreachable!(),
} }
} }
@ -1127,7 +1130,7 @@ mod test {
assert_get_from(mem, "b", 3, sn3); assert_get_from(mem, "b", 3, sn3);
assert_get_from(mem, "b", 4, sn4); assert_get_from(mem, "b", 4, sn4);
let vals: Vec<_> = mem.walk_call_stack().filter_map(expect_int).collect(); let vals: Vec<_> = mem.walk_call_stack().filter_map(expect_small_number).collect();
let expected = [6, 1, 3, 1, 7]; let expected = [6, 1, 3, 1, 7];
assert_eq!(vals, expected); assert_eq!(vals, expected);
@ -1136,7 +1139,7 @@ mod test {
mem.get_from("b", sn1, sr()).unwrap_err(); mem.get_from("b", sn1, sr()).unwrap_err();
assert_get_from(mem, "b", 3, sn2); assert_get_from(mem, "b", 3, sn2);
let vals: Vec<_> = mem.walk_call_stack().filter_map(expect_int).collect(); let vals: Vec<_> = mem.walk_call_stack().filter_map(expect_small_number).collect();
let expected = [1, 7]; let expected = [1, 7];
assert_eq!(vals, expected); assert_eq!(vals, expected);

View File

@ -484,8 +484,8 @@ impl ExecutorContext {
} }
/// Returns true if we should not send engine commands for any reason. /// Returns true if we should not send engine commands for any reason.
pub fn no_engine_commands(&self) -> bool { pub async fn no_engine_commands(&self) -> bool {
self.is_mock() || self.engine.execution_kind().is_isolated() self.is_mock() || self.engine.execution_kind().await.is_isolated()
} }
pub async fn send_clear_scene( pub async fn send_clear_scene(
@ -713,7 +713,7 @@ impl ExecutorContext {
"Post interpretation KCL memory stats: {:#?}", "Post interpretation KCL memory stats: {:#?}",
exec_state.memory().stats exec_state.memory().stats
)); ));
let session_data = self.engine.get_session_data(); let session_data = self.engine.get_session_data().await;
Ok(session_data) Ok(session_data)
} }
@ -734,8 +734,11 @@ impl ExecutorContext {
exec_state exec_state
.global .global
.artifact_commands .artifact_commands
.extend(self.engine.take_artifact_commands()); .extend(self.engine.take_artifact_commands().await);
exec_state.global.artifact_responses.extend(self.engine.responses()); exec_state
.global
.artifact_responses
.extend(self.engine.take_responses().await);
// Build the artifact graph. // Build the artifact graph.
match build_artifact_graph( match build_artifact_graph(
&exec_state.global.artifact_commands, &exec_state.global.artifact_commands,

View File

@ -1581,7 +1581,7 @@ pub struct CallExpression {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(rename_all = "camelCase", tag = "type")]
pub struct CallExpressionKw { pub struct CallExpressionKw {
pub callee: Node<Identifier>, pub callee: Node<Identifier>,
pub unlabeled: Option<Expr>, pub unlabeled: Option<Expr>,
@ -1591,6 +1591,9 @@ pub struct CallExpressionKw {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)] #[ts(optional)]
pub digest: Option<Digest>, pub digest: Option<Digest>,
#[serde(default, skip_serializing_if = "NonCodeMeta::is_empty")]
pub non_code_meta: NonCodeMeta,
} }
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -1714,6 +1717,7 @@ impl CallExpressionKw {
unlabeled, unlabeled,
arguments, arguments,
digest: None, digest: None,
non_code_meta: Default::default(),
})) }))
} }
@ -2080,30 +2084,6 @@ impl Literal {
} }
} }
impl From<Node<Literal>> for KclValue {
fn from(literal: Node<Literal>) -> Self {
let meta = vec![literal.metadata()];
match literal.inner.value {
LiteralValue::Number { value, .. } => KclValue::Number { value, meta },
LiteralValue::String(value) => KclValue::String { value, meta },
LiteralValue::Bool(value) => KclValue::Bool { value, meta },
}
}
}
impl From<&Node<Literal>> for KclValue {
fn from(literal: &Node<Literal>) -> Self {
Self::from(literal.to_owned())
}
}
impl From<&BoxNode<Literal>> for KclValue {
fn from(literal: &BoxNode<Literal>) -> Self {
let b: &Node<Literal> = literal;
Self::from(b)
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)] #[ts(export)]
#[serde(tag = "type")] #[serde(tag = "type")]
@ -3088,20 +3068,7 @@ pub enum FnArgType {
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
pub enum DefaultParamVal { pub enum DefaultParamVal {
KclNone(KclNone), KclNone(KclNone),
Literal(Literal), Literal(Node<Literal>),
}
// TODO: This should actually take metadata.
impl From<DefaultParamVal> for KclValue {
fn from(v: DefaultParamVal) -> Self {
match v {
DefaultParamVal::KclNone(kcl_none) => Self::KclNone {
value: kcl_none,
meta: Default::default(),
},
DefaultParamVal::Literal(literal) => Self::from_literal(literal.value, Vec::new()),
}
}
} }
impl DefaultParamVal { impl DefaultParamVal {

View File

@ -882,6 +882,17 @@ fn property_separator(i: &mut TokenSlice) -> PResult<()> {
.parse_next(i) .parse_next(i)
} }
/// Match something that separates the labeled arguments of a fn call.
fn labeled_arg_separator(i: &mut TokenSlice) -> PResult<()> {
alt((
// Normally you need a comma.
comma_sep,
// But, if the argument list is ending, no need for a comma.
peek(preceded(opt(whitespace), close_paren)).void(),
))
.parse_next(i)
}
/// Parse a KCL object value. /// Parse a KCL object value.
pub(crate) fn object(i: &mut TokenSlice) -> PResult<Node<ObjectExpression>> { pub(crate) fn object(i: &mut TokenSlice) -> PResult<Node<ObjectExpression>> {
let open = open_brace(i)?; let open = open_brace(i)?;
@ -2496,14 +2507,6 @@ fn labeled_argument(i: &mut TokenSlice) -> PResult<LabeledArg> {
.parse_next(i) .parse_next(i)
} }
/// Arguments are passed into a function,
/// preceded by the name of the parameter (the label).
fn labeled_arguments(i: &mut TokenSlice) -> PResult<Vec<LabeledArg>> {
separated(0.., labeled_argument, comma_sep)
.context(expected("function arguments"))
.parse_next(i)
}
/// A type of a function argument. /// A type of a function argument.
/// This can be: /// This can be:
/// - a primitive type, e.g. 'number' or 'string' or 'bool' /// - a primitive type, e.g. 'number' or 'string' or 'bool'
@ -2579,7 +2582,7 @@ fn parameter(i: &mut TokenSlice) -> PResult<ParamDescription> {
arg_name, arg_name,
type_, type_,
default_value: match (question_mark.is_some(), default_literal) { default_value: match (question_mark.is_some(), default_literal) {
(true, Some(lit)) => Some(DefaultParamVal::Literal(lit.inner)), (true, Some(lit)) => Some(DefaultParamVal::Literal(*lit)),
(true, None) => Some(DefaultParamVal::none()), (true, None) => Some(DefaultParamVal::none()),
(false, None) => None, (false, None) => None,
(false, Some(lit)) => { (false, Some(lit)) => {
@ -2783,7 +2786,28 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
ignore_whitespace(i); ignore_whitespace(i);
let initial_unlabeled_arg = opt((expression, comma, opt(whitespace)).map(|(arg, _, _)| arg)).parse_next(i)?; let initial_unlabeled_arg = opt((expression, comma, opt(whitespace)).map(|(arg, _, _)| arg)).parse_next(i)?;
let args = labeled_arguments(i)?; let args: Vec<_> = repeat(
0..,
alt((
terminated(non_code_node.map(NonCodeOr::NonCode), whitespace),
terminated(labeled_argument, labeled_arg_separator).map(NonCodeOr::Code),
)),
)
.parse_next(i)?;
let (args, non_code_nodes): (Vec<_>, BTreeMap<usize, _>) = args.into_iter().enumerate().fold(
(Vec::new(), BTreeMap::new()),
|(mut args, mut non_code_nodes), (i, e)| {
match e {
NonCodeOr::NonCode(x) => {
non_code_nodes.insert(i, vec![x]);
}
NonCodeOr::Code(x) => {
args.push(x);
}
}
(args, non_code_nodes)
},
);
if let Some(std_fn) = crate::std::get_stdlib_fn(&fn_name.name) { if let Some(std_fn) = crate::std::get_stdlib_fn(&fn_name.name) {
let just_args: Vec<_> = args.iter().collect(); let just_args: Vec<_> = args.iter().collect();
typecheck_all_kw(std_fn, &just_args)?; typecheck_all_kw(std_fn, &just_args)?;
@ -2792,6 +2816,10 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
opt(comma_sep).parse_next(i)?; opt(comma_sep).parse_next(i)?;
let end = close_paren.parse_next(i)?.end; let end = close_paren.parse_next(i)?.end;
let non_code_meta = NonCodeMeta {
non_code_nodes,
..Default::default()
};
Ok(Node { Ok(Node {
start: fn_name.start, start: fn_name.start,
end, end,
@ -2801,6 +2829,7 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
unlabeled: initial_unlabeled_arg, unlabeled: initial_unlabeled_arg,
arguments: args, arguments: args,
digest: None, digest: None,
non_code_meta,
}, },
outer_attrs: Vec::new(), outer_attrs: Vec::new(),
}) })
@ -4390,14 +4419,6 @@ let myBox = box([0,0], -3, -16, -10)
crate::parsing::top_level_parse(some_program_string).unwrap(); crate::parsing::top_level_parse(some_program_string).unwrap();
} }
#[test]
fn arg_labels() {
let input = r#"length: 3"#;
let module_id = ModuleId::default();
let tokens = crate::parsing::token::lex(input, module_id).unwrap();
super::labeled_arguments(&mut tokens.as_slice()).unwrap();
}
#[test] #[test]
fn kw_fn() { fn kw_fn() {
for input in ["val = foo(x, y = z)", "val = foo(y = z)"] { for input in ["val = foo(x, y = z)", "val = foo(y = z)"] {
@ -4879,6 +4900,22 @@ my14 = 4 ^ 2 - 3 ^ 2 * 2
r#"fn foo(x?: number = 2) { return 1 }"# r#"fn foo(x?: number = 2) { return 1 }"#
); );
snapshot_test!(kw_function_call_in_pipe, r#"val = 1 |> f(arg = x)"#); snapshot_test!(kw_function_call_in_pipe, r#"val = 1 |> f(arg = x)"#);
snapshot_test!(
kw_function_call_multiline,
r#"val = f(
arg = x,
foo = x,
bar = x,
)"#
);
snapshot_test!(
kw_function_call_multiline_with_comments,
r#"val = f(
arg = x,
// foo = x,
bar = x,
)"#
);
} }
#[allow(unused)] #[allow(unused)]

View File

@ -0,0 +1,86 @@
---
source: kcl/src/parsing/parser.rs
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 87,
"id": {
"end": 3,
"name": "val",
"start": 0,
"type": "Identifier"
},
"init": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "arg"
},
"arg": {
"end": 29,
"name": "x",
"start": 28,
"type": "Identifier",
"type": "Identifier"
}
},
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "foo"
},
"arg": {
"end": 51,
"name": "x",
"start": 50,
"type": "Identifier",
"type": "Identifier"
}
},
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "bar"
},
"arg": {
"end": 73,
"name": "x",
"start": 72,
"type": "Identifier",
"type": "Identifier"
}
}
],
"callee": {
"end": 7,
"name": "f",
"start": 6,
"type": "Identifier"
},
"end": 87,
"start": 6,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
"start": 0,
"type": "VariableDeclarator"
},
"end": 87,
"kind": "const",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 87,
"start": 0
}

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