Compare commits

...

15 Commits

Author SHA1 Message Date
252fde9be6 demo zoom not working 2024-10-02 08:34:56 +10:00
cd91774881 add unit tests for stdlib autocompletes etc (#4066)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-10-01 13:11:09 -07:00
88cd27425e Initial kcl to core codegen (C++) util (#2999)
* wip on this

* wip on kcl-to-core tool

* Update conn_mock_core.rs

* stopping point

* wip

* use enum for is_mock for added versatility

* get at least circular patterns working to finish POC

* fmt

* fmt part 2

* Update Cargo.lock

* quick fix for tests

* quick fix

* more fixes

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

* get more cases of this working

* Update src/wasm-lib/kcl-to-core/src/conn_mock_core.rs

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

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

* kcl core gen plane code by default

* fmt

* fix build?

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

* Fix clippy errors

* Remove unneeded clippy allow

* post merge build fix attempt

* Add missing indexmap dependency

* Fix to use IndexMap instead of HashMap

* Migrate to kittycad-modeling-cmds

* fix build

* fmt

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

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

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

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

* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest)"

This reverts commit 991cdde15e.

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

This reverts commit a4ae03c740.

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

This reverts commit 9a082e7c9f.

* Regenerate derive_docs

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

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

This reverts commit a8bdb35627.

* Fix e2e test failing by masking state indicator

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

* Confirm snapshot change

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

* Confirm snapshot change

* Fix build from last merge with main

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-10-01 19:45:01 +00:00
a284a270b7 Fix e2e test failing by masking state indicator (#4064)
* Fix e2e test failing by masking state indicator

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

* Confirm snapshot change

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

* Confirm snapshot change

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-01 18:14:12 +00:00
e2f5ad47a2 bump reqwests and http and kittycad.rs (#4065)
bump reqwests

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-10-01 11:05:44 -07:00
2a3693651a KCL stdlib 'map' function (#4054)
I had to revert https://github.com/KittyCAD/modeling-app/pull/4031 because it broke syntax highlighting. This is the same PR, but updated to fix syntax highlighting.

Highlighting broke because the KCL LSP could not determine how to autocomplete the `map` function. The first argument of `map` is `[KclValue]` and the LSP doesn't know any good suggestions for "any KCL value", so it error'd out. I am using the value `[0..9]` for this case now. Tested that syntax highlighting works again.
2024-10-01 08:50:23 -05:00
9ca49c6366 Split artifacts per arch and re-enable updater for nightly builds (#3921)
* Split builds per os and per architecture to minimize artifacts size
Fixes #3920

* Add missing if case; Add nightly build updater support

* Fix linux x86_64
2024-10-01 08:25:41 -04:00
2a2e4a8b63 Revert "KCL stdlib 'map' function (#4031)"
It broke the KCL LSP.

This reverts commit a3c0a2b03b.
2024-09-30 21:03:50 -05:00
258bce8adc Rename arrayReduce to reduce (#4030)
I think when we make reduce work with objects,
we'll either keep the same function and make it
polymorphic, or we'll have namespaces/modules and
use Array.reduce and Object.reduce. Either way this
name can be changed.
2024-09-30 20:03:28 -05:00
a3c0a2b03b KCL stdlib 'map' function (#4031)
KCL: New 'map' stdlib function.

map(f, [1, 2, ...]) == [f(1), f(2), ...]
2024-09-30 19:37:41 -05:00
2ed2e9cf86 Bump kittycad from 0.3.20 to 0.3.22 in /src/wasm-lib (#4036)
Bumps [kittycad](https://github.com/KittyCAD/kittycad.rs) from 0.3.20 to 0.3.22.
- [Release notes](https://github.com/KittyCAD/kittycad.rs/releases)
- [Commits](https://github.com/KittyCAD/kittycad.rs/compare/v0.3.20...v0.3.22)

---
updated-dependencies:
- dependency-name: kittycad
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 15:35:07 -07:00
438d1ec746 Bump syn from 2.0.77 to 2.0.79 in /src/wasm-lib (#4035)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.77 to 2.0.79.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.77...2.0.79)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 15:34:57 -07:00
5112b48324 Should exit sketchMode when creating new file in the file tree pane (#3993)
* fix new file sketch mode issue

* initial extron app fixture

* Add tests for exiting sketch mode on file tree actions

* organise files

* before all after all clean up

* tweak after each

* makes typedKeys as unsafe

* update mask for draft line snapshots

* fix mask

* add fix again
2024-09-30 21:56:04 +00:00
8cb17a8936 fix token.txt (#4037)
* fix token.txt

* try and make windowns happy

* fmt
2024-09-30 21:01:21 +00:00
125207f60c KCL: If-else expressions (#4022)
Closes https://github.com/KittyCAD/modeling-app/issues/3677

You can review each commit separately, they're neat commits with logical purpose in each.

Future enhancements:

- https://github.com/KittyCAD/modeling-app/issues/4015
- https://github.com/KittyCAD/modeling-app/issues/4020
- Right now the parser errors are not very good, especially if you forget to put an expression in the end of an if/else block
2024-09-30 15:40:50 -05:00
113 changed files with 19468 additions and 4923 deletions

View File

@ -51,8 +51,6 @@ jobs:
run: |
VERSION=$(date +'%-y.%-m.%-d') yarn bump-jsons
# TODO: see if we need to inject updater nightly URL here https://dl.zoo.dev/releases/modeling-app/nightly/last_update.json
- uses: actions/upload-artifact@v3
with:
name: prepared-files
@ -63,12 +61,25 @@ jobs:
- id: export_version
run: echo "version=`cat package.json | jq -r '.version'`" >> "$GITHUB_OUTPUT"
- name: Prepare electron-builder.yml file for nightly
if: ${{ github.event_name == 'schedule' }}
run: |
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/nightly"' electron-builder.yml
- uses: actions/upload-artifact@v3
if: ${{ github.event_name == 'schedule' }}
with:
name: prepared-files-nightly
path: |
electron-builder.yml
- name: Prepare electron-builder.yml file for updater test
if: ${{ env.CUT_RELEASE_PR == 'true' }}
run: |
yq -i '.publish[0].url = "https://dl.zoo.dev/releases/modeling-app/updater-test"' electron-builder.yml
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: prepared-files-updater-test
path: |
@ -108,6 +119,16 @@ jobs:
mkdir src/wasm-lib/pkg
cp prepared-files/src/wasm-lib/pkg/wasm_lib* src/wasm-lib/pkg
- uses: actions/download-artifact@v3
if: ${{ github.event_name == 'schedule' }}
name: prepared-files-nightly
- name: Copy updated electron-builder.yml file for nightly build
if: ${{ github.event_name == 'schedule' }}
run: |
ls -R prepared-files-nightly
cp prepared-files-nightly/electron-builder.yml electron-builder.yml
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
@ -152,11 +173,17 @@ jobs:
- uses: actions/upload-artifact@v3
with:
name: out-${{ matrix.os }}
name: out-arm64-${{ matrix.os }}
path: |
out/Zoo*.*
out/Zoo*arm64*.*
out/latest*.yml
- uses: actions/upload-artifact@v3
with:
name: out-x64-${{ matrix.os }}
path: |
out/Zoo*x*64*.*
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
- uses: actions/download-artifact@v3
@ -176,10 +203,16 @@ jobs:
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: updater-test-${{ matrix.os }}
name: updater-test-arm64-${{ matrix.os }}
path: |
out/Zoo*.*
out/latest*.yml
out/Zoo*arm64*.*
- uses: actions/upload-artifact@v3
if: ${{ env.CUT_RELEASE_PR == 'true' }}
with:
name: updater-test-x64-${{ matrix.os }}
path: |
out/Zoo*x64*.*
publish-apps-release:
@ -201,17 +234,32 @@ jobs:
- uses: actions/download-artifact@v3
with:
name: out-windows-2022
name: out-arm64-windows-2022
path: out
- uses: actions/download-artifact@v3
with:
name: out-macos-14
name: out-x64-windows-2022
path: out
- uses: actions/download-artifact@v3
with:
name: out-ubuntu-22.04
name: out-arm64-macos-14
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-macos-14
path: out
- uses: actions/download-artifact@v3
with:
name: out-arm64-ubuntu-22.04
path: out
- uses: actions/download-artifact@v3
with:
name: out-x64-ubuntu-22.04
path: out
- name: Generate the download static endpoint

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,6 @@ layout: manual
* [`angledLineToX`](kcl/angledLineToX)
* [`angledLineToY`](kcl/angledLineToY)
* [`arc`](kcl/arc)
* [`arrayReduce`](kcl/arrayReduce)
* [`asin`](kcl/asin)
* [`assert`](kcl/assert)
* [`assertEqual`](kcl/assertEqual)
@ -62,6 +61,7 @@ layout: manual
* [`log10`](kcl/log10)
* [`log2`](kcl/log2)
* [`m`](kcl/m)
* [`map`](kcl/map)
* [`max`](kcl/max)
* [`min`](kcl/min)
* [`mirror2d`](kcl/mirror2d)
@ -78,6 +78,7 @@ layout: manual
* [`profileStart`](kcl/profileStart)
* [`profileStartX`](kcl/profileStartX)
* [`profileStartY`](kcl/profileStartY)
* [`reduce`](kcl/reduce)
* [`rem`](kcl/rem)
* [`revolve`](kcl/revolve)
* [`segAng`](kcl/segAng)

56
docs/kcl/map.md Normal file

File diff suppressed because one or more lines are too long

47
docs/kcl/reduce.md Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,89 @@
---
title: "BinaryOperator"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Add two numbers.
**enum:** `+`
----
Subtract two numbers.
**enum:** `-`
----
Multiply two numbers.
**enum:** `*`
----
Divide two numbers.
**enum:** `/`
----
Modulo two numbers.
**enum:** `%`
----
Raise a number to a power.
**enum:** `^`
----

View File

@ -0,0 +1,161 @@
---
title: "BinaryPart"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `BinaryExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)| | No |
| `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CallExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| | No |
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `optional` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `UnaryExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)| | No |
| `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `IfExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| | No |
| `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`| | No |
| `final_else` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----

View File

@ -0,0 +1,75 @@
---
title: "BodyItem"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ExpressionStatement`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `expression` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `VariableDeclaration`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ReturnStatement`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `argument` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----

View File

@ -0,0 +1,41 @@
---
title: "CommentStyle"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Like // foo
**enum:** `line`
----
Like /* foo */
**enum:** `block`
----

24
docs/kcl/types/ElseIf.md Normal file
View File

@ -0,0 +1,24 @@
---
title: "ElseIf"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -0,0 +1,21 @@
---
title: "Environment"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `bindings` |`object`| | No |
| `parent` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |

View File

@ -0,0 +1,16 @@
---
title: "EnvironmentRef"
excerpt: "An index pointing to an environment."
layout: manual
---
An index pointing to an environment.
**Type:** `integer` (`uint`)

297
docs/kcl/types/Expr.md Normal file
View File

@ -0,0 +1,297 @@
---
title: "Expr"
excerpt: "An expression can be evaluated to yield a single KCL value."
layout: manual
---
An expression can be evaluated to yield a single KCL value.
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| An expression can be evaluated to yield a single KCL value. | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`TagDeclarator`](/docs/kcl/types#tag-declaration)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `value` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `BinaryExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)| An expression can be evaluated to yield a single KCL value. | No |
| `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`FunctionExpression`](/docs/kcl/types/FunctionExpression)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`| | No |
| `body` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CallExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| An expression can be evaluated to yield a single KCL value. | No |
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `optional` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `PipeExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `body` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `PipeSubstitution`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `elements` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ObjectExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `properties` |`[` [`ObjectProperty`](/docs/kcl/types/ObjectProperty) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| An expression can be evaluated to yield a single KCL value. | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| An expression can be evaluated to yield a single KCL value. | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `UnaryExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)| An expression can be evaluated to yield a single KCL value. | No |
| `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `IfExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`| | No |
| `final_else` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application).
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `None`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
----

View File

@ -0,0 +1,24 @@
---
title: "FunctionExpression"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`| | No |
| `body` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -0,0 +1,23 @@
---
title: "Identifier"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

195
docs/kcl/types/KclValue.md Normal file
View File

@ -0,0 +1,195 @@
---
title: "KclValue"
excerpt: "A memory item."
layout: manual
---
A memory item.
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `UserVal`| | No |
| `value` |``| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`TagIdentifier`](/docs/kcl/types#tag-identifier)| | No |
| `value` |`string`| | No |
| `info` |[`TagEngineInfo`](/docs/kcl/types/TagEngineInfo)| | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`TagDeclarator`](/docs/kcl/types#tag-declaration)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `value` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
A plane.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Plane`| | No |
| `id` |`string`| The id of the plane. | No |
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| A memory item. | No |
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the planes Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
A face.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Face`| | No |
| `id` |`string`| The id of the face. | No |
| `value` |`string`| The tag of the face. | No |
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces X axis be? | No |
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the faces Y axis be? | No |
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
An solid is a collection of extrude surfaces.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Solid`](/docs/kcl/types/Solid)| | No |
| `id` |`string`| The id of the solid. | No |
| `value` |`[` [`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface) `]`| The extrude surfaces. | No |
| `sketch` |[`Sketch`](/docs/kcl/types/Sketch)| The sketch. | No |
| `height` |`number`| The height of the solid. | No |
| `startCapId` |`string`| The id of the extrusion start cap | No |
| `endCapId` |`string`| The id of the extrusion end cap | No |
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Solids`| | No |
| `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`| | No |
----
Data for an imported geometry.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`ImportedGeometry`](/docs/kcl/types/ImportedGeometry)| | No |
| `id` |`string`| The ID of the imported geometry. | No |
| `value` |`[` `string` `]`| The original file paths. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Function`| | No |
| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| A memory item. | No |
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| A memory item. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
----

View File

@ -0,0 +1,56 @@
---
title: "LiteralIdentifier"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----

View File

@ -0,0 +1,57 @@
---
title: "LiteralValue"
excerpt: ""
layout: manual
---
**This schema accepts any of the following:**
**Type:** `integer` (`int64`)
----
**Type:** `number` (`double`)
----
**Type:** `string`
----
**Type:** `boolean`
----

View File

@ -0,0 +1,57 @@
---
title: "MemberObject"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
----

View File

@ -0,0 +1,22 @@
---
title: "NonCodeMeta"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `nonCodeNodes` |`object`| | No |
| `start` |`[` [`NonCodeNode`](/docs/kcl/types/NonCodeNode) `]`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -0,0 +1,23 @@
---
title: "NonCodeNode"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `value` |[`NonCodeValue`](/docs/kcl/types/NonCodeValue)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -0,0 +1,103 @@
---
title: "NonCodeValue"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `shebang`| | No |
| `value` |`string`| | No |
----
An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `inlineComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
A block comment. An example of this is the following: ```python,no_run /* This is a block comment */ 1 + 1 ``` Now this is important. The block comment is attached to the next line. This is always the case. Also the block comment doesn't have a new line above it. If it did it would be a `NewLineBlockComment`.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `blockComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
A block comment that has a new line above it. The user explicitly added a new line above the block comment.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `newLineBlockComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `newLine`| | No |
----

View File

@ -0,0 +1,24 @@
---
title: "ObjectProperty"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `key` |[`Identifier`](/docs/kcl/types/Identifier)| | No |
| `value` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -0,0 +1,23 @@
---
title: "Parameter"
excerpt: "Parameter of a KCL function."
layout: manual
---
Parameter of a KCL function.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `identifier` |[`Identifier`](/docs/kcl/types/Identifier)| The parameter's label or name. | No |
| `optional` |`boolean`| Is the parameter optional? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

25
docs/kcl/types/Program.md Normal file
View File

@ -0,0 +1,25 @@
---
title: "Program"
excerpt: "A KCL program top level, or function body."
layout: manual
---
A KCL program top level, or function body.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `body` |`[` [`BodyItem`](/docs/kcl/types/BodyItem) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| A KCL program top level, or function body. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -0,0 +1,22 @@
---
title: "ProgramMemory"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `environments` |`[` [`Environment`](/docs/kcl/types/Environment) `]`| | No |
| `currentEnv` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `return` |[`KclValue`](/docs/kcl/types/KclValue)| | No |

View File

@ -0,0 +1,41 @@
---
title: "UnaryOperator"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Negate a number.
**enum:** `-`
----
Negate a boolean.
**enum:** `!`
----

View File

@ -0,0 +1,24 @@
---
title: "VariableDeclarator"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `id` |[`Identifier`](/docs/kcl/types/Identifier)| The identifier of the variable. | No |
| `init` |[`Expr`](/docs/kcl/types/Expr)| The value of the variable. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -0,0 +1,65 @@
---
title: "VariableKind"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Declare a variable.
**enum:** `let`
----
Declare a variable that is read-only.
**enum:** `const`
----
Declare a function.
**enum:** `fn`
----
Declare a variable.
**enum:** `var`
----

View File

@ -155,15 +155,16 @@ test(
// Find the toast.
// Look out for the toast message
await expect(exportingToastMessage).toBeVisible()
await expect(alreadyExportingToastMessage).not.toBeVisible()
// Expect it to succeed.
await expect(errorToastMessage).not.toBeVisible()
await expect(engineErrorToastMessage).not.toBeVisible()
const successToastMessage = page.getByText(`Exported successfully`)
await expect(successToastMessage).toBeVisible()
await expect(exportingToastMessage).not.toBeVisible()
await test.step('Check the success toast message shows and nothing else', async () =>
Promise.all([
expect(alreadyExportingToastMessage).not.toBeVisible(),
expect(errorToastMessage).not.toBeVisible(),
expect(engineErrorToastMessage).not.toBeVisible(),
expect(successToastMessage).toBeVisible(),
expect(exportingToastMessage).not.toBeVisible(),
]))
await test.step('Check the export size', async () => {
await expect

View File

@ -1,4 +1,5 @@
import { test, expect } from '@playwright/test'
import { _test, _expect } from './playwright-deprecated'
import { test, expect } from './fixtures/fixtureSetup'
import * as fsp from 'fs/promises'
import * as fs from 'fs'
import {
@ -11,14 +12,98 @@ import {
import { join } from 'path'
import { FILE_EXT } from 'lib/constants'
test.beforeEach(async ({ context, page }, testInfo) => {
_test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
_test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
test.describe('integrations tests', () => {
test(
'Creating a new file or switching file while in sketchMode should exit sketchMode',
{ tag: '@electron' },
async ({ tronApp, homePage, scene, editor, toolbar }) => {
test.skip(
process.platform === 'win32',
'windows times out will waiting for the execution indicator?'
)
await tronApp.initialise({
fixtures: { homePage, scene, editor, toolbar },
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'test-sample')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('e2e-can-sketch-on-chamfer.kcl'),
join(bracketDir, 'main.kcl')
)
},
})
const [clickObj] = await scene.makeMouseHelpers(600, 300)
await test.step('setup test', async () => {
await homePage.expectState({
projectCards: [
{
title: 'test-sample',
fileCount: 1,
folderCount: 1,
},
],
sortBy: 'last-modified-desc',
})
await homePage.openProject('test-sample')
// windows times out here, hence the skip above
await scene.waitForExecutionDone()
})
await test.step('enter sketch mode', async () => {
await clickObj()
await scene.moveNoWhere()
await editor.expectState({
activeLines: [
'|>startProfileAt([75.8,317.2],%)//[$startCapTag,$EndCapTag]',
],
highlightedCode: '',
diagnostics: [],
})
await toolbar.editSketch()
await expect(toolbar.exitSketchBtn).toBeVisible()
})
await test.step('check sketch mode is exited when creating new file', async () => {
await toolbar.fileTreeBtn.click()
await toolbar.expectFileTreeState(['main.kcl'])
await toolbar.createFile({ wait: true })
// check we're out of sketch mode
await expect(toolbar.exitSketchBtn).not.toBeVisible()
await expect(toolbar.startSketchBtn).toBeVisible()
})
await test.step('setup for next assertion', async () => {
await toolbar.openFile('main.kcl')
await clickObj()
await scene.moveNoWhere()
await editor.expectState({
activeLines: [
'|>startProfileAt([75.8,317.2],%)//[$startCapTag,$EndCapTag]',
],
highlightedCode: '',
diagnostics: [],
})
await toolbar.editSketch()
await expect(toolbar.exitSketchBtn).toBeVisible()
await toolbar.expectFileTreeState(['main.kcl', 'Untitled.kcl'])
})
await test.step('check sketch mode is exited when opening a different file', async () => {
await toolbar.openFile('untitled.kcl', { wait: false })
// check we're out of sketch mode
await expect(toolbar.exitSketchBtn).not.toBeVisible()
await expect(toolbar.startSketchBtn).toBeVisible()
})
}
)
})
test.describe('when using the file tree to', () => {
const fromFile = 'main.kcl'
const toFile = 'hello.kcl'
@ -26,11 +111,8 @@ test.describe('when using the file tree to', () => {
test(
`rename ${fromFile} to ${toFile}, and doesn't crash on reload and settings load`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
async ({ browser: _, tronApp }, testInfo) => {
await tronApp.initialise()
const {
panesOpen,
@ -38,10 +120,10 @@ test.describe('when using the file tree to', () => {
pasteCodeInEditor,
renameFile,
editorTextMatches,
} = await getUtils(page, test)
} = await getUtils(tronApp.page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
tronApp.page.on('console', console.log)
await panesOpen(['files', 'code'])
@ -55,39 +137,38 @@ test.describe('when using the file tree to', () => {
await pasteCodeInEditor(kclCube)
await renameFile(fromFile, toFile)
await page.reload()
await tronApp.page.reload()
await test.step('Postcondition: editor has same content as before the rename', async () => {
await editorTextMatches(kclCube)
})
await test.step('Postcondition: opening and closing settings works', async () => {
const settingsOpenButton = page.getByRole('link', {
const settingsOpenButton = tronApp.page.getByRole('link', {
name: 'settings Settings',
})
const settingsCloseButton = page.getByTestId('settings-close-button')
const settingsCloseButton = tronApp.page.getByTestId(
'settings-close-button'
)
await settingsOpenButton.click()
await settingsCloseButton.click()
})
await electronApp.close()
await tronApp.close()
}
)
test(
`create many new untitled files they increment their names`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
async ({ browser: _, tronApp }, testInfo) => {
await tronApp.initialise()
const { panesOpen, createAndSelectProject, createNewFile } =
await getUtils(page, test)
await getUtils(tronApp.page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
tronApp.page.on('console', console.log)
await panesOpen(['files'])
@ -101,23 +182,21 @@ test.describe('when using the file tree to', () => {
await test.step('Postcondition: there are 5 new Untitled-*.kcl files', async () => {
await expect(
page
tronApp.page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: /Untitled[-]?[0-5]?/ })
).toHaveCount(5)
})
await electronApp.close()
await tronApp.close()
}
)
test(
'create a new file with the same name as an existing file cancels the operation',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
})
async ({ browser: _, tronApp }, testInfo) => {
await tronApp.initialise()
const {
openKclCodePanel,
@ -128,10 +207,10 @@ test.describe('when using the file tree to', () => {
renameFile,
selectFile,
editorTextMatches,
} = await getUtils(page, test)
} = await getUtils(tronApp.page, _test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
tronApp.page.on('console', console.log)
await createAndSelectProject('project-000')
await openKclCodePanel()
@ -159,25 +238,22 @@ test.describe('when using the file tree to', () => {
await selectFile(kcl1)
await editorTextMatches(kclCube)
})
await page.waitForTimeout(500)
await tronApp.page.waitForTimeout(500)
await test.step(`Postcondition: ${kcl2} still exists with the original content`, async () => {
await selectFile(kcl2)
await editorTextMatches(kclCylinder)
})
await electronApp.close()
await tronApp?.close?.()
}
)
test(
'deleting all files recreates a default main.kcl with no code',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async () => {},
})
async ({ browser: _, tronApp }, testInfo) => {
await tronApp.initialise()
const {
panesOpen,
@ -185,10 +261,10 @@ test.describe('when using the file tree to', () => {
pasteCodeInEditor,
deleteFile,
editorTextMatches,
} = await getUtils(page, test)
} = await getUtils(tronApp.page, _test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
tronApp.page.on('console', console.log)
await panesOpen(['files', 'code'])
@ -208,7 +284,7 @@ test.describe('when using the file tree to', () => {
await editorTextMatches('')
})
await electronApp.close()
await tronApp.close()
}
)
@ -217,10 +293,8 @@ test.describe('when using the file tree to', () => {
{
tag: '@electron',
},
async ({ browser: _ }, testInfo) => {
const { page } = await setupElectron({
testInfo,
})
async ({ browser: _, tronApp }, testInfo) => {
await tronApp.initialise()
const {
panesOpen,
@ -230,10 +304,10 @@ test.describe('when using the file tree to', () => {
openDebugPanel,
closeDebugPanel,
expectCmdLog,
} = await getUtils(page, test)
} = await getUtils(tronApp.page, test)
await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log)
await tronApp.page.setViewportSize({ width: 1200, height: 500 })
tronApp.page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000')
@ -248,30 +322,30 @@ test.describe('when using the file tree to', () => {
// Create a large lego file
await createNewFile('lego')
const legoFile = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'lego.kcl' }),
const legoFile = tronApp.page.getByRole('listitem').filter({
has: tronApp.page.getByRole('button', { name: 'lego.kcl' }),
})
await expect(legoFile).toBeVisible({ timeout: 60_000 })
await _expect(legoFile).toBeVisible({ timeout: 60_000 })
await legoFile.click()
const kclLego = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/lego.kcl',
'utf-8'
)
await pasteCodeInEditor(kclLego)
const mainFile = page.getByRole('listitem').filter({
has: page.getByRole('button', { name: 'main.kcl' }),
const mainFile = tronApp.page.getByRole('listitem').filter({
has: tronApp.page.getByRole('button', { name: 'main.kcl' }),
})
// Open settings and enable the debug panel
await page
await tronApp.page
.getByRole('link', {
name: 'settings Settings',
})
.click()
await page.locator('#showDebugPanel').getByText('OffOn').click()
await page.getByTestId('settings-close-button').click()
await tronApp.page.locator('#showDebugPanel').getByText('OffOn').click()
await tronApp.page.getByTestId('settings-close-button').click()
await test.step('swap between small and large files', async () => {
await _test.step('swap between small and large files', async () => {
await openDebugPanel()
// Previously created a file so we need to start back at main.kcl
await mainFile.click()
@ -283,12 +357,14 @@ test.describe('when using the file tree to', () => {
await expectCmdLog('[data-message-type="execution-done"]', 60_000)
await closeDebugPanel()
})
await tronApp.close()
}
)
})
test.describe('Renaming in the file tree', () => {
test(
_test.describe('Renaming in the file tree', () => {
_test(
'A file you have open',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
@ -333,56 +409,56 @@ test.describe('Renaming in the file tree', () => {
const renameInput = page.getByPlaceholder('fileToRename.kcl')
const codeLocator = page.locator('.cm-content')
await test.step('Open project and file pane', async () => {
await expect(projectLink).toBeVisible()
await _test.step('Open project and file pane', async () => {
await _expect(projectLink).toBeVisible()
await projectLink.click()
await expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
await _expect(projectMenuButton).toBeVisible()
await _expect(projectMenuButton).toContainText('main.kcl')
await u.openFilePanel()
await expect(fileToRename).toBeVisible()
expect(checkUnRenamedFS()).toBeTruthy()
expect(checkRenamedFS()).toBeFalsy()
await _expect(fileToRename).toBeVisible()
_expect(checkUnRenamedFS()).toBeTruthy()
_expect(checkRenamedFS()).toBeFalsy()
await fileToRename.click()
await expect(projectMenuButton).toContainText('fileToRename.kcl')
await _expect(projectMenuButton).toContainText('fileToRename.kcl')
await u.openKclCodePanel()
await expect(codeLocator).toContainText('circle(')
await _expect(codeLocator).toContainText('circle(')
await u.closeKclCodePanel()
})
await test.step('Rename the file', async () => {
await _test.step('Rename the file', async () => {
await fileToRename.click({ button: 'right' })
await renameMenuItem.click()
await expect(renameInput).toBeVisible()
await _expect(renameInput).toBeVisible()
await renameInput.fill(newFileName)
await page.keyboard.press('Enter')
})
await test.step('Verify the file is renamed', async () => {
await expect(fileToRename).not.toBeAttached()
await expect(renamedFile).toBeVisible()
expect(checkUnRenamedFS()).toBeFalsy()
expect(checkRenamedFS()).toBeTruthy()
await _test.step('Verify the file is renamed', async () => {
await _expect(fileToRename).not.toBeAttached()
await _expect(renamedFile).toBeVisible()
_expect(checkUnRenamedFS()).toBeFalsy()
_expect(checkRenamedFS()).toBeTruthy()
})
await test.step('Verify we navigated', async () => {
await expect(projectMenuButton).toContainText(newFileName + FILE_EXT)
await _test.step('Verify we navigated', async () => {
await _expect(projectMenuButton).toContainText(newFileName + FILE_EXT)
const url = page.url()
expect(url).toContain(newFileName)
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
await expect(projectMenuButton).not.toContainText('main.kcl')
expect(url).not.toContain('fileToRename.kcl')
expect(url).not.toContain('main.kcl')
_expect(url).toContain(newFileName)
await _expect(projectMenuButton).not.toContainText('fileToRename.kcl')
await _expect(projectMenuButton).not.toContainText('main.kcl')
_expect(url).not.toContain('fileToRename.kcl')
_expect(url).not.toContain('main.kcl')
await u.openKclCodePanel()
await expect(codeLocator).toContainText('circle(')
await _expect(codeLocator).toContainText('circle(')
})
await electronApp.close()
}
)
test(
_test(
'A file you do not have open',
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
@ -426,54 +502,54 @@ test.describe('Renaming in the file tree', () => {
const renameInput = page.getByPlaceholder('fileToRename.kcl')
const codeLocator = page.locator('.cm-content')
await test.step('Open project and file pane', async () => {
await expect(projectLink).toBeVisible()
await _test.step('Open project and file pane', async () => {
await _expect(projectLink).toBeVisible()
await projectLink.click()
await expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
await _expect(projectMenuButton).toBeVisible()
await _expect(projectMenuButton).toContainText('main.kcl')
await u.openFilePanel()
await expect(fileToRename).toBeVisible()
expect(checkUnRenamedFS()).toBeTruthy()
expect(checkRenamedFS()).toBeFalsy()
await _expect(fileToRename).toBeVisible()
_expect(checkUnRenamedFS()).toBeTruthy()
_expect(checkRenamedFS()).toBeFalsy()
})
await test.step('Rename the file', async () => {
await _test.step('Rename the file', async () => {
await fileToRename.click({ button: 'right' })
await renameMenuItem.click()
await expect(renameInput).toBeVisible()
await _expect(renameInput).toBeVisible()
await renameInput.fill(newFileName)
await page.keyboard.press('Enter')
})
await test.step('Verify the file is renamed', async () => {
await expect(fileToRename).not.toBeAttached()
await expect(renamedFile).toBeVisible()
expect(checkUnRenamedFS()).toBeFalsy()
expect(checkRenamedFS()).toBeTruthy()
await _test.step('Verify the file is renamed', async () => {
await _expect(fileToRename).not.toBeAttached()
await _expect(renamedFile).toBeVisible()
_expect(checkUnRenamedFS()).toBeFalsy()
_expect(checkRenamedFS()).toBeTruthy()
})
await test.step('Verify we have not navigated', async () => {
await expect(projectMenuButton).toContainText('main.kcl')
await expect(projectMenuButton).not.toContainText(
await _test.step('Verify we have not navigated', async () => {
await _expect(projectMenuButton).toContainText('main.kcl')
await _expect(projectMenuButton).not.toContainText(
newFileName + FILE_EXT
)
await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
await _expect(projectMenuButton).not.toContainText('fileToRename.kcl')
const url = page.url()
expect(url).toContain('main.kcl')
expect(url).not.toContain(newFileName)
expect(url).not.toContain('fileToRename.kcl')
_expect(url).toContain('main.kcl')
_expect(url).not.toContain(newFileName)
_expect(url).not.toContain('fileToRename.kcl')
await u.openKclCodePanel()
await expect(codeLocator).toContainText('fillet(')
await _expect(codeLocator).toContainText('fillet(')
})
await electronApp.close()
}
)
test(
_test(
`A folder you're not inside`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
@ -519,48 +595,51 @@ test.describe('Renaming in the file tree', () => {
return fs.existsSync(folderPath)
}
await test.step('Open project and file pane', async () => {
await expect(projectLink).toBeVisible()
await _test.step('Open project and file pane', async () => {
await _expect(projectLink).toBeVisible()
await projectLink.click()
await expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
await _expect(projectMenuButton).toBeVisible()
await _expect(projectMenuButton).toContainText('main.kcl')
const url = page.url()
expect(url).toContain('main.kcl')
expect(url).not.toContain('folderToRename')
_expect(url).toContain('main.kcl')
_expect(url).not.toContain('folderToRename')
await u.openFilePanel()
await expect(folderToRename).toBeVisible()
expect(checkUnRenamedFolderFS()).toBeTruthy()
expect(checkRenamedFolderFS()).toBeFalsy()
await _expect(folderToRename).toBeVisible()
_expect(checkUnRenamedFolderFS()).toBeTruthy()
_expect(checkRenamedFolderFS()).toBeFalsy()
})
await test.step('Rename the folder', async () => {
await _test.step('Rename the folder', async () => {
await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible()
await _expect(renameMenuItem).toBeVisible()
await renameMenuItem.click()
await expect(renameInput).toBeVisible()
await _expect(renameInput).toBeVisible()
await renameInput.fill(newFolderName)
await page.keyboard.press('Enter')
})
await test.step('Verify the folder is renamed, and no navigation occurred', async () => {
const url = page.url()
expect(url).toContain('main.kcl')
expect(url).not.toContain('folderToRename')
await _test.step(
'Verify the folder is renamed, and no navigation occurred',
async () => {
const url = page.url()
_expect(url).toContain('main.kcl')
_expect(url).not.toContain('folderToRename')
await expect(projectMenuButton).toContainText('main.kcl')
await expect(renamedFolder).toBeVisible()
await expect(folderToRename).not.toBeAttached()
expect(checkUnRenamedFolderFS()).toBeFalsy()
expect(checkRenamedFolderFS()).toBeTruthy()
})
await _expect(projectMenuButton).toContainText('main.kcl')
await _expect(renamedFolder).toBeVisible()
await _expect(folderToRename).not.toBeAttached()
_expect(checkUnRenamedFolderFS()).toBeFalsy()
_expect(checkRenamedFolderFS()).toBeTruthy()
}
)
await electronApp.close()
}
)
test(
_test(
`A folder you are inside`,
{ tag: '@electron' },
async ({ browser: _ }, testInfo) => {
@ -609,66 +688,69 @@ test.describe('Renaming in the file tree', () => {
return fs.existsSync(folderPath)
}
await test.step('Open project and navigate into folder', async () => {
await expect(projectLink).toBeVisible()
await _test.step('Open project and navigate into folder', async () => {
await _expect(projectLink).toBeVisible()
await projectLink.click()
await expect(projectMenuButton).toBeVisible()
await expect(projectMenuButton).toContainText('main.kcl')
await _expect(projectMenuButton).toBeVisible()
await _expect(projectMenuButton).toContainText('main.kcl')
const url = page.url()
expect(url).toContain('main.kcl')
expect(url).not.toContain('folderToRename')
_expect(url).toContain('main.kcl')
_expect(url).not.toContain('folderToRename')
await u.openFilePanel()
await expect(folderToRename).toBeVisible()
await _expect(folderToRename).toBeVisible()
await folderToRename.click()
await expect(fileWithinFolder).toBeVisible()
await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
const newUrl = page.url()
expect(newUrl).toContain('folderToRename')
expect(newUrl).toContain('someFileWithin.kcl')
expect(newUrl).not.toContain('main.kcl')
expect(checkUnRenamedFolderFS()).toBeTruthy()
expect(checkRenamedFolderFS()).toBeFalsy()
_expect(newUrl).toContain('folderToRename')
_expect(newUrl).toContain('someFileWithin.kcl')
_expect(newUrl).not.toContain('main.kcl')
_expect(checkUnRenamedFolderFS()).toBeTruthy()
_expect(checkRenamedFolderFS()).toBeFalsy()
})
await test.step('Rename the folder', async () => {
await _test.step('Rename the folder', async () => {
await page.waitForTimeout(60000)
await folderToRename.click({ button: 'right' })
await expect(renameMenuItem).toBeVisible()
await _expect(renameMenuItem).toBeVisible()
await renameMenuItem.click()
await expect(renameInput).toBeVisible()
await _expect(renameInput).toBeVisible()
await renameInput.fill(newFolderName)
await page.keyboard.press('Enter')
})
await test.step('Verify the folder is renamed, and navigated to new path', async () => {
const urlSnippet = encodeURIComponent(
join(newFolderName, 'someFileWithin.kcl')
)
await page.waitForURL(new RegExp(urlSnippet))
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
await expect(renamedFolder).toBeVisible()
await expect(folderToRename).not.toBeAttached()
await _test.step(
'Verify the folder is renamed, and navigated to new path',
async () => {
const urlSnippet = encodeURIComponent(
join(newFolderName, 'someFileWithin.kcl')
)
await page.waitForURL(new RegExp(urlSnippet))
await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
await _expect(renamedFolder).toBeVisible()
await _expect(folderToRename).not.toBeAttached()
// URL is synchronous, so we check the other stuff first
const url = page.url()
expect(url).not.toContain('main.kcl')
expect(url).toContain(newFolderName)
expect(url).toContain('someFileWithin.kcl')
expect(checkUnRenamedFolderFS()).toBeFalsy()
expect(checkRenamedFolderFS()).toBeTruthy()
})
// URL is synchronous, so we check the other stuff first
const url = page.url()
_expect(url).not.toContain('main.kcl')
_expect(url).toContain(newFolderName)
_expect(url).toContain('someFileWithin.kcl')
_expect(checkUnRenamedFolderFS()).toBeFalsy()
_expect(checkRenamedFolderFS()).toBeTruthy()
}
)
await electronApp.close()
}
)
})
test.describe('Deleting items from the file pane', () => {
test(
_test.describe('Deleting items from the file pane', () => {
_test(
`delete file when main.kcl exists, navigate to main.kcl`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
@ -700,45 +782,48 @@ test.describe('Deleting items from the file pane', () => {
const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
const deleteConfirmation = page.getByTestId('delete-confirmation')
await test.step('Open project and navigate to fileToDelete.kcl', async () => {
await projectCard.click()
await u.waitForPageLoad()
await u.openFilePanel()
await _test.step(
'Open project and navigate to fileToDelete.kcl',
async () => {
await projectCard.click()
await u.waitForPageLoad()
await u.openFilePanel()
await fileToDelete.click()
await u.waitForPageLoad()
await u.openKclCodePanel()
await expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
await u.closeKclCodePanel()
})
await fileToDelete.click()
await u.waitForPageLoad()
await u.openKclCodePanel()
await _expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
await u.closeKclCodePanel()
}
)
await test.step('Delete fileToDelete.kcl', async () => {
await _test.step('Delete fileToDelete.kcl', async () => {
await fileToDelete.click({ button: 'right' })
await expect(deleteMenuItem).toBeVisible()
await _expect(deleteMenuItem).toBeVisible()
await deleteMenuItem.click()
await expect(deleteConfirmation).toBeVisible()
await _expect(deleteConfirmation).toBeVisible()
await deleteConfirmation.click()
})
await test.step('Check deletion and navigation', async () => {
await _test.step('Check deletion and navigation', async () => {
await u.waitForPageLoad()
await expect(fileToDelete).not.toBeVisible()
await _expect(fileToDelete).not.toBeVisible()
await u.closeFilePanel()
await u.openKclCodePanel()
await expect(u.codeLocator).toContainText('circle(')
await expect(projectMenuButton).toContainText('main.kcl')
await _expect(u.codeLocator).toContainText('circle(')
await _expect(projectMenuButton).toContainText('main.kcl')
})
await electronApp.close()
}
)
test.fixme(
_test.fixme(
'TODO - delete file we have open when main.kcl does not exist',
async () => {}
)
test(
_test(
`Delete folder we are not in, don't navigate`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
@ -772,32 +857,32 @@ test.describe('Deleting items from the file pane', () => {
const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
const deleteConfirmation = page.getByTestId('delete-confirmation')
await test.step('Open project and open project pane', async () => {
await _test.step('Open project and open project pane', async () => {
await projectCard.click()
await u.waitForPageLoad()
await expect(projectMenuButton).toContainText('main.kcl')
await _expect(projectMenuButton).toContainText('main.kcl')
await u.closeKclCodePanel()
await u.openFilePanel()
})
await test.step('Delete folderToDelete', async () => {
await _test.step('Delete folderToDelete', async () => {
await folderToDelete.click({ button: 'right' })
await expect(deleteMenuItem).toBeVisible()
await _expect(deleteMenuItem).toBeVisible()
await deleteMenuItem.click()
await expect(deleteConfirmation).toBeVisible()
await _expect(deleteConfirmation).toBeVisible()
await deleteConfirmation.click()
})
await test.step('Check deletion and no navigation', async () => {
await expect(folderToDelete).not.toBeAttached()
await expect(projectMenuButton).toContainText('main.kcl')
await _test.step('Check deletion and no navigation', async () => {
await _expect(folderToDelete).not.toBeAttached()
await _expect(projectMenuButton).toContainText('main.kcl')
})
await electronApp.close()
}
)
test(
_test(
`Delete folder we are in, navigate to main.kcl`,
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
@ -834,36 +919,45 @@ test.describe('Deleting items from the file pane', () => {
const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
const deleteConfirmation = page.getByTestId('delete-confirmation')
await test.step('Open project and navigate into folderToDelete', async () => {
await projectCard.click()
await u.waitForPageLoad()
await expect(projectMenuButton).toContainText('main.kcl')
await u.closeKclCodePanel()
await u.openFilePanel()
await _test.step(
'Open project and navigate into folderToDelete',
async () => {
await projectCard.click()
await u.waitForPageLoad()
await _expect(projectMenuButton).toContainText('main.kcl')
await u.closeKclCodePanel()
await u.openFilePanel()
await folderToDelete.click()
await expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await expect(projectMenuButton).toContainText('someFileWithin.kcl')
})
await folderToDelete.click()
await _expect(fileWithinFolder).toBeVisible()
await fileWithinFolder.click()
await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
}
)
await test.step('Delete folderToDelete', async () => {
await _test.step('Delete folderToDelete', async () => {
await folderToDelete.click({ button: 'right' })
await expect(deleteMenuItem).toBeVisible()
await _expect(deleteMenuItem).toBeVisible()
await deleteMenuItem.click()
await expect(deleteConfirmation).toBeVisible()
await _expect(deleteConfirmation).toBeVisible()
await deleteConfirmation.click()
})
await test.step('Check deletion and navigation to main.kcl', async () => {
await expect(folderToDelete).not.toBeAttached()
await expect(fileWithinFolder).not.toBeAttached()
await expect(projectMenuButton).toContainText('main.kcl')
})
await _test.step(
'Check deletion and navigation to main.kcl',
async () => {
await _expect(folderToDelete).not.toBeAttached()
await _expect(fileWithinFolder).not.toBeAttached()
await _expect(projectMenuButton).toContainText('main.kcl')
}
)
await electronApp.close()
}
)
test.fixme('TODO - delete folder we are in, with no main.kcl', async () => {})
_test.fixme(
'TODO - delete folder we are in, with no main.kcl',
async () => {}
)
})

View File

@ -1,70 +0,0 @@
import type { Page } from '@playwright/test'
import { test as base } from '@playwright/test'
import { getUtils, setup, tearDown } from './test-utils'
import fsp from 'fs/promises'
import { join } from 'path'
import { CmdBarFixture } from './cmdBarFixture'
import { EditorFixture } from './editorFixture'
import { ToolbarFixture } from './toolbarFixture'
import { SceneFixture } from './sceneFixture'
export class AuthenticatedApp {
public readonly page: Page
constructor(page: Page) {
this.page = page
}
async initialise(code = '') {
const u = await getUtils(this.page)
await this.page.addInitScript(async (code) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
}, code)
await this.page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
}
getInputFile = (fileName: string) => {
return fsp.readFile(
join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName),
'utf-8'
)
}
}
export const test = base.extend<{
app: AuthenticatedApp
cmdBar: CmdBarFixture
editor: EditorFixture
toolbar: ToolbarFixture
scene: SceneFixture
}>({
app: async ({ page }, use) => {
await use(new AuthenticatedApp(page))
},
cmdBar: async ({ page }, use) => {
await use(new CmdBarFixture(page))
},
editor: async ({ page }, use) => {
await use(new EditorFixture(page))
},
toolbar: async ({ page }, use) => {
await use(new ToolbarFixture(page))
},
scene: async ({ page }, use) => {
await use(new SceneFixture(page))
},
})
test.beforeEach(async ({ context, page }, testInfo) => {
await setup(context, page, testInfo)
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
export { expect } from '@playwright/test'

View File

@ -25,11 +25,14 @@ type CmdBarSerialised =
}
export class CmdBarFixture {
public readonly page: Page
public page: Page
constructor(page: Page) {
this.page = page
}
reConstruct = (page: Page) => {
this.page = page
}
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
const reviewForm = await this.page.locator('#review-form')

View File

@ -1,5 +1,6 @@
import type { Page, Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import { sansWhitespace } from '../test-utils'
interface EditorState {
activeLines: Array<string>
@ -7,19 +8,20 @@ interface EditorState {
diagnostics: Array<string>
}
function removeWhitespace(str: string) {
return str.replace(/\s+/g, '').trim()
}
export class EditorFixture {
public readonly page: Page
public page: Page
private readonly diagnosticsTooltip: Locator
private readonly diagnosticsGutterIcon: Locator
private readonly codeContent: Locator
private readonly activeLine: Locator
private diagnosticsTooltip!: Locator
private diagnosticsGutterIcon!: Locator
private codeContent!: Locator
private activeLine!: Locator
constructor(page: Page) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.codeContent = page.locator('.cm-content')
this.diagnosticsTooltip = page.locator('.cm-tooltip-lint')
@ -94,16 +96,16 @@ export class EditorFixture {
this._serialiseDiagnostics(),
])
const state: EditorState = {
activeLines: activeLines.map(removeWhitespace).filter(Boolean),
highlightedCode: removeWhitespace(highlightedCode),
activeLines: activeLines.map(sansWhitespace).filter(Boolean),
highlightedCode: sansWhitespace(highlightedCode),
diagnostics,
}
return state
})
.toEqual({
activeLines: expectedState.activeLines.map(removeWhitespace),
highlightedCode: removeWhitespace(expectedState.highlightedCode),
diagnostics: expectedState.diagnostics.map(removeWhitespace),
activeLines: expectedState.activeLines.map(sansWhitespace),
highlightedCode: sansWhitespace(expectedState.highlightedCode),
diagnostics: expectedState.diagnostics.map(sansWhitespace),
})
}
}

View File

@ -0,0 +1,140 @@
import type {
BrowserContext,
ElectronApplication,
Page,
TestInfo,
} from '@playwright/test'
import { test as base } from '@playwright/test'
import { getUtils, setup, setupElectron, tearDown } from '../test-utils'
import fsp from 'fs/promises'
import { join } from 'path'
import { CmdBarFixture } from './cmdBarFixture'
import { EditorFixture } from './editorFixture'
import { ToolbarFixture } from './toolbarFixture'
import { SceneFixture } from './sceneFixture'
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { HomePageFixture } from './homePageFixture'
import { unsafeTypedKeys } from 'lib/utils'
export class AuthenticatedApp {
public readonly page: Page
public readonly context: BrowserContext
public readonly testInfo: TestInfo
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this.page = page
this.context = context
this.testInfo = testInfo
}
async initialise(code = '') {
await setup(this.context, this.page, this.testInfo)
const u = await getUtils(this.page)
await this.page.addInitScript(async (code) => {
localStorage.setItem('persistCode', code)
;(window as any).playwrightSkipFilePicker = true
}, code)
await this.page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
}
getInputFile = (fileName: string) => {
return fsp.readFile(
join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName),
'utf-8'
)
}
}
interface Fixtures {
app: AuthenticatedApp
tronApp: AuthenticatedTronApp
cmdBar: CmdBarFixture
editor: EditorFixture
toolbar: ToolbarFixture
scene: SceneFixture
homePage: HomePageFixture
}
export class AuthenticatedTronApp {
public readonly _page: Page
public page: Page
public readonly context: BrowserContext
public readonly testInfo: TestInfo
public electronApp?: ElectronApplication
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this._page = page
this.page = page
this.context = context
this.testInfo = testInfo
}
async initialise(
arg: {
fixtures: Partial<Fixtures>
folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean
appSettings?: Partial<SaveSettingsPayload>
} = { fixtures: {} }
) {
const { electronApp, page } = await setupElectron({
testInfo: this.testInfo,
folderSetupFn: arg.folderSetupFn,
cleanProjectDir: arg.cleanProjectDir,
appSettings: arg.appSettings,
})
this.page = page
this.electronApp = electronApp
await page.setViewportSize({ width: 1200, height: 500 })
for (const key of unsafeTypedKeys(arg.fixtures)) {
const fixture = arg.fixtures[key]
if (
!fixture ||
fixture instanceof AuthenticatedApp ||
fixture instanceof AuthenticatedTronApp
)
continue
fixture.reConstruct(page)
}
}
close = async () => {
await this.electronApp?.close?.()
}
debugPause = () =>
new Promise(() => {
console.log('UN-RESOLVING PROMISE')
})
}
export const test = base.extend<Fixtures>({
app: async ({ page, context }, use, testInfo) => {
await use(new AuthenticatedApp(context, page, testInfo))
},
tronApp: async ({ page, context }, use, testInfo) => {
await use(new AuthenticatedTronApp(context, page, testInfo))
},
cmdBar: async ({ page }, use) => {
await use(new CmdBarFixture(page))
},
editor: async ({ page }, use) => {
await use(new EditorFixture(page))
},
toolbar: async ({ page }, use) => {
await use(new ToolbarFixture(page))
},
scene: async ({ page }, use) => {
await use(new SceneFixture(page))
},
homePage: async ({ page }, use) => {
await use(new HomePageFixture(page))
},
})
test.afterEach(async ({ page }, testInfo) => {
await tearDown(page, testInfo)
})
export { expect } from '@playwright/test'

View File

@ -0,0 +1,103 @@
import type { Page, Locator } from '@playwright/test'
import { expect } from '@playwright/test'
interface ProjectCardState {
title: string
fileCount: number
folderCount: number
}
interface HomePageState {
projectCards: ProjectCardState[]
sortBy: 'last-modified-desc' | 'last-modified-asc' | 'name-asc' | 'name-desc'
}
export class HomePageFixture {
public page: Page
projectCard!: Locator
projectCardTitle!: Locator
projectCardFile!: Locator
projectCardFolder!: Locator
sortByDateBtn!: Locator
sortByNameBtn!: Locator
constructor(page: Page) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.projectCard = this.page.getByTestId('project-link')
this.projectCardTitle = this.page.getByTestId('project-title')
this.projectCardFile = this.page.getByTestId('project-file-count')
this.projectCardFolder = this.page.getByTestId('project-folder-count')
this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified')
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
}
private _serialiseSortBy = async (): Promise<
HomePageState['sortBy'] | null
> => {
const [dateBtnDesc, dateBtnAsc, nameBtnDesc, nameBtnAsc] =
await Promise.all([
this.sortByDateBtn.getByLabel('arrow down').isVisible(),
this.sortByDateBtn.getByLabel('arrow up').isVisible(),
this.sortByNameBtn.getByLabel('arrow down').isVisible(),
this.sortByNameBtn.getByLabel('arrow up').isVisible(),
])
if (dateBtnDesc) return 'last-modified-desc'
if (dateBtnAsc) return 'last-modified-asc'
if (nameBtnDesc) return 'name-desc'
if (nameBtnAsc) return 'name-asc'
return null
}
private _serialiseProjectCards = async (): Promise<
Array<ProjectCardState>
> => {
const projectCards = await this.projectCard.all()
const projectCardStates: Array<ProjectCardState> = []
for (const projectCard of projectCards) {
const [title, fileCount, folderCount] = await Promise.all([
(await projectCard.locator(this.projectCardTitle).textContent()) || '',
Number(await projectCard.locator(this.projectCardFile).textContent()),
Number(await projectCard.locator(this.projectCardFolder).textContent()),
])
projectCardStates.push({
title: title,
fileCount,
folderCount,
})
}
return projectCardStates
}
/**
* Date is excluded from expectState, since it changes
* Maybe there a good sanity check we can do each time?
*/
expectState = async (expectedState: HomePageState) => {
await expect
.poll(async () => {
const [projectCards, sortBy] = await Promise.all([
this._serialiseProjectCards(),
this._serialiseSortBy(),
])
return {
projectCards,
sortBy,
}
})
.toEqual(expectedState)
}
openProject = async (projectTitle: string) => {
const projectCard = this.projectCard.locator(
this.page.getByText(projectTitle)
)
await projectCard.click()
}
}

View File

@ -6,18 +6,24 @@ import {
doAndWaitForImageDiff,
openAndClearDebugPanel,
sendCustomCmd,
} from './test-utils'
} from '../test-utils'
type mouseParams = {
pixelDiff: number
}
export class SceneFixture {
public readonly page: Page
private readonly exeIndicator: Locator
public page: Page
private exeIndicator!: Locator
constructor(page: Page) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
}
@ -25,33 +31,38 @@ export class SceneFixture {
x: number,
y: number,
{ steps }: { steps: number } = { steps: 5000 }
) => [
(params?: mouseParams) => {
if (params?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
() => this.page.mouse.click(x, y),
params.pixelDiff
)
}
return this.page.mouse.click(x, y)
},
(params?: mouseParams) => {
if (params?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
() => this.page.mouse.move(x, y, { steps }),
params.pixelDiff
)
}
return this.page.mouse.move(x, y, { steps })
},
]
) =>
[
(clickParams?: mouseParams) => {
if (clickParams?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
() => this.page.mouse.click(x, y),
clickParams.pixelDiff
)
}
return this.page.mouse.click(x, y)
},
(moveParams?: mouseParams) => {
if (moveParams?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
() => this.page.mouse.move(x, y, { steps }),
moveParams.pixelDiff
)
}
return this.page.mouse.move(x, y, { steps })
},
] as const
/** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene.
*
* Expects the viewPort to be 1000x500 */
clickNoWhere = () => this.page.mouse.click(998, 60)
/** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene.
*
* Expects the viewPort to be 1000x500 */
moveNoWhere = (steps?: number) => this.page.mouse.move(998, 60, { steps })
moveCameraTo = async (
pos: { x: number; y: number; z: number },

View File

@ -0,0 +1,79 @@
import type { Page, Locator } from '@playwright/test'
import { expect } from './fixtureSetup'
import { doAndWaitForImageDiff } from '../test-utils'
export class ToolbarFixture {
public page: Page
extrudeButton!: Locator
startSketchBtn!: Locator
rectangleBtn!: Locator
exitSketchBtn!: Locator
editSketchBtn!: Locator
fileTreeBtn!: Locator
createFileBtn!: Locator
fileCreateToast!: Locator
filePane!: Locator
exeIndicator!: Locator
constructor(page: Page) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.startSketchBtn = page.getByTestId('sketch')
this.rectangleBtn = page.getByTestId('corner-rectangle')
this.exitSketchBtn = page.getByTestId('sketch-exit')
this.editSketchBtn = page.getByText('Edit Sketch')
this.fileTreeBtn = page.locator('[id="files-button-holder"]')
this.createFileBtn = page.getByTestId('create-file-button')
this.filePane = page.locator('#files-pane')
this.fileCreateToast = page.getByText('Successfully created')
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
}
startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
editSketch = async () => {
await this.editSketchBtn.first().click()
// One of the rare times we want to allow a arbitrary wait
// this is for the engine animation, as it takes 500ms to complete
await this.page.waitForTimeout(600)
}
private _serialiseFileTree = async () => {
return this.page
.locator('#files-pane')
.getByTestId('file-tree-item')
.allInnerTexts()
}
/**
* TODO folders, in expect state
*/
expectFileTreeState = async (expected: string[]) => {
await expect.poll(this._serialiseFileTree).toEqual(expected)
}
createFile = async ({ wait }: { wait: boolean } = { wait: false }) => {
await this.createFileBtn.click()
await expect(this.fileCreateToast).toBeVisible()
if (wait) {
await this.fileCreateToast.waitFor({ state: 'detached' })
}
}
/**
* Opens file by it's name and waits for execution to finish
*/
openFile = async (
fileName: string,
{ wait }: { wait?: boolean } = { wait: true }
) => {
await this.filePane.getByText(fileName).click()
if (wait) {
await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
}
}
}

View File

@ -0,0 +1,7 @@
import { test, expect } from '@playwright/test'
/** @deprecated, import from ./fixtureSetup.ts instead */
export const _test = test
/** @deprecated, import from ./fixtureSetup.ts instead */
export const _expect = expect

View File

@ -1,7 +1,7 @@
import { test, expect, AuthenticatedApp } from './fixtureSetup'
import { EditorFixture } from './editorFixture'
import { SceneFixture } from './sceneFixture'
import { ToolbarFixture } from './toolbarFixture'
import { test, expect, AuthenticatedApp } from './fixtures/fixtureSetup'
import { EditorFixture } from './fixtures/editorFixture'
import { SceneFixture } from './fixtures/sceneFixture'
import { ToolbarFixture } from './fixtures/toolbarFixture'
// test file is for testing point an click code gen functionality that's not sketch mode related

View File

@ -455,6 +455,7 @@ test(
await page.mouse.move(startXPx + PUR * 20, 500 - PUR * 10)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
@ -474,6 +475,7 @@ test(
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
}
)
@ -531,6 +533,7 @@ test(
// Ensure the draft rectangle looks the same as it usually does
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
}
)
@ -585,6 +588,7 @@ test(
// Ensure the draft rectangle looks the same as it usually does
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
await expect(page.locator('.cm-content')).toHaveText(
`const sketch001 = startSketchOn('XZ')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -1066,3 +1066,7 @@ export async function openAndClearDebugPanel(page: Page) {
await openDebugPanel(page)
return clearCommandLogs(page)
}
export function sansWhitespace(str: string) {
return str.replace(/\s+/g, '').trim()
}

View File

@ -1,21 +0,0 @@
import type { Page, Locator } from '@playwright/test'
import { doAndWaitForImageDiff } from './test-utils'
export class ToolbarFixture {
public readonly page: Page
readonly extrudeButton: Locator
readonly startSketchBtn: Locator
readonly rectangleBtn: Locator
readonly exitSketchBtn: Locator
constructor(page: Page) {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.startSketchBtn = page.getByTestId('sketch')
this.rectangleBtn = page.getByTestId('corner-rectangle')
this.exitSketchBtn = page.getByTestId('sketch-exit')
}
startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
}

View File

@ -306,9 +306,19 @@ export class CameraControls {
event: 'camera_drag_end',
callback: cb,
})
this.engineCommandManager.subscribeToUnreliable({
event: 'default_camera_zoom',
callback: (a) => {
console.log('zoom', a)
cb(a)
},
})
this.engineCommandManager.subscribeTo({
event: 'default_camera_zoom',
callback: cb,
callback: (a) => {
console.log('zoom', a)
cb(a)
},
})
this.engineCommandManager.subscribeTo({
event: 'default_camera_get_settings',

View File

@ -194,7 +194,7 @@ const FileTreeItem = ({
}
return (
<div className="contents" ref={itemRef}>
<div className="contents" data-testid="file-tree-item" ref={itemRef}>
{fileOrDir.children === null ? (
<li
className={
@ -389,12 +389,14 @@ interface FileTreeProps {
export const FileTreeMenu = () => {
const { send } = useFileContext()
const { send: modelingSend } = useModelingContext()
function createFile() {
send({
type: 'Create file',
data: { name: '', makeDir: false, shouldSetToRename: true },
})
modelingSend({ type: 'Cancel' })
}
function createFolder() {

View File

@ -104,20 +104,33 @@ function ProjectCard({
ref={inputRef}
/>
) : (
<h3 className="font-sans relative z-0 p-2">
<h3
className="font-sans relative z-0 p-2"
data-testid="project-title"
>
{project.name?.replace(FILE_EXT, '')}
</h3>
)}
<span className="px-2 text-chalkboard-60 text-xs">
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
{numberOfFolders > 0 &&
`/ ${numberOfFolders} folder${numberOfFolders === 1 ? '' : 's'}`}
<span data-testid="project-file-count">{numberOfFiles}</span> file
{numberOfFiles === 1 ? '' : 's'}{' '}
{numberOfFolders > 0 && (
<>
{'/ '}
<span data-testid="project-folder-count">
{numberOfFolders}
</span>{' '}
folder{numberOfFolders === 1 ? '' : 's'}
</>
)}
</span>
<span className="px-2 text-chalkboard-60 text-xs">
Edited{' '}
{project.metadata && project.metadata.modified
? getDisplayedTime(parseInt(project.metadata.modified))
: 'never'}
<span data-testid="project-edit-date">
{project.metadata && project.metadata.modified
? getDisplayedTime(parseInt(project.metadata.modified))
: 'never'}
</span>
</span>
</div>
</Link>

View File

@ -1230,7 +1230,7 @@ type ModelTypes = Models['OkModelingCmdResponse_type']['type']
type UnreliableResponses = Extract<
Models['OkModelingCmdResponse_type'],
{ type: 'highlight_set_entity' | 'camera_drag_move' }
{ type: 'highlight_set_entity' | 'camera_drag_move' | 'default_camera_zoom' }
>
export interface UnreliableSubscription<T extends UnreliableResponses['type']> {
event: T
@ -1572,9 +1572,11 @@ export class EngineCommandManager extends EventTarget {
'message',
(event: MessageEvent) => {
const result: UnreliableResponses = JSON.parse(event.data)
console.log('result', result)
Object.values(
this.unreliableSubscriptions[result.type] || {}
).forEach(
// TODO: There is only one response that uses the unreliable channel atm,
// highlight_set_entity, if there are more it's likely they will all have the same
// sequence logic, but I'm not sure if we use a single global sequence or a sequence
@ -1933,6 +1935,9 @@ export class EngineCommandManager extends EventTarget {
) {
;(cmd as any).sequence = this.outSequence
this.outSequence++
if(cmd.type === 'default_camera_zoom') {
console.log('sending zoom', cmd)
}
this.engineConnection?.unreliableSend(command)
return Promise.resolve(null)
} else if (

View File

@ -15,6 +15,30 @@ export function isArray(val: any): val is unknown[] {
}
/**
* An alternative to `Object.keys()` that returns an array of keys with types.
*
* It's UNSAFE because because of TS's structural subtyping and how at runtime, you can
* extend a JS object with whatever keys you want.
*
* Why we shouldn't be extending objects with arbitrary keys at run time, the structural subtyping
* issue could be a confusing bug, for example, in the below snippet `myKeys` is typed as
* `('x' | 'y')[]` but is really `('x' | 'y' | 'name')[]`
* ```ts
* interface Point { x: number; y: number }
* interface NamedPoint { x: number; y: number; name: string }
*
* let point: Point = { x: 1, y: 2 }
* let namedPoint: NamedPoint = { x: 1, y: 2, name: 'A' }
*
* // Structural subtyping allows this assignment
* point = namedPoint // This is allowed because NamedPoint has all properties of Point
* const myKeys = unsafeTypedKeys(point) // typed as ('x' | 'y')[] but is really ('x' | 'y' | 'name')[]
* ```
*/
export function unsafeTypedKeys<T extends object>(obj: T): Array<keyof T> {
return Object.keys(obj) as Array<keyof T>
}
/*
* Predicate that checks if a value is not null and not undefined. This is
* useful for functions like Array::filter() and Array::find() that have
* overloads that accept a type guard.

View File

@ -78,7 +78,7 @@ export const authMachine = setup({
),
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwBmAEzYA7ABYAbAFZTcgBzGbN44adWANCACeiKbGdthypk4AnBFyVs6uQXYAvom+aFh4BMTk1LSQjExgAE6FVIXYKmIAhuhkpQC2GcLikpDSDPJKSCBqGlo6XQYIrk7YETYWctYRxmMWFk6+AUPj2I5OdjZyrnZOFmbJqRg4Ern0zDkABFQYHbo9mtoMuoOGFhHYxlZOhvbOsUGGRaIL4WbBONzWQxWYwWOx2H4HEBpY4tCAAeQwTEuskUd3UD36oEGIlMNlCuzk8Js0TcVisgP8iG2lmcGysb0mW3ByRSIAYVAgcF0yLxvUez0QIms5ImVJpNjpDKWxmw9PGdLh4Te00+iORjSylFRjFFBKeA0QThGQWcexMwWhniBCGiqrepisUVMdlszgieqO2BOdBNXXufXNRKMHtGVuphlJkXs4Wdriso2CCasdgipOidID6WDkAx6FNEYlCAT5jmcjrckMdj2b3GzpsjbBMVMWezDbGPMSQA */
/** @xstate-layout N4IgpgJg5mDOIC5QEECuAXAFgOgMabFwGsBJAMwBkB7KGCEgOwGIIqGxsBLBgNyqI75CRALQAbGnRHcA2gAYAuolAAHKrE7pObZSAAeiAIwAWQ9gBspuQCYAnAGYAHPYCsx+4ccAaEAE9E1q7YcoZyxrYR1m7mcrYAvnE+aFh4BMTk1LSQjExgAE55VHnYKmIAhuhkRQC2qcLikpDSDPJKSCBqGlo67QYI9gDs5tge5o6h5vau7oY+-v3mA9jWco4u5iu21ua2YcYJSRg4Eln0zJkABFQYrbqdmtoMun2GA7YjxuPmLqvGNh5zRCfJaOcyLUzuAYuFyGcwHEDJY6NCAAeQwTEuskUd3UDx6oD6Im2wUcAzkMJ2cjBxlMgIWLmwZLWljecjJTjh8IYVAgcF0iJxXUez0QIgGxhJZIpu2ptL8AWwtje1nCW2iq1shns8MRdXSlGRjEFeKevUQjkcy3sqwGHimbg83nlCF22GMytVUWMMUc8USCKO2BOdCN7Xu3VNBKMKsVFp2hm2vu+1id83slkVrgTxhcW0pNJ1geDkDR6GNEZFCAT1kZZLk9cMLltb0WdPMjewjjC1mzOZCtk5CSAA */
id: 'Auth',
initial: 'checkIfLoggedIn',
context: {
@ -162,15 +162,15 @@ async function getUser(input: { token?: string }) {
}
}
const userPromise = !isDesktop()
? fetch(url, {
const userPromise = isDesktop()
? getUserDesktop(token, VITE_KC_API_BASE_URL)
: fetch(url, {
method: 'GET',
credentials: 'include',
headers,
})
.then((res) => res.json())
.catch((err) => console.error('error from Browser getUser', err))
: getUserDesktop(input.token ?? '', VITE_KC_API_BASE_URL)
const user = await userPromise

View File

@ -248,6 +248,7 @@ const Home = () => {
<small>Sort by</small>
<ActionButton
Element="button"
data-testid="home-sort-by-name"
className={
'text-xs border-primary/10 ' +
(!sort.includes('name')
@ -269,6 +270,7 @@ const Home = () => {
</ActionButton>
<ActionButton
Element="button"
data-testid="home-sort-by-modified"
className={
'text-xs border-primary/10 ' +
(!isSortByModified

1271
src/wasm-lib/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -24,12 +24,11 @@ wasm-bindgen-futures = "0.4.42"
[dev-dependencies]
anyhow = "1"
hyper = { version = "0.14.29", features = ["server", "http1"] }
image = { version = "0.25.1", default-features = false, features = ["png"] }
kittycad = { workspace = true, default-features = true }
kittycad-modeling-cmds = { workspace = true }
pretty_assertions = "1.4.1"
reqwest = { version = "0.11.26", default-features = false }
reqwest = { version = "0.12", default-features = false }
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
twenty-twenty = "0.8"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
@ -67,12 +66,12 @@ members = [
"kcl",
"kcl-macros",
"kcl-test-server",
"kcl-to-core",
]
[workspace.dependencies]
http = "0.2.12"
kittycad = { version = "0.3.20", default-features = false, features = ["js", "requests"] }
kittycad-modeling-session = "0.1.4"
http = "1"
kittycad = { version = "0.3.23", default-features = false, features = ["js", "requests"] }
kittycad-modeling-cmds = { version = "0.2.64", features = ["websocket"] }
[[test]]

View File

@ -20,7 +20,7 @@ quote = "1"
regex = "1.10"
serde = { version = "1.0.210", features = ["derive"] }
serde_tokenstream = "0.2"
syn = { version = "2.0.77", features = ["full"] }
syn = { version = "2.0.79", features = ["full"] }
[dev-dependencies]
anyhow = "1.0.89"

View File

@ -758,7 +758,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();

View File

@ -14,7 +14,7 @@ mod test_examples_someFn {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}

View File

@ -14,7 +14,7 @@ mod test_examples_someFn {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}

View File

@ -14,7 +14,7 @@ mod test_examples_show {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}
@ -47,7 +47,7 @@ mod test_examples_show {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}

View File

@ -14,7 +14,7 @@ mod test_examples_show {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}

View File

@ -15,7 +15,7 @@ mod test_examples_my_func {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}
@ -48,7 +48,7 @@ mod test_examples_my_func {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}

View File

@ -15,7 +15,7 @@ mod test_examples_line_to {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}
@ -48,7 +48,7 @@ mod test_examples_line_to {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}

View File

@ -14,7 +14,7 @@ mod test_examples_min {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}
@ -47,7 +47,7 @@ mod test_examples_min {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}

View File

@ -14,7 +14,7 @@ mod test_examples_show {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}

View File

@ -14,7 +14,7 @@ mod test_examples_import {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}

View File

@ -14,7 +14,7 @@ mod test_examples_import {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}

View File

@ -14,7 +14,7 @@ mod test_examples_import {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}

View File

@ -14,7 +14,7 @@ mod test_examples_show {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}

View File

@ -14,7 +14,7 @@ mod test_examples_some_function {
fs: std::sync::Arc::new(crate::fs::FileManager::new()),
stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None).await.unwrap();
}

View File

@ -15,7 +15,7 @@ databake = "0.1.8"
kcl-lib = { path = "../kcl" }
proc-macro2 = "1"
quote = "1"
syn = { version = "2.0.77", features = ["full"] }
syn = { version = "2.0.79", features = ["full"] }
[dev-dependencies]
pretty_assertions = "1.4.1"

View File

@ -7,7 +7,7 @@ license = "MIT"
[dependencies]
anyhow = "1.0.89"
hyper = { version = "0.14.29", features = ["server"] }
hyper = { version = "0.14.29", features = ["http1", "server", "tcp"] }
kcl-lib = { version = "0.2", path = "../kcl" }
pico-args = "0.5.0"
serde = { version = "1.0.210", features = ["derive"] }

View File

@ -0,0 +1,23 @@
[package]
name = "kcl-to-core"
description = "Utility methods to convert kcl to engine core executable tests"
version = "0.1.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
[lib]
[[bin]]
name = "kcl-to-core"
path = "src/tool.rs"
[dependencies]
anyhow = "1"
async-trait = "0.1.81"
indexmap = "2.5.0"
kcl-lib = { path = "../kcl" }
kittycad = { workspace = true, features = ["clap"] }
kittycad-modeling-cmds = { workspace = true }
tokio = { version = "1.38", features = ["full", "time", "rt", "tracing"] }
uuid = { version = "1.9.1", features = ["v4", "js", "serde"] }

View File

@ -0,0 +1,469 @@
use anyhow::Result;
use indexmap::IndexMap;
use kcl_lib::{errors::KclError, executor::DefaultPlanes};
use kittycad_modeling_cmds::{
self as kcmc,
id::ModelingCmdId,
ok_response::OkModelingCmdResponse,
shared::PathSegment::{self, *},
websocket::{ModelingBatch, ModelingCmdReq, OkWebSocketResponseData, WebSocketRequest, WebSocketResponse},
};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use tokio::sync::RwLock;
const CPP_PREFIX: &str = "const double scaleFactor = 100;\n";
const NEED_PLANES: bool = true;
#[derive(Debug, Clone)]
pub struct EngineConnection {
batch: Arc<Mutex<Vec<(WebSocketRequest, kcl_lib::executor::SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::executor::SourceRange)>>>,
core_test: Arc<Mutex<String>>,
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
}
impl EngineConnection {
pub async fn new(result: Arc<Mutex<String>>) -> Result<EngineConnection> {
if let Ok(mut code) = result.lock() {
code.push_str(CPP_PREFIX);
}
Ok(EngineConnection {
batch: Arc::new(Mutex::new(Vec::new())),
batch_end: Arc::new(Mutex::new(IndexMap::new())),
core_test: result,
default_planes: Default::default(),
})
}
fn handle_command(&self, cmd_id: &ModelingCmdId, cmd: &kcmc::ModelingCmd) -> (String, OkModelingCmdResponse) {
let cpp_id = id_to_cpp(cmd_id);
let cmd_id = format!("{}", cmd_id);
let mut this_response = OkModelingCmdResponse::Empty {};
let new_code = match cmd {
kcmc::ModelingCmd::ObjectVisible(kcmc::ObjectVisible { hidden, object_id }) => {
format!(r#"scene->getSceneObject(Utils::UUID("{object_id}"))->setHidden({hidden});"#)
}
kcmc::ModelingCmd::EnableSketchMode(kcmc::EnableSketchMode {
entity_id,
animated: _,
ortho: _,
adjust_camera: _,
planar_normal,
}) => {
if let Some(normal) = planar_normal {
format!(
r#"
if(!scene->enableSketchMode(Utils::UUID("{entity_id}"), glm::dvec3 {{ {}, {}, {}, }}, nullopt))
{{
Utils::Plane plane_{cpp_id}(glm::dvec3 {{ 0, 0, 0 }}, glm::dvec3 {{ 1, 0, 0 }}, glm::dvec3 {{ 0, 1, 0 }});
scene->enableSketchMode(plane_{cpp_id}, nullopt, nullopt, false);
}}
"#,
normal.x, normal.y, normal.z
)
} else {
"".into()
}
}
kcmc::ModelingCmd::SketchModeDisable(kcmc::SketchModeDisable {}) => "scene->disableSketchMode();".into(),
kcmc::ModelingCmd::MakePlane(kcmc::MakePlane {
origin,
x_axis,
y_axis,
size,
..
}) => {
let plane_id = format!("plane_{}", cpp_id);
format!(
r#"
auto {plane_id} = make_shared<Object>("plane", glm::dvec3 {{ 0, 0, 0 }});
{plane_id}->setUUID(Utils::UUID("{cmd_id}"));
{plane_id}->makePlane(glm::dvec3 {{ {}, {}, {} }} * scaleFactor, glm::dvec3 {{ {}, {}, {} }}, glm::dvec3 {{ {}, {}, {} }}, {}, false);
{plane_id}->setHidden();
scene->addSceneObject({plane_id});
"#,
origin.x.0,
origin.y.0,
origin.z.0,
x_axis.x,
x_axis.y,
x_axis.z,
y_axis.x,
y_axis.y,
y_axis.z,
size.0
)
}
kcmc::ModelingCmd::StartPath(kcmc::StartPath {}) => {
let sketch_id = format!("sketch_{}", cpp_id);
let path_id = format!("path_{}", cpp_id);
format!(
r#"
auto {sketch_id} = make_shared<Object>("sketch", glm::dvec3 {{ 0, 0, 0 }});
{sketch_id}->setUUID(Utils::UUID("{cmd_id}"));
{sketch_id}->makePath(true);
auto {path_id} = {sketch_id}->get<Model::Brep::Path>();
scene->addSceneObject({sketch_id});
"#
)
}
kcmc::ModelingCmd::MovePathPen(kcmc::MovePathPen { path, to }) => {
format!(
r#"
path_{}->moveTo(glm::dvec3 {{ {}, {}, 0.0 }} * scaleFactor);
"#,
id_to_cpp(path),
to.x.0,
to.y.0
)
}
kcmc::ModelingCmd::ExtendPath(kcmc::ExtendPath { path, segment }) => match segment {
Line { end, relative } => {
format!(
r#"
path_{}->lineTo(glm::dvec3 {{ {}, {}, 0.0 }} * scaleFactor, {{ {} }});
"#,
id_to_cpp(path),
end.x.0,
end.y.0,
relative
)
}
PathSegment::Arc {
center,
radius,
start,
end,
relative,
} => {
let start = start.value;
let end = end.value;
let radius = radius.0;
format!(
r#"
path_{}->addArc(glm::dvec2 {{ {}, {} }} * scaleFactor, {radius} * scaleFactor, {start}, {end}, {{ {} }});
"#,
id_to_cpp(path),
center.x.0,
center.y.0,
relative
)
}
PathSegment::TangentialArcTo {
angle_snap_increment: _,
to,
} => {
format!(
r#"
path_{}->tangentialArcTo(glm::dvec3 {{ {}, {}, {} }} * scaleFactor, nullopt, {{ true }});
"#,
id_to_cpp(path),
to.x.0,
to.y.0,
to.z.0,
)
}
_ => {
format!("//{:?}", cmd)
}
},
kcmc::ModelingCmd::ClosePath(kcmc::ClosePath { path_id }) => {
format!(
r#"
path_{}->close();
sketch_{}->toSolid2D();
"#,
uuid_to_cpp(path_id),
uuid_to_cpp(path_id)
)
}
kcmc::ModelingCmd::Extrude(kcmc::Extrude { distance, target }) => {
format!(
r#"
scene->getSceneObject(Utils::UUID("{target}"))->extrudeToSolid3D({} * scaleFactor, true);
"#,
distance.0
)
}
kcmc::ModelingCmd::Revolve(kcmc::Revolve {
angle,
axis,
axis_is_2d,
origin,
target,
tolerance,
}) => {
let ox = origin.x.0;
let oy = origin.y.0;
let oz = origin.z.0;
let ax = axis.x;
let ay = axis.y;
let az = axis.z;
let angle = angle.value;
let tolerance = tolerance.0;
format!(
r#"
scene->getSceneObject(Utils::UUID("{target}"))->revolveToSolid3D(nullopt, glm::dvec3 {{ {ox}, {oy}, {oz} }} * scaleFactor, glm::dvec3 {{ {ax}, {ay}, {az} }}, {axis_is_2d}, {angle}, {tolerance});
"#
)
}
kcmc::ModelingCmd::Solid2dAddHole(kcmc::Solid2dAddHole { hole_id, object_id }) => {
format!(
r#"scene->getSceneObject(Utils::UUID("{object_id}"))->get<Model::Brep::Solid2D>()->addHole(
make_shared<Model::Brep::Path>(*scene->getSceneObject(Utils::UUID("{hole_id}"))->get<Model::Brep::Solid2D>()->getPath())
);"#
)
}
kcmc::ModelingCmd::Solid3dGetExtrusionFaceInfo(kcmc::Solid3dGetExtrusionFaceInfo {
object_id,
edge_id,
}) => {
format!(
r#"
//face info get {} {}
"#,
object_id, edge_id
)
}
kcmc::ModelingCmd::EntityCircularPattern(kcmc::EntityCircularPattern {
entity_id,
axis,
center,
num_repetitions,
arc_degrees,
rotate_duplicates,
}) => {
let entity_ids = generate_repl_uuids(*num_repetitions as usize);
this_response = OkModelingCmdResponse::EntityCircularPattern(kcmc::output::EntityCircularPattern {
entity_ids: entity_ids.clone(),
});
let mut base_code: String = format!(
r#"
auto reps_{cpp_id} = scene->entityCircularPattern(Utils::UUID("{}"), {num_repetitions}, glm::dvec3 {{ {}, {}, {} }} * scaleFactor, glm::dvec3 {{ {}, {}, {} }} * scaleFactor, {arc_degrees}, {rotate_duplicates});
"#,
entity_id, axis.x, axis.y, axis.z, center.x.0, center.y.0, center.z.0
);
let repl_uuid_fix_code = codegen_cpp_repl_uuid_setters(&cpp_id, &entity_ids);
base_code.push_str(&repl_uuid_fix_code);
base_code
}
kcmc::ModelingCmd::EntityLinearPattern(kcmc::EntityLinearPattern {
entity_id: _,
axis: _,
num_repetitions: _,
spacing: _,
}) => {
// let num_transforms = transforms.len();
// let num_repetitions = transform.iter().map(|t| if t.replicate { 1 } else { 0 } ).sum();
// let mut base_code: String = format!(
// r#"
// std::vector<std::optional<Scene::Scene::LinearPatternTransform>> transforms_{cpp_id}({num_transforms});
// "#);
// for t in transform {
// translations_xyz.push(t.translate.x.to_millimeters(state.units));
// translations_xyz.push(t.translate.y.to_millimeters(state.units));
// translations_xyz.push(t.translate.z.to_millimeters(state.units));
// scale_xyz.push(t.scale.x);
// scale_xyz.push(t.scale.y);
// scale_xyz.push(t.scale.z);
// }
// let entity_ids = generate_repl_uuids(*num_repetitions as usize);
// this_response = OkModelingCmdResponse::EntityLinearPattern {
// data: kittycad::types::EntityLinearPattern {
// entity_ids: entity_ids.clone(),
// },
// };
// let mut base_code: String = format!(
// r#"
// auto reps_{cpp_id} = scene->entityCircularPattern(Utils::UUID("{}"), {num_repetitions}, glm::dvec3 {{ {}, {}, {} }} * scaleFactor, glm::dvec3 {{ {}, {}, {} }} * scaleFactor, {arc_degrees}, {rotate_duplicates});
// "#,
// entity_id, axis.x, axis.y, axis.z, center.x, center.y, center.z
// );
// let repl_uuid_fix_code = codegen_cpp_repl_uuid_setters(&cpp_id, &entity_ids);
// base_code.push_str(&repl_uuid_fix_code);
// base_code
format!("//{:?}", cmd)
}
_ => {
//helps us follow along with the currently unhandled engine commands
format!("//{:?}", cmd)
}
};
(new_code, this_response)
}
}
fn id_to_cpp(id: &ModelingCmdId) -> String {
uuid_to_cpp(&id.0)
}
fn uuid_to_cpp(id: &uuid::Uuid) -> String {
let str = format!("{}", id);
str::replace(&str, "-", "_")
}
fn generate_repl_uuids(count: usize) -> Vec<uuid::Uuid> {
let mut repl_ids: Vec<uuid::Uuid> = Vec::new();
repl_ids.resize_with(count, uuid::Uuid::new_v4);
repl_ids
}
fn codegen_cpp_repl_uuid_setters(reps_id: &str, entity_ids: &[uuid::Uuid]) -> String {
let mut codegen = String::new();
for (i, id) in entity_ids.iter().enumerate() {
let cpp_id = uuid_to_cpp(id);
let iter = format!(
r#"
//change object id -> {id}
auto repl_{cpp_id} = scene->getSceneObject(reps_{reps_id}[{i}]);
scene->removeSceneObject(repl_{cpp_id}->getUUID(), false);
repl_{cpp_id}->setUUID(Utils::UUID("{id}"));
scene->addSceneObject(repl_{cpp_id});
"#
);
codegen.push_str(&iter);
}
codegen
}
#[async_trait::async_trait]
impl kcl_lib::engine::EngineManager for EngineConnection {
fn batch(&self) -> Arc<Mutex<Vec<(WebSocketRequest, kcl_lib::executor::SourceRange)>>> {
self.batch.clone()
}
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::executor::SourceRange)>>> {
self.batch_end.clone()
}
async fn default_planes(&self, source_range: kcl_lib::executor::SourceRange) -> Result<DefaultPlanes, KclError> {
if NEED_PLANES {
{
let opt = self.default_planes.read().await.as_ref().cloned();
if let Some(planes) = opt {
return Ok(planes);
}
} // drop the read lock
let new_planes = self.new_default_planes(source_range).await?;
*self.default_planes.write().await = Some(new_planes.clone());
Ok(new_planes)
} else {
Ok(DefaultPlanes::default())
}
}
async fn clear_scene_post_hook(&self, _source_range: kcl_lib::executor::SourceRange) -> Result<(), KclError> {
Ok(())
}
async fn inner_send_modeling_cmd(
&self,
id: uuid::Uuid,
_source_range: kcl_lib::executor::SourceRange,
cmd: WebSocketRequest,
_id_to_source_range: std::collections::HashMap<uuid::Uuid, kcl_lib::executor::SourceRange>,
) -> Result<WebSocketResponse, KclError> {
match cmd {
WebSocketRequest::ModelingCmdBatchReq(ModelingBatch {
ref requests,
batch_id: _,
responses: _,
}) => {
let mut responses = HashMap::new();
for request in requests {
let (new_code, this_response);
if let Ok(mut test_code) = self.core_test.lock() {
(new_code, this_response) = self.handle_command(&request.cmd_id, &request.cmd);
if !new_code.is_empty() {
let new_code = new_code
.trim()
.split(' ')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
+ "\n";
//println!("{new_code}");
test_code.push_str(&new_code);
}
} else {
this_response = OkModelingCmdResponse::Empty {};
}
responses.insert(
request.cmd_id,
kcmc::websocket::BatchResponse::Success {
response: this_response,
},
);
}
Ok(WebSocketResponse::Success(kcmc::websocket::SuccessWebSocketResponse {
success: true,
request_id: Some(id),
resp: OkWebSocketResponseData::ModelingBatch { responses },
}))
}
WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd, cmd_id }) => {
//also handle unbatched requests inline
let (new_code, this_response);
if let Ok(mut test_code) = self.core_test.lock() {
(new_code, this_response) = self.handle_command(&cmd_id, &cmd);
if !new_code.is_empty() {
let new_code = new_code
.trim()
.split(' ')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
+ "\n";
//println!("{new_code}");
test_code.push_str(&new_code);
}
} else {
this_response = OkModelingCmdResponse::Empty {};
}
Ok(WebSocketResponse::Success(kcmc::websocket::SuccessWebSocketResponse {
success: true,
request_id: Some(id),
resp: OkWebSocketResponseData::Modeling {
modeling_response: this_response,
},
}))
}
_ => Ok(WebSocketResponse::Success(kcmc::websocket::SuccessWebSocketResponse {
success: true,
request_id: Some(id),
resp: OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::Empty {},
},
})),
}
}
}

View File

@ -0,0 +1,30 @@
use anyhow::Result;
use kcl_lib::executor::ExecutorContext;
use std::sync::{Arc, Mutex};
#[cfg(not(target_arch = "wasm32"))]
mod conn_mock_core;
///Converts the given kcl code to an engine test
pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
let tokens = kcl_lib::token::lexer(code)?;
let parser = kcl_lib::parser::Parser::new(tokens);
let program = parser.ast()?;
let result = Arc::new(Mutex::new("".into()));
let ref_result = Arc::clone(&result);
let ctx = ExecutorContext {
engine: Arc::new(Box::new(
crate::conn_mock_core::EngineConnection::new(ref_result).await?,
)),
fs: Arc::new(kcl_lib::fs::FileManager::new()),
stdlib: Arc::new(kcl_lib::std::StdLib::new()),
settings: Default::default(),
context_type: kcl_lib::executor::ContextType::MockCustomForwarded,
};
let _memory = ctx.run(&program, None).await?;
let result = result.lock().expect("mutex lock").clone();
Ok(result)
}

View File

@ -0,0 +1,19 @@
use kcl_to_core::*;
use std::{env, fs};
#[tokio::main]
async fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("Usage: kcl-to-core path/to/file.kcl");
return;
}
let file_path = &args[1];
let kcl = fs::read_to_string(file_path).expect("read file");
let result = kcl_to_engine_core(&kcl).await.expect("kcl conversion");
println!("{}", result);
}

View File

@ -0,0 +1,19 @@
use kcl_to_core::*;
#[tokio::test]
async fn kcl_to_core_test() {
let result = kcl_to_engine_core(
r#"
const part001 = startSketchOn('XY')
|> startProfileAt([11.19, 28.35], %)
|> line([28.67, -13.25], %, $here)
|> line([-4.12, -22.81], %)
|> line([-33.24, 14.55], %)
|> close(%)
|> extrude(5, %)
"#,
)
.await;
assert!(result.is_ok());
}

View File

@ -35,7 +35,7 @@ measurements = "0.11.0"
mime_guess = "2.0.5"
parse-display = "0.9.1"
pyo3 = { version = "0.22.3", optional = true }
reqwest = { version = "0.11.26", default-features = false, features = ["stream", "rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = ["stream", "rustls-tls"] }
ropey = "1.6.1"
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1", "preserve_order"] }
serde = { version = "1.0.210", features = ["derive"] }

View File

@ -18,7 +18,11 @@ use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, DocumentSymbol, FoldingRange, FoldingRangeKind, Range as LspRange, SymbolKind,
};
pub use crate::ast::types::{literal_value::LiteralValue, none::KclNone};
pub use crate::ast::types::{
condition::{ElseIf, IfExpression},
literal_value::LiteralValue,
none::KclNone,
};
use crate::{
docs::StdLibFn,
errors::{KclError, KclErrorDetails},
@ -30,12 +34,14 @@ use crate::{
std::{kcl_stdlib::KclStdLibFn, FunctionKind},
};
mod condition;
mod literal_value;
mod none;
/// Position-independent digest of the AST node.
pub type Digest = [u8; 32];
/// A KCL program top level, or function body.
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
@ -72,6 +78,7 @@ macro_rules! compute_digest {
}
};
}
pub(crate) use compute_digest;
impl Program {
compute_digest!(|slf, hasher| {
@ -82,6 +89,14 @@ impl Program {
hasher.update(slf.non_code_meta.compute_digest());
});
/// Is the last body item an expression?
pub fn ends_with_expr(&self) -> bool {
let Some(ref last) = self.body.last() else {
return false;
};
matches!(last, BodyItem::ExpressionStatement(_))
}
pub fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> {
// Check if we are in the non code meta.
if let Some(meta) = self.get_non_code_meta_for_position(pos) {
@ -501,6 +516,7 @@ pub enum Expr {
ObjectExpression(Box<ObjectExpression>),
MemberExpression(Box<MemberExpression>),
UnaryExpression(Box<UnaryExpression>),
IfExpression(Box<IfExpression>),
None(KclNone),
}
@ -519,6 +535,7 @@ impl Expr {
Expr::ObjectExpression(oe) => oe.compute_digest(),
Expr::MemberExpression(me) => me.compute_digest(),
Expr::UnaryExpression(ue) => ue.compute_digest(),
Expr::IfExpression(e) => e.compute_digest(),
Expr::None(_) => {
let mut hasher = Sha256::new();
hasher.update(b"Value::None");
@ -562,6 +579,7 @@ impl Expr {
Expr::PipeExpression(pipe_exp) => Some(&pipe_exp.non_code_meta),
Expr::UnaryExpression(_unary_exp) => None,
Expr::PipeSubstitution(_pipe_substitution) => None,
Expr::IfExpression(_) => None,
Expr::None(_none) => None,
}
}
@ -584,6 +602,7 @@ impl Expr {
Expr::TagDeclarator(_) => {}
Expr::PipeExpression(ref mut pipe_exp) => pipe_exp.replace_value(source_range, new_value),
Expr::UnaryExpression(ref mut unary_exp) => unary_exp.replace_value(source_range, new_value),
Expr::IfExpression(_) => {}
Expr::PipeSubstitution(_) => {}
Expr::None(_) => {}
}
@ -603,6 +622,7 @@ impl Expr {
Expr::ObjectExpression(object_expression) => object_expression.start(),
Expr::MemberExpression(member_expression) => member_expression.start(),
Expr::UnaryExpression(unary_expression) => unary_expression.start(),
Expr::IfExpression(expr) => expr.start(),
Expr::None(none) => none.start,
}
}
@ -621,6 +641,7 @@ impl Expr {
Expr::ObjectExpression(object_expression) => object_expression.end(),
Expr::MemberExpression(member_expression) => member_expression.end(),
Expr::UnaryExpression(unary_expression) => unary_expression.end(),
Expr::IfExpression(expr) => expr.end(),
Expr::None(none) => none.end,
}
}
@ -639,6 +660,7 @@ impl Expr {
Expr::ObjectExpression(object_expression) => object_expression.get_hover_value_for_position(pos, code),
Expr::MemberExpression(member_expression) => member_expression.get_hover_value_for_position(pos, code),
Expr::UnaryExpression(unary_expression) => unary_expression.get_hover_value_for_position(pos, code),
Expr::IfExpression(expr) => expr.get_hover_value_for_position(pos, code),
// TODO: LSP hover information for values/types. https://github.com/KittyCAD/modeling-app/issues/1126
Expr::None(_) => None,
Expr::Literal(_) => None,
@ -670,11 +692,12 @@ impl Expr {
member_expression.rename_identifiers(old_name, new_name)
}
Expr::UnaryExpression(ref mut unary_expression) => unary_expression.rename_identifiers(old_name, new_name),
Expr::IfExpression(ref mut expr) => expr.rename_identifiers(old_name, new_name),
Expr::None(_) => {}
}
}
/// Get the constraint level for a value type.
/// Get the constraint level for an expression.
pub fn get_constraint_level(&self) -> ConstraintLevel {
match self {
Expr::Literal(literal) => literal.get_constraint_level(),
@ -692,6 +715,7 @@ impl Expr {
Expr::ObjectExpression(object_expression) => object_expression.get_constraint_level(),
Expr::MemberExpression(member_expression) => member_expression.get_constraint_level(),
Expr::UnaryExpression(unary_expression) => unary_expression.get_constraint_level(),
Expr::IfExpression(expr) => expr.get_constraint_level(),
Expr::None(none) => none.get_constraint_level(),
}
}
@ -720,6 +744,7 @@ pub enum BinaryPart {
CallExpression(Box<CallExpression>),
UnaryExpression(Box<UnaryExpression>),
MemberExpression(Box<MemberExpression>),
IfExpression(Box<IfExpression>),
}
impl From<BinaryPart> for SourceRange {
@ -743,6 +768,7 @@ impl BinaryPart {
BinaryPart::CallExpression(ce) => ce.compute_digest(),
BinaryPart::UnaryExpression(ue) => ue.compute_digest(),
BinaryPart::MemberExpression(me) => me.compute_digest(),
BinaryPart::IfExpression(e) => e.compute_digest(),
}
}
@ -755,6 +781,7 @@ impl BinaryPart {
BinaryPart::CallExpression(call_expression) => call_expression.get_constraint_level(),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_constraint_level(),
BinaryPart::MemberExpression(member_expression) => member_expression.get_constraint_level(),
BinaryPart::IfExpression(e) => e.get_constraint_level(),
}
}
@ -772,6 +799,7 @@ impl BinaryPart {
unary_expression.replace_value(source_range, new_value)
}
BinaryPart::MemberExpression(_) => {}
BinaryPart::IfExpression(e) => e.replace_value(source_range, new_value),
}
}
@ -783,6 +811,7 @@ impl BinaryPart {
BinaryPart::CallExpression(call_expression) => call_expression.start(),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.start(),
BinaryPart::MemberExpression(member_expression) => member_expression.start(),
BinaryPart::IfExpression(e) => e.start(),
}
}
@ -794,6 +823,7 @@ impl BinaryPart {
BinaryPart::CallExpression(call_expression) => call_expression.end(),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.end(),
BinaryPart::MemberExpression(member_expression) => member_expression.end(),
BinaryPart::IfExpression(e) => e.end(),
}
}
@ -809,6 +839,7 @@ impl BinaryPart {
BinaryPart::CallExpression(call_expression) => call_expression.execute(exec_state, ctx).await,
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, ctx).await,
BinaryPart::MemberExpression(member_expression) => member_expression.get_result(exec_state),
BinaryPart::IfExpression(e) => e.get_result(exec_state, ctx).await,
}
}
@ -822,6 +853,7 @@ impl BinaryPart {
}
BinaryPart::CallExpression(call_expression) => call_expression.get_hover_value_for_position(pos, code),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_hover_value_for_position(pos, code),
BinaryPart::IfExpression(e) => e.get_hover_value_for_position(pos, code),
BinaryPart::MemberExpression(member_expression) => {
member_expression.get_hover_value_for_position(pos, code)
}
@ -845,6 +877,7 @@ impl BinaryPart {
BinaryPart::MemberExpression(ref mut member_expression) => {
member_expression.rename_identifiers(old_name, new_name)
}
BinaryPart::IfExpression(ref mut if_expression) => if_expression.rename_identifiers(old_name, new_name),
}
}
}
@ -1331,7 +1364,7 @@ impl CallExpression {
};
match exec_result {
Ok(()) => {}
Ok(_) => {}
Err(err) => {
// We need to override the source ranges so we don't get the embedded kcl
// function from the stdlib.
@ -3149,6 +3182,7 @@ async fn inner_execute_pipe_body(
| Expr::ObjectExpression(_)
| Expr::MemberExpression(_)
| Expr::UnaryExpression(_)
| Expr::IfExpression(_)
| Expr::None(_) => {}
};
let metadata = Metadata {

View File

@ -0,0 +1,215 @@
use crate::errors::KclError;
use crate::executor::BodyType;
use crate::executor::ExecState;
use crate::executor::ExecutorContext;
use crate::executor::KclValue;
use crate::executor::Metadata;
use crate::executor::SourceRange;
use crate::executor::StatementKind;
use super::compute_digest;
use super::impl_value_meta;
use super::ConstraintLevel;
use super::Hover;
use super::{Digest, Expr};
use databake::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sha2::{Digest as DigestTrait, Sha256};
// TODO: This should be its own type, similar to Program,
// but guaranteed to have an Expression as its final item.
// https://github.com/KittyCAD/modeling-app/issues/4015
type IfBlock = crate::ast::types::Program;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
#[serde(tag = "type")]
pub struct IfExpression {
pub start: usize,
pub end: usize,
pub cond: Box<Expr>,
pub then_val: Box<IfBlock>,
pub else_ifs: Vec<ElseIf>,
pub final_else: Box<IfBlock>,
pub digest: Option<Digest>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ElseIf {
pub start: usize,
pub end: usize,
pub cond: Expr,
pub then_val: Box<IfBlock>,
pub digest: Option<Digest>,
}
// Source code metadata
impl_value_meta!(IfExpression);
impl_value_meta!(ElseIf);
impl IfExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.cond.compute_digest());
hasher.update(slf.then_val.compute_digest());
for else_if in &mut slf.else_ifs {
hasher.update(else_if.compute_digest());
}
hasher.update(slf.final_else.compute_digest());
});
fn source_ranges(&self) -> Vec<SourceRange> {
vec![SourceRange::from(self)]
}
}
impl From<IfExpression> for Metadata {
fn from(value: IfExpression) -> Self {
Self {
source_range: value.into(),
}
}
}
impl From<ElseIf> for Metadata {
fn from(value: ElseIf) -> Self {
Self {
source_range: value.into(),
}
}
}
impl From<&IfExpression> for Metadata {
fn from(value: &IfExpression) -> Self {
Self {
source_range: value.into(),
}
}
}
impl From<&ElseIf> for Metadata {
fn from(value: &ElseIf) -> Self {
Self {
source_range: value.into(),
}
}
}
impl ElseIf {
compute_digest!(|slf, hasher| {
hasher.update(slf.cond.compute_digest());
hasher.update(slf.then_val.compute_digest());
});
#[allow(dead_code)]
fn source_ranges(&self) -> Vec<SourceRange> {
vec![SourceRange([self.start, self.end])]
}
}
// Execution
impl IfExpression {
#[async_recursion::async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
// Check the `if` branch.
let cond = ctx
.execute_expr(&self.cond, exec_state, &Metadata::from(self), StatementKind::Expression)
.await?
.get_bool()?;
if cond {
let block_result = ctx.inner_execute(&self.then_val, exec_state, BodyType::Block).await?;
// Block must end in an expression, so this has to be Some.
// Enforced by the parser.
// See https://github.com/KittyCAD/modeling-app/issues/4015
return Ok(block_result.unwrap());
}
// Check any `else if` branches.
for else_if in &self.else_ifs {
let cond = ctx
.execute_expr(
&else_if.cond,
exec_state,
&Metadata::from(self),
StatementKind::Expression,
)
.await?
.get_bool()?;
if cond {
let block_result = ctx
.inner_execute(&else_if.then_val, exec_state, BodyType::Block)
.await?;
// Block must end in an expression, so this has to be Some.
// Enforced by the parser.
// See https://github.com/KittyCAD/modeling-app/issues/4015
return Ok(block_result.unwrap());
}
}
// Run the final `else` branch.
ctx.inner_execute(&self.final_else, exec_state, BodyType::Block)
.await
.map(|expr| expr.unwrap())
}
}
// IDE support and refactors
impl IfExpression {
pub fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> {
self.cond
.get_hover_value_for_position(pos, code)
.or_else(|| self.then_val.get_hover_value_for_position(pos, code))
.or_else(|| {
self.else_ifs
.iter()
.find_map(|else_if| else_if.get_hover_value_for_position(pos, code))
})
.or_else(|| self.final_else.get_hover_value_for_position(pos, code))
}
/// Rename all identifiers that have the old name to the new given name.
pub fn rename_identifiers(&mut self, old_name: &str, new_name: &str) {
self.cond.rename_identifiers(old_name, new_name);
self.then_val.rename_identifiers(old_name, new_name);
for else_if in &mut self.else_ifs {
else_if.rename_identifiers(old_name, new_name);
}
self.final_else.rename_identifiers(old_name, new_name);
}
/// Get the constraint level.
pub fn get_constraint_level(&self) -> ConstraintLevel {
ConstraintLevel::Full {
source_ranges: self.source_ranges(),
}
}
pub fn replace_value(&mut self, source_range: SourceRange, new_value: Expr) {
self.cond.replace_value(source_range, new_value.clone());
for else_if in &mut self.else_ifs {
else_if.cond.replace_value(source_range, new_value.clone());
}
}
}
impl ElseIf {
fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> {
self.cond
.get_hover_value_for_position(pos, code)
.or_else(|| self.then_val.get_hover_value_for_position(pos, code))
}
/// Rename all identifiers that have the old name to the new given name.
fn rename_identifiers(&mut self, old_name: &str, new_name: &str) {
self.cond.rename_identifiers(old_name, new_name);
self.then_val.rename_identifiers(old_name, new_name);
}
}
// Linting
impl IfExpression {}
impl ElseIf {}

View File

@ -81,6 +81,8 @@ impl StdLibFnArg {
} else if self.type_ == "TagIdentifier" && self.required {
// TODO: actually use the ast to populate this.
return Ok(Some((index, format!("${{{}:{}}}", index, "myTag"))));
} else if self.type_ == "[KclValue]" && self.required {
return Ok(Some((index, format!("${{{}:{}}}", index, "[0..9]"))));
}
get_autocomplete_snippet_from_schema(&self.schema.schema.clone().into(), index)
}
@ -903,6 +905,13 @@ mod tests {
);
}
#[test]
fn get_autocomplete_snippet_map() {
let map_fn: Box<dyn StdLibFn> = Box::new(crate::std::array::Map);
let snippet = map_fn.to_autocomplete_snippet().unwrap();
assert_eq!(snippet, r#"map(${0:[0..9]})${}"#);
}
#[test]
fn get_autocomplete_snippet_pattern_linear_2d() {
let pattern_fn: Box<dyn StdLibFn> = Box::new(crate::std::patterns::PatternLinear2D);
@ -916,4 +925,18 @@ mod tests {
}, ${4:%})${}"#
);
}
// We want to test the snippets we compile at lsp start.
#[test]
fn get_all_stdlib_autocomplete_snippets() {
let stdlib = crate::std::StdLib::new();
crate::lsp::kcl::get_completions_from_stdlib(&stdlib).unwrap();
}
// We want to test the signatures we compile at lsp start.
#[test]
fn get_all_stdlib_signatures() {
let stdlib = crate::std::StdLib::new();
crate::lsp::kcl::get_signatures_from_stdlib(&stdlib).unwrap();
}
}

View File

@ -776,6 +776,25 @@ impl From<KclValue> for Vec<SourceRange> {
}
}
impl From<&KclValue> for Vec<SourceRange> {
fn from(item: &KclValue) -> Self {
match item {
KclValue::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(),
KclValue::TagDeclarator(ref t) => vec![t.into()],
KclValue::TagIdentifier(t) => t.meta.iter().map(|m| m.source_range).collect(),
KclValue::Solid(e) => e.meta.iter().map(|m| m.source_range).collect(),
KclValue::Solids { value } => value
.iter()
.flat_map(|eg| eg.meta.iter().map(|m| m.source_range))
.collect(),
KclValue::ImportedGeometry(i) => i.meta.iter().map(|m| m.source_range).collect(),
KclValue::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
KclValue::Plane(p) => p.meta.iter().map(|m| m.source_range).collect(),
KclValue::Face(f) => f.meta.iter().map(|m| m.source_range).collect(),
}
}
}
impl KclValue {
pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> {
if let KclValue::UserVal(user_val) = self {
@ -906,6 +925,23 @@ impl KclValue {
}
}
/// If this KCL value is a bool, retrieve it.
pub fn get_bool(&self) -> Result<bool, KclError> {
let Self::UserVal(uv) = self else {
return Err(KclError::Type(KclErrorDetails {
source_ranges: self.into(),
message: format!("Expected bool, found {}", self.human_friendly_type()),
}));
};
let JValue::Bool(b) = uv.value else {
return Err(KclError::Type(KclErrorDetails {
source_ranges: self.into(),
message: format!("Expected bool, found {}", human_friendly_type(&uv.value)),
}));
};
Ok(b)
}
/// If this memory item is a function, call it with the given arguments, return its val as Ok.
/// If it's not a function, return Err.
pub async fn call_fn(
@ -1613,6 +1649,22 @@ impl ExtrudeSurface {
}
}
/// The type of ExecutorContext being used
#[derive(PartialEq, Debug, Default, Clone)]
pub enum ContextType {
/// Live engine connection
#[default]
Live,
/// Completely mocked connection
/// Mock mode is only for the modeling app when they just want to mock engine calls and not
/// actually make them.
Mock,
/// Handled by some other interpreter/conversion system
MockCustomForwarded,
}
/// The executor context.
/// Cloning will return another handle to the same engine connection/session,
/// as this uses `Arc` under the hood.
@ -1622,9 +1674,7 @@ pub struct ExecutorContext {
pub fs: Arc<FileManager>,
pub stdlib: Arc<StdLib>,
pub settings: ExecutorSettings,
/// Mock mode is only for the modeling app when they just want to mock engine calls and not
/// actually make them.
pub is_mock: bool,
pub context_type: ContextType,
}
/// The executor settings.
@ -1734,10 +1784,14 @@ impl ExecutorContext {
fs: Arc::new(FileManager::new()),
stdlib: Arc::new(StdLib::new()),
settings,
is_mock: false,
context_type: ContextType::Live,
})
}
pub fn is_mock(&self) -> bool {
self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
}
/// For executing unit tests.
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_for_unit_test(units: UnitLength, engine_addr: Option<String>) -> Result<Self> {
@ -1844,20 +1898,22 @@ impl ExecutorContext {
program: &crate::ast::types::Program,
exec_state: &mut ExecState,
body_type: BodyType,
) -> Result<(), KclError> {
) -> Result<Option<KclValue>, KclError> {
let mut last_expr = None;
// Iterate over the body of the program.
for statement in &program.body {
match statement {
BodyItem::ExpressionStatement(expression_statement) => {
let metadata = Metadata::from(expression_statement);
// Discard return value.
self.execute_expr(
&expression_statement.expression,
exec_state,
&metadata,
StatementKind::Expression,
)
.await?;
last_expr = Some(
self.execute_expr(
&expression_statement.expression,
exec_state,
&metadata,
StatementKind::Expression,
)
.await?,
);
}
BodyItem::VariableDeclaration(variable_declaration) => {
for declaration in &variable_declaration.declarations {
@ -1875,6 +1931,7 @@ impl ExecutorContext {
.await?;
exec_state.memory.add(&var_name, memory_item, source_range)?;
}
last_expr = None;
}
BodyItem::ReturnStatement(return_statement) => {
let metadata = Metadata::from(return_statement);
@ -1887,6 +1944,7 @@ impl ExecutorContext {
)
.await?;
exec_state.memory.return_ = Some(value);
last_expr = None;
}
}
}
@ -1903,7 +1961,7 @@ impl ExecutorContext {
.await?;
}
Ok(())
Ok(last_expr)
}
pub async fn execute_expr<'a>(
@ -1960,6 +2018,7 @@ impl ExecutorContext {
Expr::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?,
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
};
Ok(item)
}
@ -2088,7 +2147,7 @@ pub(crate) async fn call_user_defined_function(
(result, fn_memory)
};
result.map(|()| fn_memory.return_)
result.map(|_| fn_memory.return_)
}
pub enum StatementKind<'a> {
@ -2114,7 +2173,7 @@ mod tests {
fs: Arc::new(crate::fs::FileManager::new()),
stdlib: Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: ContextType::Mock,
};
let exec_state = ctx.run(&program, None).await?;

View File

@ -10,11 +10,11 @@ use winnow::{
use crate::{
ast::types::{
ArrayExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, CommentStyle, Expr,
ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier, Literal, LiteralIdentifier,
LiteralValue, MemberExpression, MemberObject, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression,
ObjectProperty, Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement, TagDeclarator,
UnaryExpression, UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
ArrayExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, CommentStyle, ElseIf,
Expr, ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier, IfExpression, Literal,
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, NonCodeMeta, NonCodeNode, NonCodeValue,
ObjectExpression, ObjectProperty, Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement,
TagDeclarator, UnaryExpression, UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
},
errors::{KclError, KclErrorDetails},
executor::SourceRange,
@ -359,6 +359,7 @@ fn operand(i: TokenSlice) -> PResult<BinaryPart> {
Expr::BinaryExpression(x) => BinaryPart::BinaryExpression(x),
Expr::CallExpression(x) => BinaryPart::CallExpression(x),
Expr::MemberExpression(x) => BinaryPart::MemberExpression(x),
Expr::IfExpression(x) => BinaryPart::IfExpression(x),
};
Ok(expr)
})
@ -670,6 +671,119 @@ fn pipe_sub(i: TokenSlice) -> PResult<PipeSubstitution> {
.parse_next(i)
}
fn else_if(i: TokenSlice) -> PResult<ElseIf> {
let start = any
.try_map(|token: Token| {
if matches!(token.token_type, TokenType::Keyword) && token.value == "else" {
Ok(token.start)
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{} is not 'else'", token.value.as_str()),
}))
}
})
.context(expected("the 'else' keyword"))
.parse_next(i)?;
ignore_whitespace(i);
let _if = any
.try_map(|token: Token| {
if matches!(token.token_type, TokenType::Keyword) && token.value == "if" {
Ok(token.start)
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{} is not 'if'", token.value.as_str()),
}))
}
})
.context(expected("the 'if' keyword"))
.parse_next(i)?;
ignore_whitespace(i);
let cond = expression(i)?;
ignore_whitespace(i);
let _ = open_brace(i)?;
let then_val = program
.verify(|block| block.ends_with_expr())
.parse_next(i)
.map(Box::new)?;
ignore_whitespace(i);
let end = close_brace(i)?.end;
ignore_whitespace(i);
Ok(ElseIf {
start,
end,
cond,
then_val,
digest: Default::default(),
})
}
fn if_expr(i: TokenSlice) -> PResult<IfExpression> {
let start = any
.try_map(|token: Token| {
if matches!(token.token_type, TokenType::Keyword) && token.value == "if" {
Ok(token.start)
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{} is not 'if'", token.value.as_str()),
}))
}
})
.context(expected("the 'if' keyword"))
.parse_next(i)?;
let _ = whitespace(i)?;
let cond = expression(i).map(Box::new)?;
let _ = whitespace(i)?;
let _ = open_brace(i)?;
ignore_whitespace(i);
let then_val = program
.verify(|block| block.ends_with_expr())
.parse_next(i)
.map_err(|e| e.cut())
.map(Box::new)?;
ignore_whitespace(i);
let _ = close_brace(i)?;
ignore_whitespace(i);
let else_ifs = repeat(0.., else_if).parse_next(i)?;
ignore_whitespace(i);
let _ = any
.try_map(|token: Token| {
if matches!(token.token_type, TokenType::Keyword) && token.value == "else" {
Ok(token.start)
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{} is not 'else'", token.value.as_str()),
}))
}
})
.context(expected("the 'else' keyword"))
.parse_next(i)?;
ignore_whitespace(i);
let _ = open_brace(i)?;
ignore_whitespace(i);
let final_else = program
.verify(|block| block.ends_with_expr())
.parse_next(i)
.map_err(|e| e.cut())
.map(Box::new)?;
ignore_whitespace(i);
let end = close_brace(i)?.end;
Ok(IfExpression {
start,
end,
cond,
then_val,
else_ifs,
final_else,
digest: Default::default(),
})
}
// Looks like
// (arg0, arg1) => {
// const x = arg0 + arg1;
@ -1069,6 +1183,7 @@ fn expr_allowed_in_pipe_expr(i: TokenSlice) -> PResult<Expr> {
object.map(Box::new).map(Expr::ObjectExpression),
pipe_sub.map(Box::new).map(Expr::PipeSubstitution),
function_expression.map(Box::new).map(Expr::FunctionExpression),
if_expr.map(Box::new).map(Expr::IfExpression),
unnecessarily_bracketed,
))
.context(expected("a KCL expression (but not a pipe expression)"))
@ -3147,6 +3262,42 @@ e
let _arr = array_elem_by_elem(&mut sl).unwrap();
}
#[test]
fn basic_if_else() {
let some_program_string = "if true {
3
} else {
4
}";
let tokens = crate::token::lexer(some_program_string).unwrap();
let mut sl: &[Token] = &tokens;
let _res = if_expr(&mut sl).unwrap();
}
#[test]
fn basic_else_if() {
let some_program_string = "else if true {
4
}";
let tokens = crate::token::lexer(some_program_string).unwrap();
let mut sl: &[Token] = &tokens;
let _res = else_if(&mut sl).unwrap();
}
#[test]
fn basic_if_else_if() {
let some_program_string = "if true {
3
} else if true {
4
} else {
5
}";
let tokens = crate::token::lexer(some_program_string).unwrap();
let mut sl: &[Token] = &tokens;
let _res = if_expr(&mut sl).unwrap();
}
#[test]
fn test_keyword_ok_in_fn_args_return() {
let some_program_string = r#"fn thing = (param) => {
@ -3511,6 +3662,24 @@ const sketch001 = startSketchOn('XY')
const my14 = 4 ^ 2 - 3 ^ 2 * 2
"#
);
snapshot_test!(
bc,
r#"const x = if true {
3
} else {
4
}"#
);
snapshot_test!(
bd,
r#"const x = if true {
3
} else if func(radius) {
4
} else {
5
}"#
);
}
#[allow(unused)]

View File

@ -0,0 +1,112 @@
---
source: kcl/src/parser/parser_impl.rs
expression: actual
---
{
"start": 0,
"end": 74,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 74,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 74,
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"name": "x",
"digest": null
},
"init": {
"type": "IfExpression",
"type": "IfExpression",
"start": 10,
"end": 74,
"cond": {
"type": "Literal",
"type": "Literal",
"start": 13,
"end": 17,
"value": true,
"raw": "true",
"digest": null
},
"then_val": {
"start": 32,
"end": 42,
"body": [
{
"type": "ExpressionStatement",
"type": "ExpressionStatement",
"start": 32,
"end": 33,
"expression": {
"type": "Literal",
"type": "Literal",
"start": 32,
"end": 33,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"else_ifs": [],
"final_else": {
"start": 63,
"end": 73,
"body": [
{
"type": "ExpressionStatement",
"type": "ExpressionStatement",
"start": 63,
"end": 64,
"expression": {
"type": "Literal",
"type": "Literal",
"start": 63,
"end": 64,
"value": 4,
"raw": "4",
"digest": null
},
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
},
"digest": null
}
],
"kind": "const",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -0,0 +1,172 @@
---
source: kcl/src/parser/parser_impl.rs
expression: actual
---
{
"start": 0,
"end": 121,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 121,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 121,
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"name": "x",
"digest": null
},
"init": {
"type": "IfExpression",
"type": "IfExpression",
"start": 10,
"end": 121,
"cond": {
"type": "Literal",
"type": "Literal",
"start": 13,
"end": 17,
"value": true,
"raw": "true",
"digest": null
},
"then_val": {
"start": 32,
"end": 42,
"body": [
{
"type": "ExpressionStatement",
"type": "ExpressionStatement",
"start": 32,
"end": 33,
"expression": {
"type": "Literal",
"type": "Literal",
"start": 32,
"end": 33,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"else_ifs": [
{
"type": "ElseIf",
"start": 44,
"end": 90,
"cond": {
"type": "CallExpression",
"type": "CallExpression",
"start": 52,
"end": 64,
"callee": {
"type": "Identifier",
"start": 52,
"end": 56,
"name": "func",
"digest": null
},
"arguments": [
{
"type": "Identifier",
"type": "Identifier",
"start": 57,
"end": 63,
"name": "radius",
"digest": null
}
],
"optional": false,
"digest": null
},
"then_val": {
"start": 65,
"end": 89,
"body": [
{
"type": "ExpressionStatement",
"type": "ExpressionStatement",
"start": 79,
"end": 80,
"expression": {
"type": "Literal",
"type": "Literal",
"start": 79,
"end": 80,
"value": 4,
"raw": "4",
"digest": null
},
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
}
],
"final_else": {
"start": 110,
"end": 120,
"body": [
{
"type": "ExpressionStatement",
"type": "ExpressionStatement",
"start": 110,
"end": 111,
"expression": {
"type": "Literal",
"type": "Literal",
"start": 110,
"end": 111,
"value": 5,
"raw": "5",
"digest": null
},
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
},
"digest": null
}
],
"kind": "const",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -4,13 +4,14 @@ use anyhow::Result;
use kcmc::{websocket::OkWebSocketResponseData, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
use serde::de::DeserializeOwned;
use serde_json::Value as JValue;
use crate::{
ast::types::{parse_json_number_as_f64, TagDeclarator},
errors::{KclError, KclErrorDetails},
executor::{
ExecState, ExecutorContext, ExtrudeSurface, KclValue, Metadata, Sketch, SketchSet, SketchSurface, Solid,
SolidSet, SourceRange, TagIdentifier,
SolidSet, SourceRange, TagIdentifier, UserVal,
},
std::{shapes::SketchOrSurface, sketch::FaceTag, FnAsArg},
};
@ -43,7 +44,7 @@ impl Args {
fs: Arc::new(crate::fs::FileManager::new()),
stdlib: Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
is_mock: true,
context_type: crate::executor::ContextType::Mock,
},
})
}
@ -497,18 +498,6 @@ where
}
}
impl<'a> FromArgs<'a> for KclValue {
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
let Some(v) = args.args.get(i) else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Argument at index {i} was missing",),
source_ranges: vec![args.source_range],
}));
};
Ok(v.to_owned())
}
}
impl<'a, T> FromArgs<'a> for Option<T>
where
T: FromKclValue<'a> + Sized,
@ -587,6 +576,20 @@ impl<'a> FromKclValue<'a> for i64 {
}
}
impl<'a> FromKclValue<'a> for UserVal {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
arg.as_user_val().map(|x| x.to_owned())
}
}
impl<'a> FromKclValue<'a> for Vec<JValue> {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
arg.as_user_val()
.and_then(|uv| uv.value.as_array())
.map(ToOwned::to_owned)
}
}
impl<'a> FromKclValue<'a> for TagDeclarator {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
arg.get_tag_declarator().ok()
@ -599,6 +602,12 @@ impl<'a> FromKclValue<'a> for TagIdentifier {
}
}
impl<'a> FromKclValue<'a> for KclValue {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
Some(arg.clone())
}
}
macro_rules! impl_from_arg_via_json {
($typ:path) => {
impl<'a> FromKclValue<'a> for $typ {
@ -609,6 +618,15 @@ macro_rules! impl_from_arg_via_json {
};
}
impl<'a, T> FromKclValue<'a> for Vec<T>
where
T: serde::de::DeserializeOwned + FromKclValue<'a>,
{
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
from_user_val(arg)
}
}
macro_rules! impl_from_arg_for_array {
($n:literal) => {
impl<'a, T> FromKclValue<'a> for [T; $n]
@ -722,23 +740,3 @@ impl<'a> FromKclValue<'a> for SketchSurface {
}
}
}
impl<'a> FromKclValue<'a> for Vec<Sketch> {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
let KclValue::UserVal(uv) = arg else {
return None;
};
uv.get::<Vec<Sketch>>().map(|x| x.0)
}
}
impl<'a> FromKclValue<'a> for Vec<u64> {
fn from_mem_item(arg: &'a KclValue) -> Option<Self> {
let KclValue::UserVal(uv) = arg else {
return None;
};
uv.get::<Vec<u64>>().map(|x| x.0)
}
}

View File

@ -1,4 +1,5 @@
use derive_docs::stdlib;
use serde_json::Value as JValue;
use super::{args::FromArgs, Args, FnAsArg};
use crate::{
@ -7,8 +8,96 @@ use crate::{
function_param::FunctionParam,
};
/// Apply a function to each element of an array.
pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (array, f): (Vec<JValue>, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?;
let array: Vec<KclValue> = array
.into_iter()
.map(|jval| {
KclValue::UserVal(UserVal {
value: jval,
meta: vec![args.source_range.into()],
})
})
.collect();
let map_fn = FunctionParam {
inner: f.func,
fn_expr: f.expr,
meta: vec![args.source_range.into()],
ctx: args.ctx.clone(),
memory: *f.memory,
};
let new_array = inner_map(array, map_fn, exec_state, &args).await?;
let uv = UserVal::new(vec![args.source_range.into()], new_array);
Ok(KclValue::UserVal(uv))
}
/// Apply a function to every element of a list.
///
/// Given a list like `[a, b, c]`, and a function like `f`, returns
/// `[f(a), f(b), f(c)]`
/// ```no_run
/// const r = 10 // radius
/// fn drawCircle = (id) => {
/// return startSketchOn("XY")
/// |> circle({ center: [id * 2 * r, 0], radius: r}, %)
/// }
///
/// // Call `drawCircle`, passing in each element of the array.
/// // The outputs from each `drawCircle` form a new array,
/// // which is the return value from `map`.
/// const circles = map(
/// [1..3],
/// drawCircle
/// )
/// ```
/// ```no_run
/// const r = 10 // radius
/// // Call `map`, using an anonymous function instead of a named one.
/// const circles = map(
/// [1..3],
/// (id) => {
/// return startSketchOn("XY")
/// |> circle({ center: [id * 2 * r, 0], radius: r}, %)
/// }
/// )
/// ```
#[stdlib {
name = "map",
}]
async fn inner_map<'a>(
array: Vec<KclValue>,
map_fn: FunctionParam<'a>,
exec_state: &mut ExecState,
args: &'a Args,
) -> Result<Vec<KclValue>, KclError> {
let mut new_array = Vec::with_capacity(array.len());
for elem in array {
let new_elem = call_map_closure(elem, &map_fn, args.source_range, exec_state).await?;
new_array.push(new_elem);
}
Ok(new_array)
}
async fn call_map_closure<'a>(
input: KclValue,
map_fn: &FunctionParam<'a>,
source_range: SourceRange,
exec_state: &mut ExecState,
) -> Result<KclValue, KclError> {
let output = map_fn.call(exec_state, vec![input]).await?;
let source_ranges = vec![source_range];
let output = output.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: "Map function must return a value".to_string(),
source_ranges,
})
})?;
Ok(output)
}
/// For each item in an array, update a value.
pub async fn array_reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (array, start, f): (Vec<u64>, Sketch, FnAsArg<'_>) = FromArgs::from_args(&args, 0)?;
let reduce_fn = FunctionParam {
inner: f.func,
@ -17,7 +106,7 @@ pub async fn array_reduce(exec_state: &mut ExecState, args: Args) -> Result<KclV
ctx: args.ctx.clone(),
memory: *f.memory,
};
inner_array_reduce(array, start, reduce_fn, exec_state, &args)
inner_reduce(array, start, reduce_fn, exec_state, &args)
.await
.map(|sg| KclValue::UserVal(UserVal::new(sg.meta.clone(), sg)))
}
@ -28,7 +117,7 @@ pub async fn array_reduce(exec_state: &mut ExecState, args: Args) -> Result<KclV
/// fn decagon = (radius) => {
/// let step = (1/10) * tau()
/// let sketch001 = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
/// return arrayReduce([1..10], sketch001, (i, sg) => {
/// return reduce([1..10], sketch001, (i, sg) => {
/// let x = cos(step * i) * radius
/// let y = sin(step * i) * radius
/// return lineTo([x, y], sg)
@ -37,9 +126,9 @@ pub async fn array_reduce(exec_state: &mut ExecState, args: Args) -> Result<KclV
/// decagon(5.0) |> close(%)
/// ```
#[stdlib {
name = "arrayReduce",
name = "reduce",
}]
async fn inner_array_reduce<'a>(
async fn inner_reduce<'a>(
array: Vec<u64>,
start: Sketch,
reduce_fn: FunctionParam<'a>,

View File

@ -247,7 +247,7 @@ pub(crate) async fn do_post_extrude(sketch: Sketch, length: f64, args: Args) ->
Some(extrude_surface)
}
}
} else if args.ctx.is_mock {
} else if args.ctx.is_mock() {
// Only pre-populate the extrude surface if we are in mock mode.
let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane {
@ -296,7 +296,7 @@ fn analyze_faces(args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces {
sides: HashMap::with_capacity(face_infos.len()),
..Default::default()
};
if args.ctx.is_mock {
if args.ctx.is_mock() {
// Create fake IDs for start and end caps, to make extrudes mock-execute safe
faces.start_cap_id = Some(Uuid::new_v4());
faces.end_cap_id = Some(Uuid::new_v4());

View File

@ -228,7 +228,7 @@ pub async fn get_opposite_edge(exec_state: &mut ExecState, args: Args) -> Result
name = "getOppositeEdge",
}]
async fn inner_get_opposite_edge(tag: TagIdentifier, exec_state: &mut ExecState, args: Args) -> Result<Uuid, KclError> {
if args.ctx.is_mock {
if args.ctx.is_mock() {
return Ok(Uuid::new_v4());
}
let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
@ -309,7 +309,7 @@ async fn inner_get_next_adjacent_edge(
exec_state: &mut ExecState,
args: Args,
) -> Result<Uuid, KclError> {
if args.ctx.is_mock {
if args.ctx.is_mock() {
return Ok(Uuid::new_v4());
}
let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;
@ -398,7 +398,7 @@ async fn inner_get_previous_adjacent_edge(
exec_state: &mut ExecState,
args: Args,
) -> Result<Uuid, KclError> {
if args.ctx.is_mock {
if args.ctx.is_mock() {
return Ok(Uuid::new_v4());
}
let face_id = args.get_adjacent_face_to_tag(exec_state, &tag, false).await?;

View File

@ -284,7 +284,7 @@ async fn inner_import(
}
}
if args.ctx.is_mock {
if args.ctx.is_mock() {
return Ok(ImportedGeometry {
id: uuid::Uuid::new_v4(),
value: import_files.iter().map(|f| f.path.to_string()).collect(),

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