Compare commits

...

22 Commits

Author SHA1 Message Date
0255fde5fe Release KCL v0.2.22 (#4187) 2024-10-17 09:23:08 -07:00
ebade29ed0 Bump image from 0.25.2 to 0.25.3 in /src/wasm-lib (#4180)
Bumps [image](https://github.com/image-rs/image) from 0.25.2 to 0.25.3.
- [Changelog](https://github.com/image-rs/image/blob/main/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.25.2...v0.25.3)

---
updated-dependencies:
- dependency-name: image
  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-10-16 23:35:55 -07:00
582d37e51b Bump uuid from 1.10.0 to 1.11.0 in /src/wasm-lib (#4179)
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.10.0...1.11.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-16 23:35:47 -07:00
4ef9429842 Bump proc-macro2 from 1.0.86 to 1.0.88 in /src/wasm-lib (#4178)
Bumps [proc-macro2](https://github.com/dtolnay/proc-macro2) from 1.0.86 to 1.0.88.
- [Release notes](https://github.com/dtolnay/proc-macro2/releases)
- [Commits](https://github.com/dtolnay/proc-macro2/compare/1.0.86...1.0.88)

---
updated-dependencies:
- dependency-name: proc-macro2
  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-10-16 23:35:33 -07:00
0577b6a984 internal: KCL modules, part 1 (#4149)
Addresses #4080. (Not ready to close it yet.)

# Important

Requires a fix for #4147 before it can work in ZMA.

# Overview

```kcl
// numbers.kcl
export fn inc = (x) => {
  return x + 1
}
```

```kcl
import inc from "numbers.kcl"

answer = inc(41)
```

This also implements multiple imports with optional renaming.

```kcl
import inc, dec from "numbers.kcl"
import identity as id, length as len from "utils.kcl"
```

Note: Imported files _must_ be in the same directory.

Things for a follow-up PR:

- #4147. Currently, we cannot read files in WebAssembly, i.e. ZMA.
- Docs
- Should be an error to `import` anywhere besides the top level. Needs parser restructuring to track the context of a "function body".
- Should be an error to have `export` anywhere besides the top level. It has no effect, but we should tell people it's not valid instead of silently ignoring it.
- Error message for cycle detection is funky because the Rust side doesn't actually know the name of the first file. Message will say "b -> a -> b" instead of "a -> b -> a" when "a" is the top-level file.
- Cache imported files so that they don't need to be re-parsed and re-executed.
2024-10-16 21:48:33 -07:00
7d44de0c12 KCL refactor: Move execution into its own file (#4174)
types.rs is really, really big. I've moved executing AST nodes into
their own file.
2024-10-16 14:33:03 -07:00
f7d5313588 CI: Remove rust-cache from static analysis (#4175)
* CI: Remove rust-cache from static analysis

* 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: ubuntu-latest)

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

* Look at this (photo)Graph *in the voice of Nickelback*

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-16 14:31:42 -07:00
bd4783e885 Bump web-sys from 0.3.70 to 0.3.72 in /src/wasm-lib (#4153)
Bumps [web-sys](https://github.com/rustwasm/wasm-bindgen) from 0.3.70 to 0.3.72.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: web-sys
  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-10-16 10:12:37 -07:00
8794696b26 Bump pyo3 from 0.22.4 to 0.22.5 in /src/wasm-lib (#4170)
Bumps [pyo3](https://github.com/pyo3/pyo3) from 0.22.4 to 0.22.5.
- [Release notes](https://github.com/pyo3/pyo3/releases)
- [Changelog](https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pyo3/pyo3/compare/v0.22.4...v0.22.5)

---
updated-dependencies:
- dependency-name: pyo3
  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-10-16 10:11:17 -07:00
1c2e415c70 Permitting whitespace before colon (#4171)
Fixes #4150 by ignoring whitespace before a colon just like how whitespace after a colon is already ignored.
Added snapshot parser::parser_impl::snapshot_tests::bh for a kcl snippet that has whitespace before a colon.

Co-authored-by: 49fl <ircsurfer33@gmail.com>
2024-10-16 10:00:52 -07:00
248ef8ebb3 Implement dynamic ranges (#4151)
Closes #4021

Allows array ranges (e.g., `[0..10]`) to take expression instead of just numeric literals as their start and end values. Both expressions are required (we don't support `[0..]`, etc.).

I've created a new kind of expression in the AST. The alternative was to represent the internals of an array as some kind of pattern which could initially be fully explicit or ranges. I figured the chosen version was simpler and easier to extend to open ranges, whereas the latter would be easier to extend to mixed ranges or other patterns. I chose simpler, it'll be easy enough to refactor if necessary.

Parsing is tested implicitly by the tests of execution and unparsing.

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
2024-10-16 06:58:04 -07:00
fbac9935fe Bugfix: double map (#4169)
Previously, map was wrapping KCL values in a JSON object unnecessarily.  The new `double_map` test would emit this error:

```
Syntax(KclErrorDetails {
 source_ranges: [SourceRange([31, 32])],
 message: "Invalid number: {\"type\":\"UserVal\",\"value\":1.0,\"__meta\":[{\"sourceRange\":[31,36]}]}"
})
```

In other words, the second `map` statement is being passed an array of JSON STRINGS, not an array of numbers.
The strings contain JSON stringified representations of user values which are numbers.

Bug is now fixed.
2024-10-15 15:58:04 -07:00
b4c171a347 Bump pyo3 from 0.22.3 to 0.22.4 in /src/wasm-lib (#4154)
Bumps [pyo3](https://github.com/pyo3/pyo3) from 0.22.3 to 0.22.4.
- [Release notes](https://github.com/pyo3/pyo3/releases)
- [Changelog](https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pyo3/pyo3/compare/v0.22.3...v0.22.4)

---
updated-dependencies:
- dependency-name: pyo3
  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-10-15 15:10:59 -07:00
0811d9fa4e Rename things to be clearer about expression vs. value (#4163) 2024-10-15 17:33:20 -04:00
1efc2b9762 KCL: Pattern repetitions now pattern instances (#4162)
Josh Gomez requests pattern calculations take the total number of instances,
not the number of extra repetitions to do. This is how we designed the
patternTransform API, but we didn't do that for patternLinear/Circular.
2024-10-15 13:25:03 -07:00
d361bda180 Add collapsible element to updater notification to show changelog, open if there breaking changes (#4051)
* Add collapsible element to updater toast notification showing release notes

* Temp create release artifacts to test updater

* Fix tsc error

* Fix some styling, make release notes not appear if no notes are present

* Add component tests

* Remove test release builds

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2024-10-15 07:30:00 -04:00
1d3ade114f KCL: Comparison operators (#4025)
Add `==, !=, >, >=, <, <=` to the language parser, executor and tests.

Closes https://github.com/KittyCAD/modeling-app/issues/4020

Currently these comparison operators are associative, allowing users to chain them, e.g. (x <= y <= z). This should not be allowed, will do in a follow-up. See https://github.com/KittyCAD/modeling-app/issues/4155
2024-10-14 10:46:39 -07:00
max
3382b66075 Add support for opposite and adjacent edges for fillets (#4103)
* add support for opposite and adjacent edges

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

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

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

* update playwright

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

* update unit tests

* enable button state checker for selections

* typos

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

* trigger ci

* fix typo

* remove leave(node)

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

* typo

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

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

* pull getEdgeTagCall  into a utility function

* mask model-state-indicator

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

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

* Rerun CI

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

* Rerun CI

* screenshot fix

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2024-10-11 23:25:51 +02:00
5e8b5c254d Update machine-api spec (#4121)
* YOYO NEW API SPEC!

* New machine-api types

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
2024-10-11 12:49:32 -07:00
b99b2d9a96 Bump js-sys from 0.3.71 to 0.3.72 in /src/wasm-lib (#4139)
Bumps [js-sys](https://github.com/rustwasm/wasm-bindgen) from 0.3.71 to 0.3.72.
- [Release notes](https://github.com/rustwasm/wasm-bindgen/releases)
- [Changelog](https://github.com/rustwasm/wasm-bindgen/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustwasm/wasm-bindgen/commits)

---
updated-dependencies:
- dependency-name: js-sys
  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-10-11 12:09:48 -07:00
81041661c7 bump test server 2024-10-11 14:31:21 -04:00
9d99b5be7f bump kcl (#4144) 2024-10-11 14:26:46 -04:00
133 changed files with 4483 additions and 1270 deletions

View File

@ -37,10 +37,6 @@ jobs:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
yarn-tsc:
@ -70,10 +66,6 @@ jobs:
node-version-file: '.nvmrc'
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn lint
python-codespell:
@ -101,11 +93,6 @@ jobs:
cache: 'yarn'
- run: yarn install
- uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- run: yarn build:wasm
- run: yarn simpleserver:bg

File diff suppressed because one or more lines are too long

View File

@ -36,7 +36,7 @@ exampleSketch = startSketchOn('XZ')
|> close(%)
|> patternCircular2d({
center: [0, 0],
repetitions: 12,
instances: 13,
arcDegrees: 360,
rotateDuplicates: true
}, %)

View File

@ -35,7 +35,7 @@ example = extrude(-5, exampleSketch)
|> patternCircular3d({
axis: [1, -1, 0],
center: [10, -20, 0],
repetitions: 10,
instances: 11,
arcDegrees: 360,
rotateDuplicates: true
}, %)

View File

@ -32,7 +32,7 @@ exampleSketch = startSketchOn('XZ')
|> circle({ center: [0, 0], radius: 1 }, %)
|> patternLinear2d({
axis: [1, 0],
repetitions: 6,
instances: 7,
distance: 4
}, %)

View File

@ -38,7 +38,7 @@ exampleSketch = startSketchOn('XZ')
example = extrude(1, exampleSketch)
|> patternLinear3d({
axis: [1, 0, 1],
repetitions: 6,
instances: 7,
distance: 6
}, %)
```

View File

@ -32,7 +32,7 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
fn decagon = (radius) => {
step = 1 / 10 * tau()
sketch001 = startSketchAt([cos(0) * radius, sin(0) * radius])
return reduce([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], sketch001, (i, sg) => {
return reduce([1..10], sketch001, (i, sg) => {
x = cos(step * i) * radius
y = sin(step * i) * radius
return lineTo([x, y], sg)

File diff suppressed because it is too large Load Diff

View File

@ -82,6 +82,78 @@ Raise a number to a power.
----
Are two numbers equal?
**enum:** `==`
----
Are two numbers not equal?
**enum:** `!=`
----
Is left greater than right
**enum:** `>`
----
Is left greater than or equal to right
**enum:** `>=`
----
Is left less than right
**enum:** `<`
----
Is left less than or equal to right
**enum:** `<=`
----

View File

@ -18,6 +18,27 @@ layout: manual
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ImportStatement`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `items` |`[` [`ImportItem`](/docs/kcl/types/ImportItem) `]`| | No |
| `path` |`string`| | No |
| `raw_path` |`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 |
@ -45,6 +66,7 @@ layout: manual
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | 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 |

View File

@ -16,7 +16,7 @@ Data for a circular pattern on a 2D sketch.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `center` |`[number, number]`| The center about which to make the pattern. This is a 2D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |

View File

@ -16,7 +16,7 @@ Data for a circular pattern on a 3D model.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `axis` |`[number, number, number]`| The axis around which to make the pattern. This is a 3D vector. | No |
| `center` |`[number, number, number]`| The center about which to make the pattern. This is a 3D vector. | No |
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |

View File

@ -197,6 +197,27 @@ An expression can be evaluated to yield a single KCL value.
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayRangeExpression`| | No |
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
| `startElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endInclusive` |`boolean`| Is the `end_element` included in the range? | 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 |

View File

@ -0,0 +1,24 @@
---
title: "ImportItem"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `name` |[`Identifier`](/docs/kcl/types/Identifier)| Name of the item to import. | No |
| `alias` |[`Identifier`](/docs/kcl/types/Identifier)| Rename the item using an identifier after "as". | 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 |

View File

@ -0,0 +1,16 @@
---
title: "ItemVisibility"
excerpt: ""
layout: manual
---
**enum:** `default`, `export`

View File

@ -16,7 +16,7 @@ Data for a linear pattern on a 2D sketch.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No |
| `axis` |`[number, number]`| The axis of the pattern. This is a 2D vector. | No |

View File

@ -16,7 +16,7 @@ Data for a linear pattern on a 3D model.
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `repetitions` |[`Uint`](/docs/kcl/types/Uint)| The number of repetitions. Must be greater than 0. This excludes the original entity. For example, if `repetitions` is 1, the original entity will be copied once. | No |
| `instances` |[`Uint`](/docs/kcl/types/Uint)| The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect. | No |
| `distance` |`number`| The distance between each repetition. This can also be referred to as spacing. | No |
| `axis` |`[number, number, number]`| The axis of the pattern. | No |

View File

@ -669,6 +669,7 @@ test.describe(
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
// exit sketch
@ -686,6 +687,7 @@ test.describe(
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: [page.getByTestId('model-state-indicator')],
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 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: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 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: 50 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: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

2
interface.d.ts vendored
View File

@ -73,7 +73,7 @@ export interface IElectronAPI {
callback: (value: { version: string }) => void
) => Electron.IpcRenderer
onUpdateDownloaded: (
callback: (value: string) => void
callback: (value: { version: string; releaseNotes: string }) => void
) => Electron.IpcRenderer
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
appRestart: () => void

View File

@ -425,6 +425,34 @@
]
}
},
"/metrics": {
"get": {
"operationId": "get_metrics",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"title": "String",
"type": "string"
}
}
},
"description": "successful operation"
},
"4XX": {
"$ref": "#/components/responses/Error"
},
"5XX": {
"$ref": "#/components/responses/Error"
}
},
"summary": "List available machines and their statuses",
"tags": [
"hidden"
]
}
},
"/ping": {
"get": {
"operationId": "ping",
@ -492,6 +520,13 @@
}
},
"tags": [
{
"description": "Hidden API endpoints that should not show up in the docs.",
"externalDocs": {
"url": "https://docs.zoo.dev/api/machines"
},
"name": "hidden"
},
{
"description": "Utilities for making parts and discovering machines.",
"externalDocs": {

View File

@ -0,0 +1,153 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { vi } from 'vitest'
import { ToastUpdate } from './ToastUpdate'
describe('ToastUpdate tests', () => {
const testData = {
version: '0.255.255',
files: [
{
url: 'Zoo Modeling App-0.255.255-x64-mac.zip',
sha512:
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
size: 141277345,
},
{
url: 'Zoo Modeling App-0.255.255-arm64-mac.zip',
sha512:
'b+ugdg7A4LhYYJaFkPRxh1RvmGGMlPJJj7inkLg9PwRtCnR9ePMlktj2VRciXF1iLh59XW4bLc4dK1dFQHMULA==',
size: 135278259,
},
{
url: 'Zoo Modeling App-0.255.255-x64-mac.dmg',
sha512:
'gCUqww05yj8OYwPiTq6bo5GbkpngSbXGtenmDD7+kUm0UyVK8WD3dMAfQJtGNG5HY23aHCHe9myE2W4mbZGmiQ==',
size: 146004232,
},
{
url: 'Zoo Modeling App-0.255.255-arm64-mac.dmg',
sha512:
'ND871ayf81F1ZT+iWVLYTc2jdf/Py6KThuxX2QFWz14ebmIbJPL07lNtxQOexOFiuk0MwRhlCy1RzOSG1b9bmw==',
size: 140021522,
},
],
path: 'Zoo Modeling App-0.255.255-x64-mac.zip',
sha512:
'VJb0qlrqNr+rVx3QLATz+B28dtHw3osQb5/+UUmQUIMuF9t0i8dTKOVL/2lyJSmLJVw2/SGDB4Ud6VlTPJ6oFw==',
releaseNotes:
'## Some markdown release notes\n\n- This is a list item\n- This is another list item\n\n```javascript\nconsole.log("Hello, world!")\n```\n',
releaseDate: '2024-10-09T11:57:59.133Z',
} as const
test('Happy path: renders the toast with good data', () => {
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={testData.releaseNotes}
/>
)
// Locators and other constants
const versionText = screen.getByTestId('update-version')
const restartButton = screen.getByRole('button', { name: /restart/i })
const dismissButton = screen.getByRole('button', { name: /got it/i })
const releaseNotes = screen.getByTestId('release-notes')
expect(versionText).toBeVisible()
expect(versionText).toHaveTextContent(testData.version)
expect(restartButton).toBeEnabled()
fireEvent.click(restartButton)
expect(onRestart.mock.calls).toHaveLength(1)
expect(dismissButton).toBeEnabled()
fireEvent.click(dismissButton)
expect(onDismiss.mock.calls).toHaveLength(1)
// I cannot for the life of me seem to get @testing-library/react
// to properly handle click events or visibility checks on the details element.
// So I'm only checking that the content is in the document.
expect(releaseNotes).toBeInTheDocument()
expect(releaseNotes).toHaveTextContent('Release notes')
const releaseNotesListItems = screen.getAllByRole('listitem')
expect(releaseNotesListItems.map((el) => el.textContent)).toEqual([
'This is a list item',
'This is another list item',
])
})
test('Happy path: renders the breaking changes notice', () => {
const releaseNotesWithBreakingChanges = `
## Some markdown release notes
- This is a list item
- This is another list item with a breaking change
- This is a list item
`
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={releaseNotesWithBreakingChanges}
/>
)
// Locators and other constants
const releaseNotes = screen.getByText('Release notes', {
selector: 'summary',
})
const listItemContents = screen
.getAllByRole('listitem')
.map((el) => el.textContent)
// I cannot for the life of me seem to get @testing-library/react
// to properly handle click events or visibility checks on the details element.
// So I'm only checking that the content is in the document.
expect(releaseNotes).toBeInTheDocument()
expect(listItemContents).toEqual([
'This is a list item',
'This is another list item with a breaking change',
'This is a list item',
])
})
test('Missing release notes: renders the toast without release notes', () => {
const onRestart = vi.fn()
const onDismiss = vi.fn()
render(
<ToastUpdate
onRestart={onRestart}
onDismiss={onDismiss}
version={testData.version}
releaseNotes={''}
/>
)
// Locators and other constants
const versionText = screen.getByTestId('update-version')
const restartButton = screen.getByRole('button', { name: /restart/i })
const dismissButton = screen.getByRole('button', { name: /got it/i })
const releaseNotes = screen.queryByText(/release notes/i, {
selector: 'details > summary',
})
const releaseNotesListItem = screen.queryByRole('listitem', {
name: /this is a list item/i,
})
expect(versionText).toBeVisible()
expect(versionText).toHaveTextContent(testData.version)
expect(releaseNotes).not.toBeInTheDocument()
expect(releaseNotesListItem).not.toBeInTheDocument()
expect(restartButton).toBeEnabled()
expect(dismissButton).toBeEnabled()
})
})

View File

@ -1,14 +1,23 @@
import toast from 'react-hot-toast'
import { ActionButton } from './ActionButton'
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { Marked } from '@ts-stack/markdown'
export function ToastUpdate({
version,
releaseNotes,
onRestart,
onDismiss,
}: {
version: string
releaseNotes?: string
onRestart: () => void
onDismiss: () => void
}) {
const containsBreakingChanges = releaseNotes
?.toLocaleLowerCase()
.includes('breaking')
return (
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
<div className="max-w-3xl min-w-[35rem] p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
@ -19,7 +28,7 @@ export function ToastUpdate({
>
v{version}
</span>
<span className="ml-4 text-md text-bold">
<p className="ml-4 text-md text-bold">
A new update has downloaded and will be available next time you
start the app. You can view the release notes{' '}
<a
@ -32,15 +41,39 @@ export function ToastUpdate({
>
here on GitHub.
</a>
</span>
</p>
</div>
{releaseNotes && (
<details
className="my-4 border border-chalkboard-30 dark:border-chalkboard-60 rounded"
open={containsBreakingChanges}
data-testid="release-notes"
>
<summary className="p-2 select-none cursor-pointer">
Release notes
{containsBreakingChanges && (
<strong className="text-destroy-50"> (Breaking changes)</strong>
)}
</summary>
<div
className="parsed-markdown py-2 px-4 mt-2 border-t border-chalkboard-30 dark:border-chalkboard-60 max-h-60 overflow-y-auto"
dangerouslySetInnerHTML={{
__html: Marked.parse(releaseNotes, {
gfm: true,
breaks: true,
sanitize: true,
}),
}}
></div>
</details>
)}
<div className="flex justify-between gap-8">
<ActionButton
Element="button"
iconStart={{
icon: 'arrowRotateRight',
}}
name="Restart app now"
name="restart"
onClick={onRestart}
>
Restart app now
@ -50,9 +83,10 @@ export function ToastUpdate({
iconStart={{
icon: 'checkmark',
}}
name="Got it"
name="dismiss"
onClick={() => {
toast.dismiss()
onDismiss()
}}
>
Got it

View File

@ -1,6 +1,9 @@
import { styleTags, tags as t } from '@lezer/highlight'
export const kclHighlight = styleTags({
'import export': t.moduleKeyword,
ImportItemAs: t.definitionKeyword,
ImportFrom: t.moduleKeyword,
'fn var let const': t.definitionKeyword,
'if else': t.controlKeyword,
return: t.controlKeyword,

View File

@ -15,8 +15,9 @@
}
statement[@isGroup=Statement] {
FunctionDeclaration { kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
ImportStatement { kw<"import"> ImportItems ImportFrom String } |
FunctionDeclaration { kw<"export">? kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
VariableDeclaration { kw<"export">? (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
ReturnStatement { kw<"return"> expression } |
ExpressionStatement { expression }
}
@ -25,6 +26,9 @@ ParamList { "(" commaSep<Parameter { VariableDefinition "?"? (":" type)? }> ")"
Body { "{" statement* "}" }
ImportItems { commaSep1NoTrailingComma<ImportItem> }
ImportItem { identifier (ImportItemAs identifier)? }
expression[@isGroup=Expression] {
String |
Number |
@ -74,6 +78,8 @@ kw<term> { @specialize[@name={term}]<identifier, term> }
commaSep<term> { (term ("," term)*)? ","? }
commaSep1NoTrailingComma<term> { term ("," term)* }
@tokens {
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
@ -106,6 +112,9 @@ commaSep<term> { (term ("," term)*)? ","? }
Shebang { "#!" ![\n]* }
ImportItemAs { "as" }
ImportFrom { "from" }
"(" ")"
"{" "}"
"[" "]"

View File

@ -293,6 +293,24 @@ code {
which lets you use them with @apply in your CSS, and get
autocomplete in classNames in your JSX.
*/
.parsed-markdown ul,
.parsed-markdown ol {
@apply list-outside pl-4 lg:pl-8 my-2;
}
.parsed-markdown ul li {
@apply list-disc;
}
.parsed-markdown li p {
@apply inline;
}
.parsed-markdown code {
@apply px-1 py-0.5 rounded-sm;
@apply bg-chalkboard-20 text-chalkboard-80;
@apply dark:bg-chalkboard-80 dark:text-chalkboard-30;
}
}
#code-mirror-override .cm-scroller,

View File

@ -70,15 +70,17 @@ if (isDesktop()) {
id: AUTO_UPDATER_TOAST_ID,
})
})
window.electron.onUpdateDownloaded((version: string) => {
window.electron.onUpdateDownloaded(({ version, releaseNotes }) => {
const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
console.log(message)
toast.custom(
ToastUpdate({
version,
releaseNotes,
onRestart: () => {
window.electron.appRestart()
},
onDismiss: () => {},
}),
{ duration: 30000, id: AUTO_UPDATER_TOAST_ID }
)

View File

@ -501,6 +501,7 @@ export function sketchOnExtrudedFace(
createIdentifier(extrudeName ? extrudeName : oldSketchName),
_tag,
]),
undefined,
'const'
)
@ -682,6 +683,7 @@ export function createPipeExpression(
export function createVariableDeclaration(
varName: string,
init: VariableDeclarator['init'],
visibility: VariableDeclaration['visibility'] = 'default',
kind: VariableDeclaration['kind'] = 'const'
): VariableDeclaration {
return {
@ -699,6 +701,7 @@ export function createVariableDeclaration(
init,
},
],
visibility,
kind,
}
}

View File

@ -620,7 +620,7 @@ describe('Testing button states', () => {
it('should return true when body exists and segment is selected', async () => {
await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true)
})
it('hould return false when body exists and not a segment is selected', async () => {
it('should return false when body exists and not a segment is selected', async () => {
await runButtonStateTest(codeWithBody, `close(%)`, false)
})
})

View File

@ -1,5 +1,7 @@
import {
CallExpression,
Expr,
Identifier,
ObjectExpression,
PathToNode,
Program,
@ -27,7 +29,7 @@ import {
sketchLineHelperMap,
} from '../std/sketch'
import { err, trap } from 'lib/trap'
import { Selections, canFilletSelection } from 'lib/selections'
import { Selections } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes'
import {
ArtifactGraph,
@ -66,7 +68,10 @@ export function modifyAstCloneWithFilletAndTag(
const artifactGraph = engineCommandManager.artifactGraph
// Step 1: modify ast with tags and group them by extrude nodes (bodies)
const extrudeToTagsMap: Map<PathToNode, string[]> = new Map()
const extrudeToTagsMap: Map<
PathToNode,
Array<{ tag: string; selectionType: string }>
> = new Map()
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
for (const selectionRange of selection.codeBasedSelections) {
@ -74,6 +79,7 @@ export function modifyAstCloneWithFilletAndTag(
codeBasedSelections: [selectionRange],
otherSelections: [],
}
const selectionType = singleSelection.codeBasedSelections[0].type
const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
@ -89,6 +95,7 @@ export function modifyAstCloneWithFilletAndTag(
)
if (err(tagResult)) return tagResult
const { tag } = tagResult
const tagInfo = { tag, selectionType }
// Group tags by their corresponding extrude node
const extrudeKey = JSON.stringify(pathToExtrudeNode)
@ -96,23 +103,29 @@ export function modifyAstCloneWithFilletAndTag(
if (lookupMap.has(extrudeKey)) {
const existingPath = lookupMap.get(extrudeKey)
if (!existingPath) return new Error('Path to extrude node not found.')
extrudeToTagsMap.get(existingPath)?.push(tag)
extrudeToTagsMap.get(existingPath)?.push(tagInfo)
} else {
lookupMap.set(extrudeKey, pathToExtrudeNode)
extrudeToTagsMap.set(pathToExtrudeNode, [tag])
extrudeToTagsMap.set(pathToExtrudeNode, [tagInfo])
}
}
// Step 2: Apply fillet(s) for each extrude node (body)
let pathToFilletNodes: Array<PathToNode> = []
for (const [pathToExtrudeNode, tags] of extrudeToTagsMap.entries()) {
for (const [pathToExtrudeNode, tagInfos] of extrudeToTagsMap.entries()) {
// Create a fillet expression with multiple tags
const radiusValue =
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
const tagCalls = tagInfos.map(({ tag, selectionType }) => {
return getEdgeTagCall(tag, selectionType)
})
const firstTag = tagCalls[0] // can be Identifier or CallExpression (for opposite and adjacent edges)
const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({
radius: radiusValue,
tags: createArrayExpression(tags.map((tag) => createIdentifier(tag))),
tags: createArrayExpression(tagCalls),
}),
createPipeSubstitution(),
])
@ -144,7 +157,7 @@ export function modifyAstCloneWithFilletAndTag(
pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tags[0]
firstTag
)
pathToFilletNodes.push(pathToFilletNode)
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
@ -165,7 +178,7 @@ export function modifyAstCloneWithFilletAndTag(
pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tags[0]
firstTag
)
pathToFilletNodes.push(pathToFilletNode)
} else {
@ -276,6 +289,21 @@ function mutateAstWithTagForSketchSegment(
return { modifiedAst: astClone, tag }
}
function getEdgeTagCall(
tag: string,
selectionType: string
): Identifier | CallExpression {
let tagCall: Expr = createIdentifier(tag)
// Modify the tag based on selectionType
if (selectionType === 'edge') {
tagCall = createCallExpressionStdLib('getOppositeEdge', [tagCall])
} else if (selectionType === 'adjacent-edge') {
tagCall = createCallExpressionStdLib('getNextAdjacentEdge', [tagCall])
}
return tagCall
}
function locateExtrudeDeclarator(
node: Program,
pathToExtrudeNode: PathToNode
@ -311,7 +339,7 @@ function locateExtrudeDeclarator(
function getPathToNodeOfFilletLiteral(
pathToExtrudeNode: PathToNode,
extrudeDeclarator: VariableDeclarator,
tag: string
tag: Identifier | CallExpression
): PathToNode {
let pathToFilletObj: PathToNode = []
let inFillet = false
@ -347,12 +375,30 @@ function getPathToNodeOfFilletLiteral(
]
}
function hasTag(node: ObjectExpression, tag: string): boolean {
function hasTag(
node: ObjectExpression,
tag: Identifier | CallExpression
): boolean {
return node.properties.some((prop) => {
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
return prop.value.elements.some(
(element) => element.type === 'Identifier' && element.name === tag
)
// if selection is a base edge:
if (tag.type === 'Identifier') {
return prop.value.elements.some(
(element) =>
element.type === 'Identifier' && element.name === tag.name
)
}
// if selection is an adjacent or opposite edge:
if (tag.type === 'CallExpression') {
return prop.value.elements.some(
(element) =>
element.type === 'CallExpression' &&
element.callee.name === tag.callee.name && // edge location
element.arguments[0].type === 'Identifier' &&
tag.arguments[0].type === 'Identifier' &&
element.arguments[0].name === tag.arguments[0].name // tag name
)
}
}
return false
})
@ -383,7 +429,7 @@ export const hasValidFilletSelection = ({
ast: Program
code: string
}) => {
// case 0: check if there is anything filletable in the scene
// check if there is anything filletable in the scene
let extrudeExists = false
traverse(ast, {
enter(node) {
@ -394,65 +440,88 @@ export const hasValidFilletSelection = ({
})
if (!extrudeExists) return false
// case 1: nothing selected, test whether the extrusion exists
if (selectionRanges) {
if (selectionRanges.codeBasedSelections.length === 0) {
return true
}
const range0 = selectionRanges.codeBasedSelections[0].range[0]
const codeLength = code.length
if (range0 === codeLength) {
return true
}
// check if nothing is selected
if (selectionRanges.codeBasedSelections.length === 0) {
return true
}
// case 2: sketch segment selected, test whether it is extruded
// TODO: add loft / sweep check
if (selectionRanges.codeBasedSelections.length > 0) {
const isExtruded = hasSketchPipeBeenExtruded(
selectionRanges.codeBasedSelections[0],
ast
// check if selection is last string in code
if (selectionRanges.codeBasedSelections[0].range[0] === code.length) {
return true
}
// selection exists:
for (const selection of selectionRanges.codeBasedSelections) {
// check if all selections are in sketchLineHelperMap
const path = getNodePathFromSourceRange(ast, selection.range)
const segmentNode = getNodeFromPath<CallExpression>(
ast,
path,
'CallExpression'
)
if (isExtruded) {
const pathToSelectedNode = getNodePathFromSourceRange(
ast,
selectionRanges.codeBasedSelections[0].range
)
const segmentNode = getNodeFromPath<CallExpression>(
ast,
pathToSelectedNode,
'CallExpression'
)
if (err(segmentNode)) return false
if (segmentNode.node.type === 'CallExpression') {
const segmentName = segmentNode.node.callee.name
if (segmentName in sketchLineHelperMap) {
// Add check whether the tag exists at all:
if (!(segmentNode.node.arguments.length === 3)) return true
// If the tag exists, check if it is already filleted
const edges = isTagUsedInFillet({
ast,
callExp: segmentNode.node,
})
// edge has already been filleted
if (
['edge', 'default'].includes(
selectionRanges.codeBasedSelections[0].type
) &&
edges.includes('baseEdge')
)
return false
return true
} else {
return false
}
}
} else {
if (err(segmentNode)) return false
if (segmentNode.node.type !== 'CallExpression') {
return false
}
if (!(segmentNode.node.callee.name in sketchLineHelperMap)) {
return false
}
}
return canFilletSelection(selectionRanges)
// check if selection is extruded
// TODO: option 1 : extrude is in the sketch pipe
// option 2: extrude is outside the sketch pipe
const extrudeExists = hasSketchPipeBeenExtruded(selection, ast)
if (err(extrudeExists)) {
return false
}
if (!extrudeExists) {
return false
}
// check if tag exists for the selection
let tagExists = false
let tag = ''
traverse(segmentNode.node, {
enter(node) {
if (node.type === 'TagDeclarator') {
tagExists = true
tag = node.value
}
},
})
// check if tag is used in fillet
if (tagExists) {
// create tag call
let tagCall: Expr = getEdgeTagCall(tag, selection.type)
// check if tag is used in fillet
let inFillet = false
let tagUsedInFillet = false
traverse(ast, {
enter(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = true
}
if (inFillet && node.type === 'ObjectExpression') {
if (hasTag(node, tagCall)) {
tagUsedInFillet = true
}
}
},
leave(node) {
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
inFillet = false
}
},
})
if (tagUsedInFillet) {
return false
}
}
}
return true
}
type EdgeTypes =

View File

@ -28,6 +28,7 @@ import {
getConstraintType,
} from './std/sketchcombos'
import { err } from 'lib/trap'
import { ImportStatement } from 'wasm-lib/kcl/bindings/ImportStatement'
/**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -120,7 +121,12 @@ export function getNodeFromPathCurry(
}
function moreNodePathFromSourceRange(
node: Expr | ExpressionStatement | VariableDeclaration | ReturnStatement,
node:
| Expr
| ImportStatement
| ExpressionStatement
| VariableDeclaration
| ReturnStatement,
sourceRange: Selection['range'],
previousPath: PathToNode = [['body', '']]
): PathToNode {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 613 KiB

View File

@ -426,6 +426,7 @@ export const _executor = async (
baseUnit,
engineCommandManager,
fileSystemManager,
undefined,
isMock
)
return execStateFromRaw(execState)

View File

@ -55,6 +55,23 @@ export interface paths {
patch?: never
trace?: never
}
'/metrics': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
/** List available machines and their statuses */
get: operations['get_metrics']
put?: never
post?: never
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
'/ping': {
parameters: {
query?: never
@ -278,6 +295,28 @@ export interface operations {
'5XX': components['responses']['Error']
}
}
get_metrics: {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
requestBody?: never
responses: {
/** @description successful operation */
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': string
}
}
'4XX': components['responses']['Error']
'5XX': components['responses']['Error']
}
}
ping: {
parameters: {
query?: never

View File

@ -287,7 +287,10 @@ app.on('ready', () => {
autoUpdater.on('update-downloaded', (info) => {
console.log('update-downloaded', info)
mainWindow?.webContents.send('update-downloaded', info.version)
mainWindow?.webContents.send('update-downloaded', {
version: info.version,
releaseNotes: info.releaseNotes,
})
})
ipcMain.handle('app.restart', () => {

View File

@ -16,11 +16,12 @@ const startDeviceFlow = (host: string): Promise<string> =>
ipcRenderer.invoke('startDeviceFlow', host)
const loginWithDeviceFlow = (): Promise<string> =>
ipcRenderer.invoke('loginWithDeviceFlow')
const onUpdateDownloaded = (
callback: (value: { version: string; releaseNotes: string }) => void
) => ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
const onUpdateDownloadStart = (
callback: (value: { version: string }) => void
) => ipcRenderer.on('update-download-start', (_event, value) => callback(value))
const onUpdateDownloaded = (callback: (value: string) => void) =>
ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
const onUpdateError = (callback: (value: Error) => void) =>
ipcRenderer.on('update-error', (_event, value) => callback(value))
const appRestart = () => ipcRenderer.invoke('app.restart')

View File

@ -1394,9 +1394,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.2"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
checksum = "d97eb9a8e0cd5b76afea91d7eecd5cf8338cd44ced04256cf1f800474b227c52"
dependencies = [
"bytemuck",
"byteorder-lite",
@ -1533,16 +1533,16 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.71"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cb94a0ffd3f3ee755c20f7d8752f45cac88605a4dcf808abcff72873296ec7b"
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kcl-lib"
version = "0.2.20"
version = "0.2.22"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1617,7 +1617,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.12"
version = "0.1.13"
dependencies = [
"anyhow",
"hyper 0.14.30",
@ -2337,18 +2337,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.86"
version = "1.0.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3"
version = "0.22.3"
version = "0.22.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15ee168e30649f7f234c3d49ef5a7a6cbf5134289bc46c29ff3155fa3221c225"
checksum = "3d922163ba1f79c04bc49073ba7b32fd5a8d3b76a87c955921234b8e77333c51"
dependencies = [
"cfg-if",
"indoc",
@ -2364,9 +2364,9 @@ dependencies = [
[[package]]
name = "pyo3-build-config"
version = "0.22.3"
version = "0.22.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e61cef80755fe9e46bb8a0b8f20752ca7676dcc07a5277d8b7768c6172e529b3"
checksum = "bc38c5feeb496c8321091edf3d63e9a6829eab4b863b4a6a65f26f3e9cc6b179"
dependencies = [
"once_cell",
"target-lexicon",
@ -2374,9 +2374,9 @@ dependencies = [
[[package]]
name = "pyo3-ffi"
version = "0.22.3"
version = "0.22.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67ce096073ec5405f5ee2b8b31f03a68e02aa10d5d4f565eca04acc41931fa1c"
checksum = "94845622d88ae274d2729fcefc850e63d7a3ddff5e3ce11bd88486db9f1d357d"
dependencies = [
"libc",
"pyo3-build-config",
@ -2384,9 +2384,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
version = "0.22.3"
version = "0.22.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2440c6d12bc8f3ae39f1e775266fa5122fd0c8891ce7520fa6048e683ad3de28"
checksum = "e655aad15e09b94ffdb3ce3d217acf652e26bbc37697ef012f5e5e348c716e5e"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@ -2396,9 +2396,9 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
version = "0.22.3"
version = "0.22.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1be962f0e06da8f8465729ea2cb71a416d2257dff56cbe40a70d3e62a93ae5d1"
checksum = "ae1e3f09eecd94618f60a455a23def79f79eba4dc561a97324bf9ac8c6df30ce"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -3829,9 +3829,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
"serde",
@ -3907,9 +3907,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.94"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef073ced962d62984fb38a36e5fdc1a2b23c9e0e1fa0689bb97afa4202ef6887"
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
dependencies = [
"cfg-if",
"once_cell",
@ -3918,9 +3918,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.94"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4bfab14ef75323f4eb75fa52ee0a3fb59611977fd3240da19b2cf36ff85030e"
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
dependencies = [
"bumpalo",
"log",
@ -3946,9 +3946,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.94"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7bec9830f60924d9ceb3ef99d55c155be8afa76954edffbb5936ff4509474e7"
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -3956,9 +3956,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.94"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c74f6e152a76a2ad448e223b0fc0b6b5747649c3d769cc6bf45737bf97d0ed6"
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
"proc-macro2",
"quote",
@ -3969,9 +3969,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.94"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a42f6c679374623f295a8623adfe63d9284091245c3504bde47c17a3ce2777d9"
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
[[package]]
name = "wasm-lib"
@ -4032,9 +4032,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.70"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
dependencies = [
"js-sys",
"wasm-bindgen",

View File

@ -18,31 +18,31 @@ kittycad.workspace = true
serde_json = "1.0.128"
tokio = { version = "1.40.0", features = ["sync"] }
toml = "0.8.19"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.44"
[dev-dependencies]
anyhow = "1"
image = { version = "0.25.1", default-features = false, features = ["png"] }
image = { version = "0.25.3", default-features = false, features = ["png"] }
kittycad = { workspace = true, default-features = true }
kittycad-modeling-cmds = { workspace = true }
pretty_assertions = "1.4.1"
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"] }
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
futures = "0.3.31"
js-sys = "0.3.71"
js-sys = "0.3.72"
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen-futures = { version = "0.4.44", features = ["futures-core-03-stream"] }
wasm-streams = "0.4.1"
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
version = "0.3.69"
version = "0.3.72"
features = [
"console",
"HtmlTextAreaElement",

View File

@ -762,7 +762,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_someFn {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_someFn {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -51,7 +51,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -18,7 +18,7 @@ mod test_examples_my_func {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -52,7 +52,7 @@ mod test_examples_my_func {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -18,7 +18,7 @@ mod test_examples_line_to {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -52,7 +52,7 @@ mod test_examples_line_to {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_min {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
@ -51,7 +51,7 @@ mod test_examples_min {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_import {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_import {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_import {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_show {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -17,7 +17,7 @@ mod test_examples_some_function {
settings: Default::default(),
context_type: crate::executor::ContextType::Mock,
};
ctx.run(&program, None, id_generator).await.unwrap();
ctx.run(&program, None, id_generator, None).await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]

View File

@ -1,7 +1,7 @@
extern crate alloc;
use kcl_lib::ast::types::{
BodyItem, Expr, Identifier, Literal, LiteralValue, NonCodeMeta, Program, VariableDeclaration, VariableDeclarator,
VariableKind,
BodyItem, Expr, Identifier, ItemVisibility, Literal, LiteralValue, NonCodeMeta, Program, VariableDeclaration,
VariableDeclarator, VariableKind,
};
use kcl_macros::parse;
use pretty_assertions::assert_eq;
@ -33,6 +33,7 @@ fn basic() {
})),
digest: None,
}],
visibility: ItemVisibility::Default,
kind: VariableKind::Const,
digest: None,
})],

View File

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

View File

@ -178,7 +178,7 @@ async fn snapshot_endpoint(body: Bytes, state: ExecutorContext) -> Response<Body
// Let users know if the test is taking a long time.
let (done_tx, done_rx) = oneshot::channel::<()>();
let timer = time_until(done_rx);
let snapshot = match state.execute_and_prepare_snapshot(&program, id_generator).await {
let snapshot = match state.execute_and_prepare_snapshot(&program, id_generator, None).await {
Ok(sn) => sn,
Err(e) => return kcl_err(e),
};

View File

@ -20,4 +20,4 @@ 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"] }
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }

View File

@ -1,6 +1,7 @@
use anyhow::Result;
use indexmap::IndexMap;
use kcl_lib::{
engine::ExecutionKind,
errors::KclError,
executor::{DefaultPlanes, IdGenerator},
};
@ -26,6 +27,7 @@ pub struct EngineConnection {
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::executor::SourceRange)>>>,
core_test: Arc<Mutex<String>>,
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
}
impl EngineConnection {
@ -39,6 +41,7 @@ impl EngineConnection {
batch_end: Arc::new(Mutex::new(IndexMap::new())),
core_test: result,
default_planes: Default::default(),
execution_kind: Default::default(),
})
}
@ -360,6 +363,18 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
self.batch_end.clone()
}
fn execution_kind(&self) -> ExecutionKind {
let guard = self.execution_kind.lock().unwrap();
*guard
}
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
let mut guard = self.execution_kind.lock().unwrap();
let original = *guard;
*guard = execution_kind;
original
}
async fn default_planes(
&self,
id_generator: &mut IdGenerator,

View File

@ -23,7 +23,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
settings: Default::default(),
context_type: kcl_lib::executor::ContextType::MockCustomForwarded,
};
let _memory = ctx.run(&program, None, IdGenerator::default()).await?;
let _memory = ctx.run(&program, None, IdGenerator::default(), None).await?;
let result = result.lock().expect("mutex lock").clone();
Ok(result)

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.20"
version = "0.2.22"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -26,7 +26,7 @@ futures = { version = "0.3.31" }
git_rev = "0.1.0"
gltf-json = "1.4.1"
http = { workspace = true }
image = { version = "0.25.1", default-features = false, features = ["png"] }
image = { version = "0.25.3", default-features = false, features = ["png"] }
indexmap = { version = "2.6.0", features = ["serde"] }
kittycad = { workspace = true }
kittycad-modeling-cmds = { workspace = true }
@ -34,7 +34,7 @@ lazy_static = "1.5.0"
measurements = "0.11.0"
mime_guess = "2.0.5"
parse-display = "0.9.1"
pyo3 = { version = "0.22.3", optional = true }
pyo3 = { version = "0.22.5", optional = true }
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"] }
@ -47,18 +47,18 @@ toml = "0.8.19"
ts-rs = { version = "10.0.0", features = ["uuid-impl", "url-impl", "chrono-impl", "no-serde-warnings", "serde-json-impl"] }
url = { version = "2.5.2", features = ["serde"] }
urlencoding = "2.1.3"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
uuid = { version = "1.11.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] }
winnow = "0.6.18"
zip = { version = "2.0.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.71" }
js-sys = { version = "0.3.72" }
tokio = { version = "1.40.0", features = ["sync", "time"] }
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
wasm-bindgen = "0.2.91"
wasm-bindgen-futures = "0.4.44"
web-sys = { version = "0.3.69", features = ["console"] }
web-sys = { version = "0.3.72", features = ["console"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
approx = "0.5"
@ -93,7 +93,7 @@ criterion = { version = "0.5.1", features = ["async_tokio"] }
expectorate = "1.1.0"
handlebars = "6.1.0"
iai = "0.1"
image = { version = "0.25.1", default-features = false, features = ["png"] }
image = { version = "0.25.3", default-features = false, features = ["png"] }
insta = { version = "1.40.0", features = ["json"] }
itertools = "0.13.0"
pretty_assertions = "1.4.1"

View File

@ -48,7 +48,10 @@ pub async fn modify_ast_for_sketch(
// Get the information about the sketch.
if let Some(ast_sketch) = program.get_variable(sketch_name) {
let constraint_level = ast_sketch.get_constraint_level();
let constraint_level = match ast_sketch {
super::types::Definition::Variable(var) => var.get_constraint_level(),
super::types::Definition::Import(import) => import.get_constraint_level(),
};
match &constraint_level {
ConstraintLevel::None { source_ranges: _ } => {}
ConstraintLevel::Ignore { source_ranges: _ } => {}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,740 @@
use super::{
human_friendly_type, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart,
CallExpression, Expr, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, ObjectExpression,
TagDeclarator, UnaryExpression, UnaryOperator,
};
use crate::{
errors::{KclError, KclErrorDetails},
executor::{
BodyType, ExecState, ExecutorContext, KclValue, Metadata, Sketch, SourceRange, StatementKind, TagEngineInfo,
TagIdentifier, UserVal,
},
std::FunctionKind,
};
use async_recursion::async_recursion;
use serde_json::Value as JValue;
impl BinaryPart {
#[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
match self {
BinaryPart::Literal(literal) => Ok(literal.into()),
BinaryPart::Identifier(identifier) => {
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
Ok(value.clone())
}
BinaryPart::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, ctx).await,
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,
}
}
}
impl MemberExpression {
pub fn get_result_array(&self, exec_state: &mut ExecState, index: usize) -> Result<KclValue, KclError> {
let array = match &self.object {
MemberObject::MemberExpression(member_expr) => member_expr.get_result(exec_state)?,
MemberObject::Identifier(identifier) => {
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
value.clone()
}
};
let array_json = array.get_json_value()?;
if let serde_json::Value::Array(array) = array_json {
if let Some(value) = array.get(index) {
Ok(KclValue::UserVal(UserVal {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("index {} not found in array", index),
source_ranges: vec![self.clone().into()],
}))
}
} else {
Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression array is not an array: {:?}", array),
source_ranges: vec![self.clone().into()],
}))
}
}
pub fn get_result(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
#[derive(Debug)]
enum Property {
Number(usize),
String(String),
}
impl Property {
fn type_name(&self) -> &'static str {
match self {
Property::Number(_) => "number",
Property::String(_) => "string",
}
}
}
let property_src: SourceRange = self.property.clone().into();
let property_sr = vec![property_src];
let property: Property = match self.property.clone() {
LiteralIdentifier::Identifier(identifier) => {
let name = identifier.name;
if !self.computed {
// Treat the property as a literal
Property::String(name.to_string())
} else {
// Actually evaluate memory to compute the property.
let prop = exec_state.memory.get(&name, property_src)?;
let KclValue::UserVal(prop) = prop else {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string or int (>= 0) here",
),
}));
};
match prop.value {
JValue::Number(ref num) => {
num
.as_u64()
.and_then(|x| usize::try_from(x).ok())
.map(Property::Number)
.ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name}'s value is not a valid property/index, you can only use a string or int (>= 0) here",
),
})
})?
}
JValue::String(ref x) => Property::String(x.to_owned()),
_ => {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!(
"{name} is not a valid property/index, you can only use a string to get the property of an object, or an int (>= 0) to get an item in an array",
),
}));
}
}
}
}
LiteralIdentifier::Literal(literal) => {
let value = literal.value.clone();
match value {
LiteralValue::IInteger(x) => {
if let Ok(x) = u64::try_from(x) {
Property::Number(x.try_into().unwrap())
} else {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!("{x} is not a valid index, indices must be whole numbers >= 0"),
}));
}
}
LiteralValue::String(s) => Property::String(s),
_ => {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: "Only strings or ints (>= 0) can be properties/indexes".to_owned(),
}));
}
}
}
};
let object = match &self.object {
// TODO: Don't use recursion here, use a loop.
MemberObject::MemberExpression(member_expr) => member_expr.get_result(exec_state)?,
MemberObject::Identifier(identifier) => {
let value = exec_state.memory.get(&identifier.name, identifier.into())?;
value.clone()
}
};
let object_json = object.get_json_value()?;
// Check the property and object match -- e.g. ints for arrays, strs for objects.
match (object_json, property) {
(JValue::Object(map), Property::String(property)) => {
if let Some(value) = map.get(&property) {
Ok(KclValue::UserVal(UserVal {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("Property '{property}' not found in object"),
source_ranges: vec![self.clone().into()],
}))
}
}
(JValue::Object(_), p) => Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Only strings can be used as the property of an object, but you're using a {}",
p.type_name()
),
source_ranges: vec![self.clone().into()],
})),
(JValue::Array(arr), Property::Number(index)) => {
let value_of_arr: Option<&JValue> = arr.get(index);
if let Some(value) = value_of_arr {
Ok(KclValue::UserVal(UserVal {
value: value.clone(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("The array doesn't have any item at index {index}"),
source_ranges: vec![self.clone().into()],
}))
}
}
(JValue::Array(_), p) => Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Only integers >= 0 can be used as the index of an array, but you're using a {}",
p.type_name()
),
source_ranges: vec![self.clone().into()],
})),
(being_indexed, _) => {
let t = human_friendly_type(&being_indexed);
Err(KclError::Semantic(KclErrorDetails {
message: format!("Only arrays and objects can be indexed, but you're trying to index a {t}"),
source_ranges: vec![self.clone().into()],
}))
}
}
}
}
impl BinaryExpression {
#[async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let left_json_value = self.left.get_result(exec_state, ctx).await?.get_json_value()?;
let right_json_value = self.right.get_result(exec_state, ctx).await?.get_json_value()?;
// First check if we are doing string concatenation.
if self.operator == BinaryOperator::Add {
if let (Some(left), Some(right)) = (
parse_json_value_as_string(&left_json_value),
parse_json_value_as_string(&right_json_value),
) {
let value = serde_json::Value::String(format!("{}{}", left, right));
return Ok(KclValue::UserVal(UserVal {
value,
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
}
let left = parse_json_number_as_f64(&left_json_value, self.left.clone().into())?;
let right = parse_json_number_as_f64(&right_json_value, self.right.clone().into())?;
let value: serde_json::Value = match self.operator {
BinaryOperator::Add => (left + right).into(),
BinaryOperator::Sub => (left - right).into(),
BinaryOperator::Mul => (left * right).into(),
BinaryOperator::Div => (left / right).into(),
BinaryOperator::Mod => (left % right).into(),
BinaryOperator::Pow => (left.powf(right)).into(),
BinaryOperator::Eq => (left == right).into(),
BinaryOperator::Neq => (left != right).into(),
BinaryOperator::Gt => (left > right).into(),
BinaryOperator::Gte => (left >= right).into(),
BinaryOperator::Lt => (left < right).into(),
BinaryOperator::Lte => (left <= right).into(),
};
Ok(KclValue::UserVal(UserVal {
value,
meta: vec![Metadata {
source_range: self.into(),
}],
}))
}
}
impl UnaryExpression {
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
if self.operator == UnaryOperator::Not {
let value = self.argument.get_result(exec_state, ctx).await?.get_json_value()?;
let Some(bool_value) = json_as_bool(&value) else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Cannot apply unary operator ! to non-boolean value: {}", value),
source_ranges: vec![self.into()],
}));
};
let negated = !bool_value;
return Ok(KclValue::UserVal(UserVal {
value: serde_json::Value::Bool(negated),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let num = parse_json_number_as_f64(
&self.argument.get_result(exec_state, ctx).await?.get_json_value()?,
self.into(),
)?;
Ok(KclValue::UserVal(UserVal {
value: (-(num)).into(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
}
}
pub(crate) async fn execute_pipe_body(
exec_state: &mut ExecState,
body: &[Expr],
source_range: SourceRange,
ctx: &ExecutorContext,
) -> Result<KclValue, KclError> {
let Some((first, body)) = body.split_first() else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Pipe expressions cannot be empty".to_owned(),
source_ranges: vec![source_range],
}));
};
// Evaluate the first element in the pipeline.
// They use the pipe_value from some AST node above this, so that if pipe expression is nested in a larger pipe expression,
// they use the % from the parent. After all, this pipe expression hasn't been executed yet, so it doesn't have any % value
// of its own.
let meta = Metadata {
source_range: SourceRange([first.start(), first.end()]),
};
let output = ctx
.execute_expr(first, exec_state, &meta, StatementKind::Expression)
.await?;
// Now that we've evaluated the first child expression in the pipeline, following child expressions
// should use the previous child expression for %.
// This means there's no more need for the previous pipe_value from the parent AST node above this one.
let previous_pipe_value = std::mem::replace(&mut exec_state.pipe_value, Some(output));
// Evaluate remaining elements.
let result = inner_execute_pipe_body(exec_state, body, ctx).await;
// Restore the previous pipe value.
exec_state.pipe_value = previous_pipe_value;
result
}
/// Execute the tail of a pipe expression. exec_state.pipe_value must be set by
/// the caller.
#[async_recursion]
async fn inner_execute_pipe_body(
exec_state: &mut ExecState,
body: &[Expr],
ctx: &ExecutorContext,
) -> Result<KclValue, KclError> {
for expression in body {
match expression {
Expr::TagDeclarator(_) => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("This cannot be in a PipeExpression: {:?}", expression),
source_ranges: vec![expression.into()],
}));
}
Expr::Literal(_)
| Expr::Identifier(_)
| Expr::BinaryExpression(_)
| Expr::FunctionExpression(_)
| Expr::CallExpression(_)
| Expr::PipeExpression(_)
| Expr::PipeSubstitution(_)
| Expr::ArrayExpression(_)
| Expr::ArrayRangeExpression(_)
| Expr::ObjectExpression(_)
| Expr::MemberExpression(_)
| Expr::UnaryExpression(_)
| Expr::IfExpression(_)
| Expr::None(_) => {}
};
let metadata = Metadata {
source_range: SourceRange([expression.start(), expression.end()]),
};
let output = ctx
.execute_expr(expression, exec_state, &metadata, StatementKind::Expression)
.await?;
exec_state.pipe_value = Some(output);
}
// Safe to unwrap here, because pipe_value always has something pushed in when the `match first` executes.
let final_output = exec_state.pipe_value.take().unwrap();
Ok(final_output)
}
impl CallExpression {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let fn_name = &self.callee.name;
let mut fn_args: Vec<KclValue> = Vec::with_capacity(self.arguments.len());
for arg in &self.arguments {
let metadata = Metadata {
source_range: SourceRange::from(arg),
};
let result = ctx
.execute_expr(arg, exec_state, &metadata, StatementKind::Expression)
.await?;
fn_args.push(result);
}
match ctx.stdlib.get_either(&self.callee.name) {
FunctionKind::Core(func) => {
// Attempt to call the function.
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
let mut result = func.std_lib_fn()(exec_state, args).await?;
// If the return result is a sketch or solid, we want to update the
// memory for the tags of the group.
// TODO: This could probably be done in a better way, but as of now this was my only idea
// and it works.
match result {
KclValue::UserVal(ref mut uval) => {
uval.mutate(|sketch: &mut Sketch| {
for (_, tag) in sketch.tags.iter() {
exec_state.memory.update_tag(&tag.value, tag.clone())?;
}
Ok::<_, KclError>(())
})?;
}
KclValue::Solid(ref mut solid) => {
for value in &solid.value {
if let Some(tag) = value.get_tag() {
// Get the past tag and update it.
let mut t = if let Some(t) = solid.sketch.tags.get(&tag.name) {
t.clone()
} else {
// It's probably a fillet or a chamfer.
// Initialize it.
TagIdentifier {
value: tag.name.clone(),
info: Some(TagEngineInfo {
id: value.get_id(),
surface: Some(value.clone()),
path: None,
sketch: solid.id,
}),
meta: vec![Metadata {
source_range: tag.clone().into(),
}],
}
};
let Some(ref info) = t.info else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Tag {} does not have path info", tag.name),
source_ranges: vec![tag.into()],
}));
};
let mut info = info.clone();
info.surface = Some(value.clone());
info.sketch = solid.id;
t.info = Some(info);
exec_state.memory.update_tag(&tag.name, t.clone())?;
// update the sketch tags.
solid.sketch.tags.insert(tag.name.clone(), t);
}
}
// Find the stale sketch in memory and update it.
if let Some(current_env) = exec_state
.memory
.environments
.get_mut(exec_state.memory.current_env.index())
{
current_env.update_sketch_tags(&solid.sketch);
}
}
_ => {}
}
Ok(result)
}
FunctionKind::Std(func) => {
let function_expression = func.function();
let (required_params, optional_params) =
function_expression.required_and_optional_params().map_err(|e| {
KclError::Semantic(KclErrorDetails {
message: format!("Error getting parts of function: {}", e),
source_ranges: vec![self.into()],
})
})?;
if fn_args.len() < required_params.len() || fn_args.len() > function_expression.params.len() {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"this function expected {} arguments, got {}",
required_params.len(),
fn_args.len(),
),
source_ranges: vec![self.into()],
}));
}
// Add the arguments to the memory.
let mut fn_memory = exec_state.memory.clone();
for (index, param) in required_params.iter().enumerate() {
fn_memory.add(
&param.identifier.name,
fn_args.get(index).unwrap().clone(),
param.identifier.clone().into(),
)?;
}
// Add the optional arguments to the memory.
for (index, param) in optional_params.iter().enumerate() {
if let Some(arg) = fn_args.get(index + required_params.len()) {
fn_memory.add(&param.identifier.name, arg.clone(), param.identifier.clone().into())?;
} else {
fn_memory.add(
&param.identifier.name,
KclValue::UserVal(UserVal {
value: serde_json::value::Value::Null,
meta: Default::default(),
}),
param.identifier.clone().into(),
)?;
}
}
let fn_dynamic_state = exec_state.dynamic_state.clone();
// TODO: Shouldn't we merge program memory into fn_dynamic_state
// here?
// Call the stdlib function
let p = &func.function().body;
let (exec_result, fn_memory) = {
let previous_memory = std::mem::replace(&mut exec_state.memory, fn_memory);
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
let result = ctx.inner_execute(p, exec_state, BodyType::Block).await;
exec_state.dynamic_state = previous_dynamic_state;
let fn_memory = std::mem::replace(&mut exec_state.memory, previous_memory);
(result, fn_memory)
};
match exec_result {
Ok(_) => {}
Err(err) => {
// We need to override the source ranges so we don't get the embedded kcl
// function from the stdlib.
return Err(err.override_source_ranges(vec![self.into()]));
}
};
let out = fn_memory.return_;
let result = out.ok_or_else(|| {
KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of stdlib function {} is undefined", fn_name),
source_ranges: vec![self.into()],
})
})?;
Ok(result)
}
FunctionKind::UserDefined => {
let source_range = SourceRange::from(self);
// Clone the function so that we can use a mutable reference to
// exec_state.
let func = exec_state.memory.get(fn_name, source_range)?.clone();
let fn_dynamic_state = exec_state.dynamic_state.merge(&exec_state.memory);
let return_value = {
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
let result = func.call_fn(fn_args, exec_state, ctx.clone()).await.map_err(|e| {
// Add the call expression to the source ranges.
e.add_source_ranges(vec![source_range])
});
exec_state.dynamic_state = previous_dynamic_state;
result?
};
let result = return_value.ok_or_else(move || {
let mut source_ranges: Vec<SourceRange> = vec![source_range];
// We want to send the source range of the original function.
if let KclValue::Function { meta, .. } = func {
source_ranges = meta.iter().map(|m| m.source_range).collect();
};
KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of user-defined function {} is undefined", fn_name),
source_ranges,
})
})?;
Ok(result)
}
}
}
}
impl TagDeclarator {
pub async fn execute(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
let memory_item = KclValue::TagIdentifier(Box::new(TagIdentifier {
value: self.name.clone(),
info: None,
meta: vec![Metadata {
source_range: self.into(),
}],
}));
exec_state.memory.add(&self.name, memory_item.clone(), self.into())?;
Ok(self.into())
}
}
impl ArrayExpression {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let mut results = Vec::with_capacity(self.elements.len());
for element in &self.elements {
let metadata = Metadata::from(element);
// TODO: Carry statement kind here so that we know if we're
// inside a variable declaration.
let value = ctx
.execute_expr(element, exec_state, &metadata, StatementKind::Expression)
.await?;
results.push(value.get_json_value()?);
}
Ok(KclValue::UserVal(UserVal {
value: results.into(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
}
}
impl ArrayRangeExpression {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let metadata = Metadata::from(&*self.start_element);
let start = ctx
.execute_expr(&self.start_element, exec_state, &metadata, StatementKind::Expression)
.await?
.get_json_value()?;
let start = parse_json_number_as_u64(&start, (&*self.start_element).into())?;
let metadata = Metadata::from(&*self.end_element);
let end = ctx
.execute_expr(&self.end_element, exec_state, &metadata, StatementKind::Expression)
.await?
.get_json_value()?;
let end = parse_json_number_as_u64(&end, (&*self.end_element).into())?;
if end < start {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: format!("Range start is greater than range end: {start} .. {end}"),
}));
}
let range: Vec<_> = if self.end_inclusive {
(start..=end).map(JValue::from).collect()
} else {
(start..end).map(JValue::from).collect()
};
Ok(KclValue::UserVal(UserVal {
value: range.into(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
}
}
impl ObjectExpression {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let mut object = serde_json::Map::new();
for property in &self.properties {
let metadata = Metadata::from(&property.value);
let result = ctx
.execute_expr(&property.value, exec_state, &metadata, StatementKind::Expression)
.await?;
object.insert(property.key.name.clone(), result.get_json_value()?);
}
Ok(KclValue::UserVal(UserVal {
value: object.into(),
meta: vec![Metadata {
source_range: self.into(),
}],
}))
}
}
pub fn parse_json_number_as_u64(j: &serde_json::Value, source_range: SourceRange) -> Result<u64, KclError> {
if let serde_json::Value::Number(n) = &j {
n.as_u64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid integer: {}", j),
})
})
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid integer: {}", j),
}))
}
}
pub fn parse_json_number_as_f64(j: &serde_json::Value, source_range: SourceRange) -> Result<f64, KclError> {
if let serde_json::Value::Number(n) = &j {
n.as_f64().ok_or_else(|| {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid number: {}", j),
})
})
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Invalid number: {}", j),
}))
}
}
pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
if let serde_json::Value::String(n) = &j {
Some(n.clone())
} else {
None
}
}
/// JSON value as bool. If it isn't a bool, returns None.
pub fn json_as_bool(j: &serde_json::Value) -> Option<bool> {
match j {
JValue::Null => None,
JValue::Bool(b) => Some(*b),
JValue::Number(_) => None,
JValue::String(_) => None,
JValue::Array(_) => None,
JValue::Object(_) => None,
}
}

View File

@ -784,6 +784,9 @@ fn test_generate_stdlib_markdown_docs() {
#[test]
fn test_generate_stdlib_json_schema() {
// If this test fails and you've modified the AST or something else which affects the json repr
// of stdlib functions, you should rerun the test with `EXPECTORATE=overwrite` to create new
// test data, then check `/docs/kcl/std.json` to ensure the changes are expected.
let stdlib = StdLib::new();
let combined = stdlib.combined();

View File

@ -859,7 +859,7 @@ mod tests {
assert_eq!(
snippet,
r#"patternCircular3d({
repetitions: ${0:10},
instances: ${0:10},
axis: [${1:3.14}, ${2:3.14}, ${3:3.14}],
center: [${4:3.14}, ${5:3.14}, ${6:3.14}],
arcDegrees: ${7:3.14},
@ -921,7 +921,7 @@ mod tests {
assert_eq!(
snippet,
r#"patternLinear2d({
repetitions: ${0:10},
instances: ${0:10},
distance: ${1:3.14},
axis: [${2:3.14}, ${3:3.14}],
}, ${4:%})${}"#

View File

@ -24,6 +24,8 @@ use crate::{
executor::{DefaultPlanes, IdGenerator},
};
use super::ExecutionKind;
#[derive(Debug, PartialEq)]
enum SocketHealth {
Active,
@ -46,6 +48,8 @@ pub struct EngineConnection {
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
/// If the server sends session data, it'll be copied to here.
session_data: Arc<Mutex<Option<ModelingSessionData>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
}
pub struct TcpRead {
@ -300,6 +304,7 @@ impl EngineConnection {
batch_end: Arc::new(Mutex::new(IndexMap::new())),
default_planes: Default::default(),
session_data,
execution_kind: Default::default(),
})
}
}
@ -314,6 +319,18 @@ impl EngineManager for EngineConnection {
self.batch_end.clone()
}
fn execution_kind(&self) -> ExecutionKind {
let guard = self.execution_kind.lock().unwrap();
*guard
}
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
let mut guard = self.execution_kind.lock().unwrap();
let original = *guard;
*guard = execution_kind;
original
}
async fn default_planes(
&self,
id_generator: &mut IdGenerator,

View File

@ -22,10 +22,13 @@ use crate::{
executor::{DefaultPlanes, IdGenerator},
};
use super::ExecutionKind;
#[derive(Debug, Clone)]
pub struct EngineConnection {
batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
}
impl EngineConnection {
@ -33,6 +36,7 @@ impl EngineConnection {
Ok(EngineConnection {
batch: Arc::new(Mutex::new(Vec::new())),
batch_end: Arc::new(Mutex::new(IndexMap::new())),
execution_kind: Default::default(),
})
}
}
@ -47,6 +51,18 @@ impl crate::engine::EngineManager for EngineConnection {
self.batch_end.clone()
}
fn execution_kind(&self) -> ExecutionKind {
let guard = self.execution_kind.lock().unwrap();
*guard
}
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
let mut guard = self.execution_kind.lock().unwrap();
let original = *guard;
*guard = execution_kind;
original
}
async fn default_planes(
&self,
_id_generator: &mut IdGenerator,

View File

@ -9,6 +9,7 @@ use kittycad_modeling_cmds as kcmc;
use wasm_bindgen::prelude::*;
use crate::{
engine::ExecutionKind,
errors::{KclError, KclErrorDetails},
executor::{DefaultPlanes, IdGenerator},
};
@ -42,6 +43,7 @@ pub struct EngineConnection {
manager: Arc<EngineCommandManager>,
batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>,
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>,
execution_kind: Arc<Mutex<ExecutionKind>>,
}
// Safety: WebAssembly will only ever run in a single-threaded context.
@ -54,6 +56,7 @@ impl EngineConnection {
manager: Arc::new(manager),
batch: Arc::new(Mutex::new(Vec::new())),
batch_end: Arc::new(Mutex::new(IndexMap::new())),
execution_kind: Default::default(),
})
}
}
@ -68,6 +71,18 @@ impl crate::engine::EngineManager for EngineConnection {
self.batch_end.clone()
}
fn execution_kind(&self) -> ExecutionKind {
let guard = self.execution_kind.lock().unwrap();
*guard
}
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind {
let mut guard = self.execution_kind.lock().unwrap();
let original = *guard;
*guard = execution_kind;
original
}
async fn default_planes(
&self,
_id_generator: &mut IdGenerator,

View File

@ -41,6 +41,23 @@ lazy_static::lazy_static! {
pub static ref GRID_SCALE_TEXT_OBJECT_ID: uuid::Uuid = uuid::Uuid::parse_str("10782f33-f588-4668-8bcd-040502d26590").unwrap();
}
/// The mode of execution. When isolated, like during an import, attempting to
/// send a command results in an error.
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum ExecutionKind {
#[default]
Normal,
Isolated,
}
impl ExecutionKind {
pub fn is_isolated(&self) -> bool {
matches!(self, ExecutionKind::Isolated)
}
}
#[async_trait::async_trait]
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Get the batch of commands to be sent to the engine.
@ -49,6 +66,13 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Get the batch of end commands to be sent to the engine.
fn batch_end(&self) -> Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>;
/// Get the current execution kind.
fn execution_kind(&self) -> ExecutionKind;
/// Replace the current execution kind with a new value and return the
/// existing value.
fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind;
/// Get the default planes.
async fn default_planes(
&self,
@ -102,6 +126,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
source_range: crate::executor::SourceRange,
cmd: &ModelingCmd,
) -> Result<(), crate::errors::KclError> {
let execution_kind = self.execution_kind();
if execution_kind.is_isolated() {
return Err(KclError::Semantic(KclErrorDetails { message: "Cannot send modeling commands while importing. Wrap your code in a function if you want to import the file.".to_owned(), source_ranges: vec![source_range] }));
}
let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
cmd: cmd.clone(),
cmd_id: id.into(),

View File

@ -14,6 +14,8 @@ pub enum KclError {
Syntax(KclErrorDetails),
#[error("semantic: {0:?}")]
Semantic(KclErrorDetails),
#[error("import cycle: {0:?}")]
ImportCycle(KclErrorDetails),
#[error("type: {0:?}")]
Type(KclErrorDetails),
#[error("unimplemented: {0:?}")]
@ -52,6 +54,7 @@ impl KclError {
KclError::Lexical(_) => "lexical",
KclError::Syntax(_) => "syntax",
KclError::Semantic(_) => "semantic",
KclError::ImportCycle(_) => "import cycle",
KclError::Type(_) => "type",
KclError::Unimplemented(_) => "unimplemented",
KclError::Unexpected(_) => "unexpected",
@ -68,6 +71,7 @@ impl KclError {
KclError::Lexical(e) => e.source_ranges.clone(),
KclError::Syntax(e) => e.source_ranges.clone(),
KclError::Semantic(e) => e.source_ranges.clone(),
KclError::ImportCycle(e) => e.source_ranges.clone(),
KclError::Type(e) => e.source_ranges.clone(),
KclError::Unimplemented(e) => e.source_ranges.clone(),
KclError::Unexpected(e) => e.source_ranges.clone(),
@ -85,6 +89,7 @@ impl KclError {
KclError::Lexical(e) => &e.message,
KclError::Syntax(e) => &e.message,
KclError::Semantic(e) => &e.message,
KclError::ImportCycle(e) => &e.message,
KclError::Type(e) => &e.message,
KclError::Unimplemented(e) => &e.message,
KclError::Unexpected(e) => &e.message,
@ -102,6 +107,7 @@ impl KclError {
KclError::Lexical(e) => e.source_ranges = source_ranges,
KclError::Syntax(e) => e.source_ranges = source_ranges,
KclError::Semantic(e) => e.source_ranges = source_ranges,
KclError::ImportCycle(e) => e.source_ranges = source_ranges,
KclError::Type(e) => e.source_ranges = source_ranges,
KclError::Unimplemented(e) => e.source_ranges = source_ranges,
KclError::Unexpected(e) => e.source_ranges = source_ranges,
@ -121,6 +127,7 @@ impl KclError {
KclError::Lexical(e) => e.source_ranges.extend(source_ranges),
KclError::Syntax(e) => e.source_ranges.extend(source_ranges),
KclError::Semantic(e) => e.source_ranges.extend(source_ranges),
KclError::ImportCycle(e) => e.source_ranges.extend(source_ranges),
KclError::Type(e) => e.source_ranges.extend(source_ranges),
KclError::Unimplemented(e) => e.source_ranges.extend(source_ranges),
KclError::Unexpected(e) => e.source_ranges.extend(source_ranges),

View File

@ -1,6 +1,9 @@
//! The executor for the AST.
use std::{collections::HashMap, sync::Arc};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use anyhow::Result;
use async_recursion::async_recursion;
@ -23,12 +26,12 @@ type Point3D = kcmc::shared::Point3d<f64>;
use crate::{
ast::types::{
human_friendly_type, BodyItem, Expr, ExpressionStatement, FunctionExpression, KclNone, Program,
ReturnStatement, TagDeclarator,
human_friendly_type, BodyItem, Expr, ExpressionStatement, FunctionExpression, ImportStatement, ItemVisibility,
KclNone, Program, ReturnStatement, TagDeclarator,
},
engine::EngineManager,
engine::{EngineManager, ExecutionKind},
errors::{KclError, KclErrorDetails},
fs::FileManager,
fs::{FileManager, FileSystem},
settings::types::UnitLength,
std::{FnAsArg, StdLib},
};
@ -47,6 +50,14 @@ pub struct ExecState {
/// The current value of the pipe operator returned from the previous
/// expression. If we're not currently in a pipeline, this will be None.
pub pipe_value: Option<KclValue>,
/// Identifiers that have been exported from the current module.
pub module_exports: HashSet<String>,
/// The stack of import statements for detecting circular module imports.
/// If this is empty, we're not currently executing an import statement.
pub import_stack: Vec<std::path::PathBuf>,
/// The directory of the current project. This is used for resolving import
/// paths. If None is given, the current working directory is used.
pub project_directory: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -391,6 +402,20 @@ impl KclValue {
KclValue::Face(_) => "Face",
}
}
pub(crate) fn is_function(&self) -> bool {
match self {
KclValue::UserVal(..)
| KclValue::TagIdentifier(..)
| KclValue::TagDeclarator(..)
| KclValue::Plane(..)
| KclValue::Face(..)
| KclValue::Solid(..)
| KclValue::Solids { .. }
| KclValue::ImportedGeometry(..) => false,
KclValue::Function { .. } => true,
}
}
}
impl From<SketchSet> for KclValue {
@ -452,6 +477,15 @@ pub enum Geometries {
Solids(Vec<Box<Solid>>),
}
impl From<Geometry> for Geometries {
fn from(value: Geometry) -> Self {
match value {
Geometry::Sketch(x) => Self::Sketches(vec![x]),
Geometry::Solid(x) => Self::Solids(vec![x]),
}
}
}
/// A sketch or a group of sketches.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -1495,6 +1529,14 @@ impl From<SourceRange> for Metadata {
}
}
impl From<&ImportStatement> for Metadata {
fn from(stmt: &ImportStatement) -> Self {
Self {
source_range: SourceRange::new(stmt.start, stmt.end),
}
}
}
impl From<&ExpressionStatement> for Metadata {
fn from(exp_statement: &ExpressionStatement) -> Self {
Self {
@ -1958,8 +2000,9 @@ impl ExecutorContext {
program: &crate::ast::types::Program,
memory: Option<ProgramMemory>,
id_generator: IdGenerator,
project_directory: Option<String>,
) -> Result<ExecState, KclError> {
self.run_with_session_data(program, memory, id_generator)
self.run_with_session_data(program, memory, id_generator, project_directory)
.await
.map(|x| x.0)
}
@ -1971,6 +2014,7 @@ impl ExecutorContext {
program: &crate::ast::types::Program,
memory: Option<ProgramMemory>,
id_generator: IdGenerator,
project_directory: Option<String>,
) -> Result<(ExecState, Option<ModelingSessionData>), KclError> {
let memory = if let Some(memory) = memory {
memory.clone()
@ -1980,6 +2024,7 @@ impl ExecutorContext {
let mut exec_state = ExecState {
memory,
id_generator,
project_directory,
..Default::default()
};
// Before we even start executing the program, set the units.
@ -2018,6 +2063,91 @@ impl ExecutorContext {
// Iterate over the body of the program.
for statement in &program.body {
match statement {
BodyItem::ImportStatement(import_stmt) => {
let source_range = SourceRange::from(import_stmt);
let path = import_stmt.path.clone();
let resolved_path = if let Some(project_dir) = &exec_state.project_directory {
std::path::PathBuf::from(project_dir).join(&path)
} else {
std::path::PathBuf::from(&path)
};
if exec_state.import_stack.contains(&resolved_path) {
return Err(KclError::ImportCycle(KclErrorDetails {
message: format!(
"circular import of modules is not allowed: {} -> {}",
exec_state
.import_stack
.iter()
.map(|p| p.as_path().to_string_lossy())
.collect::<Vec<_>>()
.join(" -> "),
resolved_path.to_string_lossy()
),
source_ranges: vec![import_stmt.into()],
}));
}
let source = self.fs.read_to_string(&resolved_path, source_range).await?;
let program = crate::parser::parse(&source)?;
let (module_memory, module_exports) = {
exec_state.import_stack.push(resolved_path.clone());
let original_execution = self.engine.replace_execution_kind(ExecutionKind::Isolated);
let original_memory = std::mem::take(&mut exec_state.memory);
let original_exports = std::mem::take(&mut exec_state.module_exports);
let result = self
.inner_execute(&program, exec_state, crate::executor::BodyType::Root)
.await;
let module_exports = std::mem::replace(&mut exec_state.module_exports, original_exports);
let module_memory = std::mem::replace(&mut exec_state.memory, original_memory);
self.engine.replace_execution_kind(original_execution);
exec_state.import_stack.pop();
result.map_err(|err| {
if let KclError::ImportCycle(_) = err {
// It was an import cycle. Keep the original message.
err.override_source_ranges(vec![source_range])
} else {
KclError::Semantic(KclErrorDetails {
message: format!(
"Error loading imported file. Open it to view more details. {path}: {}",
err.message()
),
source_ranges: vec![source_range],
})
}
})?;
(module_memory, module_exports)
};
for import_item in &import_stmt.items {
// Extract the item from the module.
let item = module_memory
.get(&import_item.name.name, import_item.into())
.map_err(|_err| {
KclError::UndefinedValue(KclErrorDetails {
message: format!("{} is not defined in module", import_item.name.name),
source_ranges: vec![SourceRange::from(&import_item.name)],
})
})?;
// Check that the item is allowed to be imported.
if !module_exports.contains(&import_item.name.name) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
import_item.name.name
),
source_ranges: vec![SourceRange::from(&import_item.name)],
}));
}
// Add the item to the current module.
exec_state.memory.add(
import_item.identifier(),
item.clone(),
SourceRange::from(&import_item.name),
)?;
}
last_expr = None;
}
BodyItem::ExpressionStatement(expression_statement) => {
let metadata = Metadata::from(expression_statement);
last_expr = Some(
@ -2044,7 +2174,21 @@ impl ExecutorContext {
StatementKind::Declaration { name: &var_name },
)
.await?;
let is_function = memory_item.is_function();
exec_state.memory.add(&var_name, memory_item, source_range)?;
// Track exports.
match variable_declaration.visibility {
ItemVisibility::Export => {
if !is_function {
return Err(KclError::Semantic(KclErrorDetails {
message: "Only functions can be exported".to_owned(),
source_ranges: vec![source_range],
}));
}
exec_state.module_exports.insert(var_name);
}
ItemVisibility::Default => {}
}
}
last_expr = None;
}
@ -2130,6 +2274,7 @@ impl ExecutorContext {
},
},
Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?,
Expr::ArrayRangeExpression(range_expression) => range_expression.execute(exec_state, self).await?,
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?,
@ -2148,8 +2293,9 @@ impl ExecutorContext {
&self,
program: &Program,
id_generator: IdGenerator,
project_directory: Option<String>,
) -> Result<TakeSnapshot> {
let _ = self.run(program, None, id_generator).await?;
let _ = self.run(program, None, id_generator, project_directory).await?;
// Zoom to fit.
self.engine
@ -2294,7 +2440,7 @@ mod tests {
settings: Default::default(),
context_type: ContextType::Mock,
};
let exec_state = ctx.run(&program, None, IdGenerator::default()).await?;
let exec_state = ctx.run(&program, None, IdGenerator::default(), None).await?;
Ok(exec_state.memory)
}

View File

@ -37,6 +37,19 @@ impl FileSystem for FileManager {
})
}
async fn read_to_string<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<String, KclError> {
tokio::fs::read_to_string(&path).await.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to read file `{}`: {}", path.as_ref().display(), e),
source_ranges: vec![source_range],
})
})
}
async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self,
path: P,

View File

@ -23,6 +23,13 @@ pub trait FileSystem: Clone {
source_range: crate::executor::SourceRange,
) -> Result<Vec<u8>, crate::errors::KclError>;
/// Read a file from the local file system.
async fn read_to_string<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<String, crate::errors::KclError>;
/// Check if a file exists on the local file system.
async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self,

View File

@ -78,6 +78,22 @@ impl FileSystem for FileManager {
Ok(bytes)
}
async fn read_to_string<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self,
path: P,
source_range: crate::executor::SourceRange,
) -> Result<String, KclError> {
let bytes = self.read(path, source_range).await?;
let string = String::from_utf8(bytes).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to convert bytes to string: {:?}", e),
source_ranges: vec![source_range],
})
})?;
Ok(string)
}
async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
&self,
path: P,

View File

@ -596,7 +596,7 @@ impl Backend {
.clear_scene(&mut id_generator, SourceRange::default())
.await?;
let exec_state = match executor_ctx.run(ast, None, id_generator).await {
let exec_state = match executor_ctx.run(ast, None, id_generator, None).await {
Ok(exec_state) => exec_state,
Err(err) => {
self.memory_map.remove(params.uri.as_str());
@ -1123,7 +1123,7 @@ impl LanguageServer for Backend {
return Ok(None);
};
let Some(value) = ast.get_value_for_position(pos) else {
let Some(value) = ast.get_expr_for_position(pos) else {
return Ok(None);
};

View File

@ -12,6 +12,13 @@ pub(crate) mod parser_impl;
pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%";
pub const PIPE_OPERATOR: &str = "|>";
/// Parse the given KCL code into an AST.
pub fn parse(code: &str) -> Result<Program, KclError> {
let tokens = crate::token::lexer(code)?;
let parser = Parser::new(tokens);
parser.ast()
}
pub struct Parser {
pub tokens: Vec<Token>,
pub unknown_tokens: Vec<Token>,

View File

@ -10,12 +10,12 @@ use winnow::{
use crate::{
ast::types::{
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, ValueMeta, VariableDeclaration, VariableDeclarator,
VariableKind,
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier,
IfExpression, ImportItem, ImportStatement, ItemVisibility, Literal, LiteralIdentifier, LiteralValue,
MemberExpression, MemberObject, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty,
Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement, TagDeclarator, UnaryExpression,
UnaryOperator, ValueMeta, VariableDeclaration, VariableDeclarator, VariableKind,
},
errors::{KclError, KclErrorDetails},
executor::SourceRange,
@ -303,6 +303,12 @@ fn binary_operator(i: TokenSlice) -> PResult<BinaryOperator> {
"*" => BinaryOperator::Mul,
"%" => BinaryOperator::Mod,
"^" => BinaryOperator::Pow,
"==" => BinaryOperator::Eq,
"!=" => BinaryOperator::Neq,
">" => BinaryOperator::Gt,
">=" => BinaryOperator::Gte,
"<" => BinaryOperator::Lt,
"<=" => BinaryOperator::Lte,
_ => {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
@ -330,6 +336,7 @@ fn operand(i: TokenSlice) -> PResult<BinaryPart> {
| Expr::PipeExpression(_)
| Expr::PipeSubstitution(_)
| Expr::ArrayExpression(_)
| Expr::ArrayRangeExpression(_)
| Expr::ObjectExpression(_) => {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges,
@ -460,8 +467,13 @@ pub enum NonCodeOr<T> {
}
/// Parse a KCL array of elements.
fn array(i: TokenSlice) -> PResult<ArrayExpression> {
alt((array_empty, array_elem_by_elem, array_end_start)).parse_next(i)
fn array(i: TokenSlice) -> PResult<Expr> {
alt((
array_empty.map(Box::new).map(Expr::ArrayExpression),
array_elem_by_elem.map(Box::new).map(Expr::ArrayExpression),
array_end_start.map(Box::new).map(Expr::ArrayRangeExpression),
))
.parse_next(i)
}
/// Match an empty array.
@ -533,44 +545,29 @@ pub(crate) fn array_elem_by_elem(i: TokenSlice) -> PResult<ArrayExpression> {
})
}
fn array_end_start(i: TokenSlice) -> PResult<ArrayExpression> {
fn array_end_start(i: TokenSlice) -> PResult<ArrayRangeExpression> {
let start = open_bracket(i)?.start;
ignore_whitespace(i);
let elements = integer_range
.context(expected("array contents, a numeric range (like 0..10)"))
.parse_next(i)?;
let start_element = Box::new(expression.parse_next(i)?);
ignore_whitespace(i);
double_period.parse_next(i)?;
ignore_whitespace(i);
let end_element = Box::new(expression.parse_next(i)?);
ignore_whitespace(i);
let end = close_bracket(i)?.end;
Ok(ArrayExpression {
Ok(ArrayRangeExpression {
start,
end,
elements,
non_code_meta: Default::default(),
start_element,
end_element,
end_inclusive: true,
digest: None,
})
}
/// Parse n..m into a vec of numbers [n, n+1, ..., m-1]
fn integer_range(i: TokenSlice) -> PResult<Vec<Expr>> {
let (token0, floor) = integer.parse_next(i)?;
double_period.parse_next(i)?;
let (_token1, ceiling) = integer.parse_next(i)?;
Ok((floor..=ceiling)
.map(|num| {
let num = num as i64;
Expr::Literal(Box::new(Literal {
start: token0.start,
end: token0.end,
value: num.into(),
raw: num.to_string(),
digest: None,
}))
})
.collect())
}
fn object_property(i: TokenSlice) -> PResult<ObjectProperty> {
let key = identifier.context(expected("the property's key (the name or identifier of the property), e.g. in 'height: 4', 'height' is the property key")).parse_next(i)?;
ignore_whitespace(i);
colon
.context(expected(
"a colon, which separates the property's key from the value you're setting it to, e.g. 'height: 4'",
@ -959,8 +956,10 @@ fn body_items_within_function(i: TokenSlice) -> PResult<WithinFunction> {
// Any of the body item variants, each of which can optionally be followed by a comment.
// If there is a comment, it may be preceded by whitespace.
let item = dispatch! {peek(any);
token if token.declaration_keyword().is_some() =>
token if token.declaration_keyword().is_some() || token.visibility_keyword().is_some() =>
(declaration.map(BodyItem::VariableDeclaration), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
token if token.value == "import" && matches!(token.token_type, TokenType::Keyword) =>
(import_stmt.map(BodyItem::ImportStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
Token { ref value, .. } if value == "return" =>
(return_stmt.map(BodyItem::ReturnStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
token if !token.is_code_token() => {
@ -1125,6 +1124,111 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> {
})
}
fn import_stmt(i: TokenSlice) -> PResult<Box<ImportStatement>> {
let import_token = any
.try_map(|token: Token| {
if matches!(token.token_type, TokenType::Keyword) && token.value == "import" {
Ok(token)
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{} is not the 'import' keyword", token.value.as_str()),
}))
}
})
.context(expected("the 'import' keyword"))
.parse_next(i)?;
let start = import_token.start;
require_whitespace(i)?;
let items = separated(1.., import_item, comma_sep)
.parse_next(i)
.map_err(|e| e.cut())?;
require_whitespace(i)?;
any.try_map(|token: Token| {
if matches!(token.token_type, TokenType::Keyword | TokenType::Word) && token.value == "from" {
Ok(())
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{} is not the 'from' keyword", token.value.as_str()),
}))
}
})
.context(expected("the 'from' keyword"))
.parse_next(i)
.map_err(|e| e.cut())?;
require_whitespace(i)?;
let path = string_literal(i)?;
let end = path.end();
let path_string = match path.value {
LiteralValue::String(s) => s,
_ => unreachable!(),
};
if path_string
.chars()
.any(|c| !c.is_ascii_alphanumeric() && c != '_' && c != '-' && c != '.')
{
return Err(ErrMode::Cut(
KclError::Syntax(KclErrorDetails {
source_ranges: vec![SourceRange::new(path.start, path.end)],
message: "import path may only contain alphanumeric characters, underscore, hyphen, and period. Files in other directories are not yet supported.".to_owned(),
})
.into(),
));
}
Ok(Box::new(ImportStatement {
items,
path: path_string,
raw_path: path.raw,
start,
end,
digest: None,
}))
}
fn import_item(i: TokenSlice) -> PResult<ImportItem> {
let name = identifier.context(expected("an identifier to import")).parse_next(i)?;
let start = name.start;
let alias = opt(preceded(
(whitespace, import_as_keyword, whitespace),
identifier.context(expected("an identifier to alias the import")),
))
.parse_next(i)?;
let end = if let Some(ref alias) = alias {
alias.end()
} else {
name.end()
};
Ok(ImportItem {
name,
alias,
start,
end,
digest: None,
})
}
fn import_as_keyword(i: TokenSlice) -> PResult<Token> {
any.try_map(|token: Token| {
if matches!(token.token_type, TokenType::Keyword | TokenType::Word) && token.value == "as" {
Ok(token)
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{} is not the 'as' keyword", token.value.as_str()),
}))
}
})
.context(expected("the 'as' keyword"))
.parse_next(i)
}
/// Parse a return statement of a user-defined function, e.g. `return x`.
pub fn return_stmt(i: TokenSlice) -> PResult<ReturnStatement> {
let start = any
@ -1189,7 +1293,7 @@ fn expr_allowed_in_pipe_expr(i: TokenSlice) -> PResult<Expr> {
literal.map(Box::new).map(Expr::Literal),
fn_call.map(Box::new).map(Expr::CallExpression),
identifier.map(Box::new).map(Expr::Identifier),
array.map(Box::new).map(Expr::ArrayExpression),
array,
object.map(Box::new).map(Expr::ObjectExpression),
pipe_sub.map(Box::new).map(Expr::PipeSubstitution),
function_expression.map(Box::new).map(Expr::FunctionExpression),
@ -1217,6 +1321,19 @@ fn possible_operands(i: TokenSlice) -> PResult<Expr> {
.parse_next(i)
}
/// Parse an item visibility specifier, e.g. export.
fn item_visibility(i: TokenSlice) -> PResult<(ItemVisibility, Token)> {
any.verify_map(|token: Token| {
if token.token_type == TokenType::Keyword && token.value == "export" {
Some((ItemVisibility::Export, token))
} else {
None
}
})
.context(expected("item visibility, e.g. 'export'"))
.parse_next(i)
}
fn declaration_keyword(i: TokenSlice) -> PResult<(VariableKind, Token)> {
let res = any
.verify_map(|token: Token| token.declaration_keyword().map(|kw| (kw, token)))
@ -1226,6 +1343,9 @@ fn declaration_keyword(i: TokenSlice) -> PResult<(VariableKind, Token)> {
/// Parse a variable/constant declaration.
fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
let (visibility, visibility_token) = opt(terminated(item_visibility, whitespace))
.parse_next(i)?
.map_or((ItemVisibility::Default, None), |pair| (pair.0, Some(pair.1)));
let decl_token = opt(declaration_keyword).parse_next(i)?;
if decl_token.is_some() {
// If there was a declaration keyword like `fn`, then it must be followed by some spaces.
@ -1238,11 +1358,14 @@ fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
"an identifier, which becomes name you're binding the value to",
))
.parse_next(i)?;
let (kind, start, dec_end) = if let Some((kind, token)) = &decl_token {
let (kind, mut start, dec_end) = if let Some((kind, token)) = &decl_token {
(*kind, token.start, token.end)
} else {
(VariableKind::Const, id.start(), id.end())
};
if let Some(token) = visibility_token {
start = token.start;
}
ignore_whitespace(i);
equals(i)?;
@ -1291,6 +1414,7 @@ fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
init: val,
digest: None,
}],
visibility,
kind,
digest: None,
})
@ -1505,25 +1629,6 @@ fn expression_stmt(i: TokenSlice) -> PResult<ExpressionStatement> {
})
}
/// Parse a KCL integer, and the token that held it.
fn integer(i: TokenSlice) -> PResult<(Token, u64)> {
let num = one_of(TokenType::Number)
.context(expected("a number token e.g. 3"))
.try_map(|token: Token| {
let source_ranges = token.as_source_ranges();
let value = token.value.clone();
token.value.parse().map(|num| (token, num)).map_err(|e| {
KclError::Syntax(KclErrorDetails {
source_ranges,
message: format!("invalid integer {value}: {e}"),
})
})
})
.context(expected("an integer e.g. 3 (but not 3.1)"))
.parse_next(i)?;
Ok(num)
}
/// Parse the given brace symbol.
fn some_brace(symbol: &'static str, i: TokenSlice) -> PResult<Token> {
one_of((TokenType::Brace, symbol))
@ -3054,123 +3159,6 @@ e
}
}
#[test]
fn test_parse_expand_array() {
let code = "const myArray = [0..10]";
let parser = crate::parser::Parser::new(crate::token::lexer(code).unwrap());
let result = parser.ast().unwrap();
let expected_result = Program {
start: 0,
end: 23,
body: vec![BodyItem::VariableDeclaration(VariableDeclaration {
start: 0,
end: 23,
declarations: vec![VariableDeclarator {
start: 6,
end: 23,
id: Identifier {
start: 6,
end: 13,
name: "myArray".to_string(),
digest: None,
},
init: Expr::ArrayExpression(Box::new(ArrayExpression {
start: 16,
end: 23,
non_code_meta: Default::default(),
elements: vec![
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 0u32.into(),
raw: "0".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 1u32.into(),
raw: "1".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 2u32.into(),
raw: "2".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 3u32.into(),
raw: "3".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 4u32.into(),
raw: "4".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 5u32.into(),
raw: "5".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 6u32.into(),
raw: "6".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 7u32.into(),
raw: "7".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 8u32.into(),
raw: "8".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 9u32.into(),
raw: "9".to_string(),
digest: None,
})),
Expr::Literal(Box::new(Literal {
start: 17,
end: 18,
value: 10u32.into(),
raw: "10".to_string(),
digest: None,
})),
],
digest: None,
})),
digest: None,
}],
kind: VariableKind::Const,
digest: None,
})],
non_code_meta: NonCodeMeta::default(),
digest: None,
};
assert_eq!(result, expected_result);
}
#[test]
fn test_error_keyword_in_variable() {
let some_program_string = r#"const let = "thing""#;
@ -3705,7 +3693,10 @@ const my14 = 4 ^ 2 - 3 ^ 2 * 2
5
}"#
);
snapshot_test!(be, "let x = 3 == 3");
snapshot_test!(bf, "let x = 3 != 3");
snapshot_test!(bg, r#"x = 4"#);
snapshot_test!(bh, "const obj = {center : [10, 10], radius: 5}");
}
#[allow(unused)]

View File

@ -24,111 +24,29 @@ expression: actual
"digest": null
},
"init": {
"type": "ArrayExpression",
"type": "ArrayExpression",
"type": "ArrayRangeExpression",
"type": "ArrayRangeExpression",
"start": 16,
"end": 23,
"elements": [
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 0,
"raw": "0",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 1,
"raw": "1",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 2,
"raw": "2",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 3,
"raw": "3",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 4,
"raw": "4",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 5,
"raw": "5",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 6,
"raw": "6",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 7,
"raw": "7",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 8,
"raw": "8",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 9,
"raw": "9",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 10,
"raw": "10",
"digest": null
}
],
"startElement": {
"type": "Literal",
"type": "Literal",
"start": 17,
"end": 18,
"value": 0,
"raw": "0",
"digest": null
},
"endElement": {
"type": "Literal",
"type": "Literal",
"start": 20,
"end": 22,
"value": 10,
"raw": "10",
"digest": null
},
"endInclusive": true,
"digest": null
},
"digest": null

View File

@ -1,6 +1,5 @@
---
source: kcl/src/parser/parser_impl.rs
assertion_line: 3423
expression: actual
---
{

View File

@ -1,6 +1,5 @@
---
source: kcl/src/parser/parser_impl.rs
assertion_line: 3470
expression: actual
---
{

View File

@ -0,0 +1,65 @@
---
source: kcl/src/parser/parser_impl.rs
expression: actual
---
{
"start": 0,
"end": 14,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 14,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 14,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "x",
"digest": null
},
"init": {
"type": "BinaryExpression",
"type": "BinaryExpression",
"start": 8,
"end": 14,
"operator": "==",
"left": {
"type": "Literal",
"type": "Literal",
"start": 8,
"end": 9,
"value": 3,
"raw": "3",
"digest": null
},
"right": {
"type": "Literal",
"type": "Literal",
"start": 13,
"end": 14,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
},
"digest": null
}
],
"kind": "const",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -0,0 +1,65 @@
---
source: kcl/src/parser/parser_impl.rs
expression: actual
---
{
"start": 0,
"end": 14,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 14,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 14,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "x",
"digest": null
},
"init": {
"type": "BinaryExpression",
"type": "BinaryExpression",
"start": 8,
"end": 14,
"operator": "!=",
"left": {
"type": "Literal",
"type": "Literal",
"start": 8,
"end": 9,
"value": 3,
"raw": "3",
"digest": null
},
"right": {
"type": "Literal",
"type": "Literal",
"start": 13,
"end": 14,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
},
"digest": null
}
],
"kind": "const",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -0,0 +1,111 @@
---
source: kcl/src/parser/parser_impl.rs
assertion_line: 3718
expression: actual
---
{
"start": 0,
"end": 42,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 42,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 42,
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"name": "obj",
"digest": null
},
"init": {
"type": "ObjectExpression",
"type": "ObjectExpression",
"start": 12,
"end": 42,
"properties": [
{
"type": "ObjectProperty",
"start": 13,
"end": 30,
"key": {
"type": "Identifier",
"start": 13,
"end": 19,
"name": "center",
"digest": null
},
"value": {
"type": "ArrayExpression",
"type": "ArrayExpression",
"start": 22,
"end": 30,
"elements": [
{
"type": "Literal",
"type": "Literal",
"start": 23,
"end": 25,
"value": 10,
"raw": "10",
"digest": null
},
{
"type": "Literal",
"type": "Literal",
"start": 27,
"end": 29,
"value": 10,
"raw": "10",
"digest": null
}
],
"digest": null
},
"digest": null
},
{
"type": "ObjectProperty",
"start": 32,
"end": 41,
"key": {
"type": "Identifier",
"start": 32,
"end": 38,
"name": "radius",
"digest": null
},
"value": {
"type": "Literal",
"type": "Literal",
"start": 40,
"end": 41,
"value": 5,
"raw": "5",
"digest": null
},
"digest": null
}
],
"digest": null
},
"digest": null
}
],
"kind": "const",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -7,7 +7,7 @@ use serde::de::DeserializeOwned;
use serde_json::Value as JValue;
use crate::{
ast::types::{parse_json_number_as_f64, TagDeclarator},
ast::types::{execute::parse_json_number_as_f64, TagDeclarator},
errors::{KclError, KclErrorDetails},
executor::{
ExecState, ExecutorContext, ExtrudeSurface, KclValue, Metadata, Sketch, SketchSet, SketchSurface, Solid,

View File

@ -28,6 +28,18 @@ pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
memory: *f.memory,
};
let new_array = inner_map(array, map_fn, exec_state, &args).await?;
let unwrapped = new_array
.clone()
.into_iter()
.map(|k| match k {
KclValue::UserVal(user_val) => Ok(user_val.value),
_ => Err(()),
})
.collect::<Result<Vec<_>, _>>();
if let Ok(unwrapped) = unwrapped {
let uv = UserVal::new(vec![args.source_range.into()], unwrapped);
return Ok(KclValue::UserVal(uv));
}
let uv = UserVal::new(vec![args.source_range.into()], new_array);
Ok(KclValue::UserVal(uv))
}

View File

@ -1,5 +1,7 @@
//! Standard library patterns.
use std::cmp::Ordering;
use anyhow::Result;
use derive_docs::stdlib;
use kcmc::{
@ -23,15 +25,18 @@ use crate::{
std::{types::Uint, Args},
};
const MUST_HAVE_ONE_INSTANCE: &str = "There must be at least 1 instance of your geometry";
/// Data for a linear pattern on a 2D sketch.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct LinearPattern2dData {
/// The number of repetitions. Must be greater than 0.
/// This excludes the original entity. For example, if `repetitions` is 1,
/// the original entity will be copied once.
pub repetitions: Uint,
/// The number of total instances. Must be greater than or equal to 1.
/// This includes the original entity. For example, if instances is 2,
/// there will be two copies -- the original, and one new copy.
/// If instances is 1, this has no effect.
pub instances: Uint,
/// The distance between each repetition. This can also be referred to as spacing.
pub distance: f64,
/// The axis of the pattern. This is a 2D vector.
@ -43,10 +48,11 @@ pub struct LinearPattern2dData {
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct LinearPattern3dData {
/// The number of repetitions. Must be greater than 0.
/// This excludes the original entity. For example, if `repetitions` is 1,
/// the original entity will be copied once.
pub repetitions: Uint,
/// The number of total instances. Must be greater than or equal to 1.
/// This includes the original entity. For example, if instances is 2,
/// there will be two copies -- the original, and one new copy.
/// If instances is 1, this has no effect.
pub instances: Uint,
/// The distance between each repetition. This can also be referred to as spacing.
pub distance: f64,
/// The axis of the pattern.
@ -66,11 +72,12 @@ impl LinearPattern {
}
}
pub fn repetitions(&self) -> u32 {
match self {
LinearPattern::TwoD(lp) => lp.repetitions.u32(),
LinearPattern::ThreeD(lp) => lp.repetitions.u32(),
}
fn repetitions(&self) -> RepetitionsNeeded {
let n = match self {
LinearPattern::TwoD(lp) => lp.instances.u32(),
LinearPattern::ThreeD(lp) => lp.instances.u32(),
};
RepetitionsNeeded::from(n)
}
pub fn distance(&self) -> f64 {
@ -278,6 +285,12 @@ async fn inner_pattern_transform<'a>(
) -> Result<Vec<Box<Solid>>, KclError> {
// Build the vec of transforms, one for each repetition.
let mut transform = Vec::with_capacity(usize::try_from(total_instances).unwrap());
if total_instances < 1 {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![args.source_range],
message: MUST_HAVE_ONE_INSTANCE.to_owned(),
}));
}
for i in 1..total_instances {
let t = make_transform(i, &transform_function, args.source_range, exec_state).await?;
transform.push(t);
@ -498,7 +511,7 @@ pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result
/// |> circle({ center: [0, 0], radius: 1 }, %)
/// |> patternLinear2d({
/// axis: [1, 0],
/// repetitions: 6,
/// instances: 7,
/// distance: 4
/// }, %)
///
@ -573,7 +586,7 @@ pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result
/// const example = extrude(1, exampleSketch)
/// |> patternLinear3d({
/// axis: [1, 0, 1],
/// repetitions: 6,
/// instances: 7,
/// distance: 6
/// }, %)
/// ```
@ -629,13 +642,26 @@ async fn pattern_linear(
) -> Result<Geometries, KclError> {
let id = exec_state.id_generator.next_uuid();
let num_repetitions = match data.repetitions() {
RepetitionsNeeded::More(n) => n,
RepetitionsNeeded::None => {
return Ok(Geometries::from(geometry));
}
RepetitionsNeeded::Invalid => {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![args.source_range],
message: MUST_HAVE_ONE_INSTANCE.to_owned(),
}));
}
};
let resp = args
.send_modeling_cmd(
id,
ModelingCmd::from(mcmd::EntityLinearPattern {
axis: kcmc::shared::Point3d::from(data.axis()),
entity_id: geometry.id(),
num_repetitions: data.repetitions(),
num_repetitions,
spacing: LengthUnit(data.distance()),
}),
)
@ -680,10 +706,11 @@ async fn pattern_linear(
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct CircularPattern2dData {
/// The number of repetitions. Must be greater than 0.
/// This excludes the original entity. For example, if `repetitions` is 1,
/// the original entity will be copied once.
pub repetitions: Uint,
/// The number of total instances. Must be greater than or equal to 1.
/// This includes the original entity. For example, if instances is 2,
/// there will be two copies -- the original, and one new copy.
/// If instances is 1, this has no effect.
pub instances: Uint,
/// The center about which to make the pattern. This is a 2D vector.
pub center: [f64; 2],
/// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
@ -697,10 +724,11 @@ pub struct CircularPattern2dData {
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct CircularPattern3dData {
/// The number of repetitions. Must be greater than 0.
/// This excludes the original entity. For example, if `repetitions` is 1,
/// the original entity will be copied once.
pub repetitions: Uint,
/// The number of total instances. Must be greater than or equal to 1.
/// This includes the original entity. For example, if instances is 2,
/// there will be two copies -- the original, and one new copy.
/// If instances is 1, this has no effect.
pub instances: Uint,
/// The axis around which to make the pattern. This is a 3D vector.
pub axis: [f64; 3],
/// The center about which to make the pattern. This is a 3D vector.
@ -716,6 +744,25 @@ pub enum CircularPattern {
TwoD(CircularPattern2dData),
}
enum RepetitionsNeeded {
/// Add this number of repetitions
More(u32),
/// No repetitions needed
None,
/// Invalid number of total instances.
Invalid,
}
impl From<u32> for RepetitionsNeeded {
fn from(n: u32) -> Self {
match n.cmp(&1) {
Ordering::Less => Self::Invalid,
Ordering::Equal => Self::None,
Ordering::Greater => Self::More(n - 1),
}
}
}
impl CircularPattern {
pub fn axis(&self) -> [f64; 3] {
match self {
@ -731,11 +778,12 @@ impl CircularPattern {
}
}
pub fn repetitions(&self) -> u32 {
match self {
CircularPattern::TwoD(lp) => lp.repetitions.u32(),
CircularPattern::ThreeD(lp) => lp.repetitions.u32(),
}
fn repetitions(&self) -> RepetitionsNeeded {
let n = match self {
CircularPattern::TwoD(lp) => lp.instances.u32(),
CircularPattern::ThreeD(lp) => lp.instances.u32(),
};
RepetitionsNeeded::from(n)
}
pub fn arc_degrees(&self) -> f64 {
@ -775,7 +823,7 @@ pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Resu
/// |> close(%)
/// |> patternCircular2d({
/// center: [0, 0],
/// repetitions: 12,
/// instances: 13,
/// arcDegrees: 360,
/// rotateDuplicates: true
/// }, %)
@ -841,7 +889,7 @@ pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Resu
/// |> patternCircular3d({
/// axis: [1, -1, 0],
/// center: [10, -20, 0],
/// repetitions: 10,
/// instances: 11,
/// arcDegrees: 360,
/// rotateDuplicates: true
/// }, %)
@ -897,6 +945,18 @@ async fn pattern_circular(
args: Args,
) -> Result<Geometries, KclError> {
let id = exec_state.id_generator.next_uuid();
let num_repetitions = match data.repetitions() {
RepetitionsNeeded::More(n) => n,
RepetitionsNeeded::None => {
return Ok(Geometries::from(geometry));
}
RepetitionsNeeded::Invalid => {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: vec![args.source_range],
message: MUST_HAVE_ONE_INSTANCE.to_owned(),
}));
}
};
let center = data.center();
let resp = args
@ -910,7 +970,7 @@ async fn pattern_circular(
y: LengthUnit(center[1]),
z: LengthUnit(center[2]),
},
num_repetitions: data.repetitions(),
num_repetitions,
arc_degrees: data.arc_degrees(),
rotate_duplicates: data.rotate_duplicates(),
}),

View File

@ -30,7 +30,7 @@ async fn do_execute_and_snapshot(ctx: &ExecutorContext, code: &str) -> anyhow::R
let program = parser.ast()?;
let snapshot = ctx
.execute_and_prepare_snapshot(&program, IdGenerator::default())
.execute_and_prepare_snapshot(&program, IdGenerator::default(), None)
.await?;
// Create a temporary file to write the output to.

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