Compare commits
22 Commits
import-fix
...
kcl-0.2.22
Author | SHA1 | Date | |
---|---|---|---|
0255fde5fe | |||
ebade29ed0 | |||
582d37e51b | |||
4ef9429842 | |||
0577b6a984 | |||
7d44de0c12 | |||
f7d5313588 | |||
bd4783e885 | |||
8794696b26 | |||
1c2e415c70 | |||
248ef8ebb3 | |||
fbac9935fe | |||
b4c171a347 | |||
0811d9fa4e | |||
1efc2b9762 | |||
d361bda180 | |||
1d3ade114f | |||
3382b66075 | |||
5e8b5c254d | |||
b99b2d9a96 | |||
81041661c7 | |||
9d99b5be7f |
13
.github/workflows/static-analysis.yml
vendored
@ -37,10 +37,6 @@ jobs:
|
|||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- run: yarn install
|
- run: yarn install
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: './src/wasm-lib'
|
|
||||||
|
|
||||||
- run: yarn build:wasm
|
- run: yarn build:wasm
|
||||||
|
|
||||||
yarn-tsc:
|
yarn-tsc:
|
||||||
@ -70,10 +66,6 @@ jobs:
|
|||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
- run: yarn install
|
- run: yarn install
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: './src/wasm-lib'
|
|
||||||
|
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
|
||||||
python-codespell:
|
python-codespell:
|
||||||
@ -101,11 +93,6 @@ jobs:
|
|||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|
||||||
- run: yarn install
|
- run: yarn install
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: './src/wasm-lib'
|
|
||||||
|
|
||||||
- run: yarn build:wasm
|
- run: yarn build:wasm
|
||||||
|
|
||||||
- run: yarn simpleserver:bg
|
- run: yarn simpleserver:bg
|
||||||
|
@ -36,7 +36,7 @@ exampleSketch = startSketchOn('XZ')
|
|||||||
|> close(%)
|
|> close(%)
|
||||||
|> patternCircular2d({
|
|> patternCircular2d({
|
||||||
center: [0, 0],
|
center: [0, 0],
|
||||||
repetitions: 12,
|
instances: 13,
|
||||||
arcDegrees: 360,
|
arcDegrees: 360,
|
||||||
rotateDuplicates: true
|
rotateDuplicates: true
|
||||||
}, %)
|
}, %)
|
||||||
|
@ -35,7 +35,7 @@ example = extrude(-5, exampleSketch)
|
|||||||
|> patternCircular3d({
|
|> patternCircular3d({
|
||||||
axis: [1, -1, 0],
|
axis: [1, -1, 0],
|
||||||
center: [10, -20, 0],
|
center: [10, -20, 0],
|
||||||
repetitions: 10,
|
instances: 11,
|
||||||
arcDegrees: 360,
|
arcDegrees: 360,
|
||||||
rotateDuplicates: true
|
rotateDuplicates: true
|
||||||
}, %)
|
}, %)
|
||||||
|
@ -32,7 +32,7 @@ exampleSketch = startSketchOn('XZ')
|
|||||||
|> circle({ center: [0, 0], radius: 1 }, %)
|
|> circle({ center: [0, 0], radius: 1 }, %)
|
||||||
|> patternLinear2d({
|
|> patternLinear2d({
|
||||||
axis: [1, 0],
|
axis: [1, 0],
|
||||||
repetitions: 6,
|
instances: 7,
|
||||||
distance: 4
|
distance: 4
|
||||||
}, %)
|
}, %)
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ exampleSketch = startSketchOn('XZ')
|
|||||||
example = extrude(1, exampleSketch)
|
example = extrude(1, exampleSketch)
|
||||||
|> patternLinear3d({
|
|> patternLinear3d({
|
||||||
axis: [1, 0, 1],
|
axis: [1, 0, 1],
|
||||||
repetitions: 6,
|
instances: 7,
|
||||||
distance: 6
|
distance: 6
|
||||||
}, %)
|
}, %)
|
||||||
```
|
```
|
||||||
|
@ -32,7 +32,7 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
|
|||||||
fn decagon = (radius) => {
|
fn decagon = (radius) => {
|
||||||
step = 1 / 10 * tau()
|
step = 1 / 10 * tau()
|
||||||
sketch001 = startSketchAt([cos(0) * radius, sin(0) * radius])
|
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
|
x = cos(step * i) * radius
|
||||||
y = sin(step * i) * radius
|
y = sin(step * i) * radius
|
||||||
return lineTo([x, y], sg)
|
return lineTo([x, y], sg)
|
||||||
|
1452
docs/kcl/std.json
@ -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:** `<=`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
## Properties
|
||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
@ -45,6 +66,7 @@ layout: manual
|
|||||||
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
| `start` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
||||||
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
| `end` |[`EnvironmentRef`](/docs/kcl/types/EnvironmentRef)| | No |
|
||||||
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
|
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
|
||||||
|
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No |
|
||||||
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | 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 |
|
| `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 |
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ Data for a circular pattern on a 2D sketch.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| 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 |
|
| `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 |
|
| `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 |
|
| `rotateDuplicates` |`boolean`| Whether or not to rotate the duplicates as they are copied. | No |
|
||||||
|
@ -16,7 +16,7 @@ Data for a circular pattern on a 3D model.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| 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 |
|
| `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 |
|
| `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 |
|
| `arcDegrees` |`number`| The arc angle (in degrees) to place the repetitions. Must be greater than 0. | No |
|
||||||
|
@ -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
|
## Properties
|
||||||
|
|
||||||
| Property | Type | Description | Required |
|
| Property | Type | Description | Required |
|
||||||
|
24
docs/kcl/types/ImportItem.md
Normal 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 |
|
||||||
|
|
||||||
|
|
16
docs/kcl/types/ItemVisibility.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: "ItemVisibility"
|
||||||
|
excerpt: ""
|
||||||
|
layout: manual
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
**enum:** `default`, `export`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ Data for a linear pattern on a 2D sketch.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| 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 |
|
| `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 |
|
| `axis` |`[number, number]`| The axis of the pattern. This is a 2D vector. | No |
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ Data for a linear pattern on a 3D model.
|
|||||||
|
|
||||||
| Property | Type | Description | Required |
|
| 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 |
|
| `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 |
|
| `axis` |`[number, number, number]`| The axis of the pattern. | No |
|
||||||
|
|
||||||
|
@ -669,6 +669,7 @@ test.describe(
|
|||||||
// screen shot should show the sketch
|
// screen shot should show the sketch
|
||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
maxDiffPixels: 100,
|
maxDiffPixels: 100,
|
||||||
|
mask: [page.getByTestId('model-state-indicator')],
|
||||||
})
|
})
|
||||||
|
|
||||||
// exit sketch
|
// exit sketch
|
||||||
@ -686,6 +687,7 @@ test.describe(
|
|||||||
// second screen shot should look almost identical, i.e. scale should be the same.
|
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||||
await expect(page).toHaveScreenshot({
|
await expect(page).toHaveScreenshot({
|
||||||
maxDiffPixels: 100,
|
maxDiffPixels: 100,
|
||||||
|
mask: [page.getByTestId('model-state-indicator')],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
2
interface.d.ts
vendored
@ -73,7 +73,7 @@ export interface IElectronAPI {
|
|||||||
callback: (value: { version: string }) => void
|
callback: (value: { version: string }) => void
|
||||||
) => Electron.IpcRenderer
|
) => Electron.IpcRenderer
|
||||||
onUpdateDownloaded: (
|
onUpdateDownloaded: (
|
||||||
callback: (value: string) => void
|
callback: (value: { version: string; releaseNotes: string }) => void
|
||||||
) => Electron.IpcRenderer
|
) => Electron.IpcRenderer
|
||||||
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
|
onUpdateError: (callback: (value: { error: Error }) => void) => Electron
|
||||||
appRestart: () => void
|
appRestart: () => void
|
||||||
|
@ -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": {
|
"/ping": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "ping",
|
"operationId": "ping",
|
||||||
@ -492,6 +520,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tags": [
|
"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.",
|
"description": "Utilities for making parts and discovering machines.",
|
||||||
"externalDocs": {
|
"externalDocs": {
|
||||||
|
153
src/components/ToastUpdate.test.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
@ -1,14 +1,23 @@
|
|||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||||
|
import { Marked } from '@ts-stack/markdown'
|
||||||
|
|
||||||
export function ToastUpdate({
|
export function ToastUpdate({
|
||||||
version,
|
version,
|
||||||
|
releaseNotes,
|
||||||
onRestart,
|
onRestart,
|
||||||
|
onDismiss,
|
||||||
}: {
|
}: {
|
||||||
version: string
|
version: string
|
||||||
|
releaseNotes?: string
|
||||||
onRestart: () => void
|
onRestart: () => void
|
||||||
|
onDismiss: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const containsBreakingChanges = releaseNotes
|
||||||
|
?.toLocaleLowerCase()
|
||||||
|
.includes('breaking')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="inset-0 z-50 grid place-content-center rounded bg-chalkboard-110/50 shadow-md">
|
<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">
|
<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}
|
v{version}
|
||||||
</span>
|
</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
|
A new update has downloaded and will be available next time you
|
||||||
start the app. You can view the release notes{' '}
|
start the app. You can view the release notes{' '}
|
||||||
<a
|
<a
|
||||||
@ -32,15 +41,39 @@ export function ToastUpdate({
|
|||||||
>
|
>
|
||||||
here on GitHub.
|
here on GitHub.
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</p>
|
||||||
</div>
|
</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">
|
<div className="flex justify-between gap-8">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
iconStart={{
|
iconStart={{
|
||||||
icon: 'arrowRotateRight',
|
icon: 'arrowRotateRight',
|
||||||
}}
|
}}
|
||||||
name="Restart app now"
|
name="restart"
|
||||||
onClick={onRestart}
|
onClick={onRestart}
|
||||||
>
|
>
|
||||||
Restart app now
|
Restart app now
|
||||||
@ -50,9 +83,10 @@ export function ToastUpdate({
|
|||||||
iconStart={{
|
iconStart={{
|
||||||
icon: 'checkmark',
|
icon: 'checkmark',
|
||||||
}}
|
}}
|
||||||
name="Got it"
|
name="dismiss"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
toast.dismiss()
|
toast.dismiss()
|
||||||
|
onDismiss()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Got it
|
Got it
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { styleTags, tags as t } from '@lezer/highlight'
|
import { styleTags, tags as t } from '@lezer/highlight'
|
||||||
|
|
||||||
export const kclHighlight = styleTags({
|
export const kclHighlight = styleTags({
|
||||||
|
'import export': t.moduleKeyword,
|
||||||
|
ImportItemAs: t.definitionKeyword,
|
||||||
|
ImportFrom: t.moduleKeyword,
|
||||||
'fn var let const': t.definitionKeyword,
|
'fn var let const': t.definitionKeyword,
|
||||||
'if else': t.controlKeyword,
|
'if else': t.controlKeyword,
|
||||||
return: t.controlKeyword,
|
return: t.controlKeyword,
|
||||||
|
@ -15,8 +15,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
statement[@isGroup=Statement] {
|
statement[@isGroup=Statement] {
|
||||||
FunctionDeclaration { kw<"fn"> VariableDefinition Equals ParamList Arrow Body } |
|
ImportStatement { kw<"import"> ImportItems ImportFrom String } |
|
||||||
VariableDeclaration { (kw<"var"> | kw<"let"> | kw<"const">)? VariableDefinition Equals expression } |
|
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 } |
|
ReturnStatement { kw<"return"> expression } |
|
||||||
ExpressionStatement { expression }
|
ExpressionStatement { expression }
|
||||||
}
|
}
|
||||||
@ -25,6 +26,9 @@ ParamList { "(" commaSep<Parameter { VariableDefinition "?"? (":" type)? }> ")"
|
|||||||
|
|
||||||
Body { "{" statement* "}" }
|
Body { "{" statement* "}" }
|
||||||
|
|
||||||
|
ImportItems { commaSep1NoTrailingComma<ImportItem> }
|
||||||
|
ImportItem { identifier (ImportItemAs identifier)? }
|
||||||
|
|
||||||
expression[@isGroup=Expression] {
|
expression[@isGroup=Expression] {
|
||||||
String |
|
String |
|
||||||
Number |
|
Number |
|
||||||
@ -74,6 +78,8 @@ kw<term> { @specialize[@name={term}]<identifier, term> }
|
|||||||
|
|
||||||
commaSep<term> { (term ("," term)*)? ","? }
|
commaSep<term> { (term ("," term)*)? ","? }
|
||||||
|
|
||||||
|
commaSep1NoTrailingComma<term> { term ("," term)* }
|
||||||
|
|
||||||
@tokens {
|
@tokens {
|
||||||
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
|
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
|
||||||
|
|
||||||
@ -106,6 +112,9 @@ commaSep<term> { (term ("," term)*)? ","? }
|
|||||||
|
|
||||||
Shebang { "#!" ![\n]* }
|
Shebang { "#!" ![\n]* }
|
||||||
|
|
||||||
|
ImportItemAs { "as" }
|
||||||
|
ImportFrom { "from" }
|
||||||
|
|
||||||
"(" ")"
|
"(" ")"
|
||||||
"{" "}"
|
"{" "}"
|
||||||
"[" "]"
|
"[" "]"
|
||||||
|
@ -293,6 +293,24 @@ code {
|
|||||||
which lets you use them with @apply in your CSS, and get
|
which lets you use them with @apply in your CSS, and get
|
||||||
autocomplete in classNames in your JSX.
|
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,
|
#code-mirror-override .cm-scroller,
|
||||||
|
@ -70,15 +70,17 @@ if (isDesktop()) {
|
|||||||
id: AUTO_UPDATER_TOAST_ID,
|
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.`
|
const message = `A new update (${version}) was downloaded and will be available next time you open the app.`
|
||||||
console.log(message)
|
console.log(message)
|
||||||
toast.custom(
|
toast.custom(
|
||||||
ToastUpdate({
|
ToastUpdate({
|
||||||
version,
|
version,
|
||||||
|
releaseNotes,
|
||||||
onRestart: () => {
|
onRestart: () => {
|
||||||
window.electron.appRestart()
|
window.electron.appRestart()
|
||||||
},
|
},
|
||||||
|
onDismiss: () => {},
|
||||||
}),
|
}),
|
||||||
{ duration: 30000, id: AUTO_UPDATER_TOAST_ID }
|
{ duration: 30000, id: AUTO_UPDATER_TOAST_ID }
|
||||||
)
|
)
|
||||||
|
@ -501,6 +501,7 @@ export function sketchOnExtrudedFace(
|
|||||||
createIdentifier(extrudeName ? extrudeName : oldSketchName),
|
createIdentifier(extrudeName ? extrudeName : oldSketchName),
|
||||||
_tag,
|
_tag,
|
||||||
]),
|
]),
|
||||||
|
undefined,
|
||||||
'const'
|
'const'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -682,6 +683,7 @@ export function createPipeExpression(
|
|||||||
export function createVariableDeclaration(
|
export function createVariableDeclaration(
|
||||||
varName: string,
|
varName: string,
|
||||||
init: VariableDeclarator['init'],
|
init: VariableDeclarator['init'],
|
||||||
|
visibility: VariableDeclaration['visibility'] = 'default',
|
||||||
kind: VariableDeclaration['kind'] = 'const'
|
kind: VariableDeclaration['kind'] = 'const'
|
||||||
): VariableDeclaration {
|
): VariableDeclaration {
|
||||||
return {
|
return {
|
||||||
@ -699,6 +701,7 @@ export function createVariableDeclaration(
|
|||||||
init,
|
init,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
visibility,
|
||||||
kind,
|
kind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -620,7 +620,7 @@ describe('Testing button states', () => {
|
|||||||
it('should return true when body exists and segment is selected', async () => {
|
it('should return true when body exists and segment is selected', async () => {
|
||||||
await runButtonStateTest(codeWithBody, `line([10, 0], %)`, true)
|
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)
|
await runButtonStateTest(codeWithBody, `close(%)`, false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
CallExpression,
|
CallExpression,
|
||||||
|
Expr,
|
||||||
|
Identifier,
|
||||||
ObjectExpression,
|
ObjectExpression,
|
||||||
PathToNode,
|
PathToNode,
|
||||||
Program,
|
Program,
|
||||||
@ -27,7 +29,7 @@ import {
|
|||||||
sketchLineHelperMap,
|
sketchLineHelperMap,
|
||||||
} from '../std/sketch'
|
} from '../std/sketch'
|
||||||
import { err, trap } from 'lib/trap'
|
import { err, trap } from 'lib/trap'
|
||||||
import { Selections, canFilletSelection } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { KclCommandValue } from 'lib/commandTypes'
|
import { KclCommandValue } from 'lib/commandTypes'
|
||||||
import {
|
import {
|
||||||
ArtifactGraph,
|
ArtifactGraph,
|
||||||
@ -66,7 +68,10 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
const artifactGraph = engineCommandManager.artifactGraph
|
const artifactGraph = engineCommandManager.artifactGraph
|
||||||
|
|
||||||
// Step 1: modify ast with tags and group them by extrude nodes (bodies)
|
// 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
|
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
|
||||||
|
|
||||||
for (const selectionRange of selection.codeBasedSelections) {
|
for (const selectionRange of selection.codeBasedSelections) {
|
||||||
@ -74,6 +79,7 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
codeBasedSelections: [selectionRange],
|
codeBasedSelections: [selectionRange],
|
||||||
otherSelections: [],
|
otherSelections: [],
|
||||||
}
|
}
|
||||||
|
const selectionType = singleSelection.codeBasedSelections[0].type
|
||||||
|
|
||||||
const result = getPathToExtrudeForSegmentSelection(
|
const result = getPathToExtrudeForSegmentSelection(
|
||||||
clonedAstForGetExtrude,
|
clonedAstForGetExtrude,
|
||||||
@ -89,6 +95,7 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
)
|
)
|
||||||
if (err(tagResult)) return tagResult
|
if (err(tagResult)) return tagResult
|
||||||
const { tag } = tagResult
|
const { tag } = tagResult
|
||||||
|
const tagInfo = { tag, selectionType }
|
||||||
|
|
||||||
// Group tags by their corresponding extrude node
|
// Group tags by their corresponding extrude node
|
||||||
const extrudeKey = JSON.stringify(pathToExtrudeNode)
|
const extrudeKey = JSON.stringify(pathToExtrudeNode)
|
||||||
@ -96,23 +103,29 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
if (lookupMap.has(extrudeKey)) {
|
if (lookupMap.has(extrudeKey)) {
|
||||||
const existingPath = lookupMap.get(extrudeKey)
|
const existingPath = lookupMap.get(extrudeKey)
|
||||||
if (!existingPath) return new Error('Path to extrude node not found.')
|
if (!existingPath) return new Error('Path to extrude node not found.')
|
||||||
extrudeToTagsMap.get(existingPath)?.push(tag)
|
extrudeToTagsMap.get(existingPath)?.push(tagInfo)
|
||||||
} else {
|
} else {
|
||||||
lookupMap.set(extrudeKey, pathToExtrudeNode)
|
lookupMap.set(extrudeKey, pathToExtrudeNode)
|
||||||
extrudeToTagsMap.set(pathToExtrudeNode, [tag])
|
extrudeToTagsMap.set(pathToExtrudeNode, [tagInfo])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Apply fillet(s) for each extrude node (body)
|
// Step 2: Apply fillet(s) for each extrude node (body)
|
||||||
let pathToFilletNodes: Array<PathToNode> = []
|
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
|
// Create a fillet expression with multiple tags
|
||||||
const radiusValue =
|
const radiusValue =
|
||||||
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
|
'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', [
|
const filletCall = createCallExpressionStdLib('fillet', [
|
||||||
createObjectExpression({
|
createObjectExpression({
|
||||||
radius: radiusValue,
|
radius: radiusValue,
|
||||||
tags: createArrayExpression(tags.map((tag) => createIdentifier(tag))),
|
tags: createArrayExpression(tagCalls),
|
||||||
}),
|
}),
|
||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
])
|
])
|
||||||
@ -144,7 +157,7 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
pathToFilletNode = getPathToNodeOfFilletLiteral(
|
pathToFilletNode = getPathToNodeOfFilletLiteral(
|
||||||
pathToExtrudeNode,
|
pathToExtrudeNode,
|
||||||
extrudeDeclarator,
|
extrudeDeclarator,
|
||||||
tags[0]
|
firstTag
|
||||||
)
|
)
|
||||||
pathToFilletNodes.push(pathToFilletNode)
|
pathToFilletNodes.push(pathToFilletNode)
|
||||||
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
|
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
|
||||||
@ -165,7 +178,7 @@ export function modifyAstCloneWithFilletAndTag(
|
|||||||
pathToFilletNode = getPathToNodeOfFilletLiteral(
|
pathToFilletNode = getPathToNodeOfFilletLiteral(
|
||||||
pathToExtrudeNode,
|
pathToExtrudeNode,
|
||||||
extrudeDeclarator,
|
extrudeDeclarator,
|
||||||
tags[0]
|
firstTag
|
||||||
)
|
)
|
||||||
pathToFilletNodes.push(pathToFilletNode)
|
pathToFilletNodes.push(pathToFilletNode)
|
||||||
} else {
|
} else {
|
||||||
@ -276,6 +289,21 @@ function mutateAstWithTagForSketchSegment(
|
|||||||
return { modifiedAst: astClone, tag }
|
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(
|
function locateExtrudeDeclarator(
|
||||||
node: Program,
|
node: Program,
|
||||||
pathToExtrudeNode: PathToNode
|
pathToExtrudeNode: PathToNode
|
||||||
@ -311,7 +339,7 @@ function locateExtrudeDeclarator(
|
|||||||
function getPathToNodeOfFilletLiteral(
|
function getPathToNodeOfFilletLiteral(
|
||||||
pathToExtrudeNode: PathToNode,
|
pathToExtrudeNode: PathToNode,
|
||||||
extrudeDeclarator: VariableDeclarator,
|
extrudeDeclarator: VariableDeclarator,
|
||||||
tag: string
|
tag: Identifier | CallExpression
|
||||||
): PathToNode {
|
): PathToNode {
|
||||||
let pathToFilletObj: PathToNode = []
|
let pathToFilletObj: PathToNode = []
|
||||||
let inFillet = false
|
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) => {
|
return node.properties.some((prop) => {
|
||||||
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
|
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
|
||||||
return prop.value.elements.some(
|
// if selection is a base edge:
|
||||||
(element) => element.type === 'Identifier' && element.name === tag
|
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
|
return false
|
||||||
})
|
})
|
||||||
@ -383,7 +429,7 @@ export const hasValidFilletSelection = ({
|
|||||||
ast: Program
|
ast: Program
|
||||||
code: string
|
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
|
let extrudeExists = false
|
||||||
traverse(ast, {
|
traverse(ast, {
|
||||||
enter(node) {
|
enter(node) {
|
||||||
@ -394,65 +440,88 @@ export const hasValidFilletSelection = ({
|
|||||||
})
|
})
|
||||||
if (!extrudeExists) return false
|
if (!extrudeExists) return false
|
||||||
|
|
||||||
// case 1: nothing selected, test whether the extrusion exists
|
// check if nothing is selected
|
||||||
if (selectionRanges) {
|
if (selectionRanges.codeBasedSelections.length === 0) {
|
||||||
if (selectionRanges.codeBasedSelections.length === 0) {
|
return true
|
||||||
return true
|
|
||||||
}
|
|
||||||
const range0 = selectionRanges.codeBasedSelections[0].range[0]
|
|
||||||
const codeLength = code.length
|
|
||||||
if (range0 === codeLength) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// case 2: sketch segment selected, test whether it is extruded
|
// check if selection is last string in code
|
||||||
// TODO: add loft / sweep check
|
if (selectionRanges.codeBasedSelections[0].range[0] === code.length) {
|
||||||
if (selectionRanges.codeBasedSelections.length > 0) {
|
return true
|
||||||
const isExtruded = hasSketchPipeBeenExtruded(
|
}
|
||||||
selectionRanges.codeBasedSelections[0],
|
|
||||||
ast
|
// 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) {
|
if (err(segmentNode)) return false
|
||||||
const pathToSelectedNode = getNodePathFromSourceRange(
|
if (segmentNode.node.type !== 'CallExpression') {
|
||||||
ast,
|
return false
|
||||||
selectionRanges.codeBasedSelections[0].range
|
}
|
||||||
)
|
if (!(segmentNode.node.callee.name in sketchLineHelperMap)) {
|
||||||
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 {
|
|
||||||
return false
|
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 =
|
type EdgeTypes =
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
getConstraintType,
|
getConstraintType,
|
||||||
} from './std/sketchcombos'
|
} from './std/sketchcombos'
|
||||||
import { err } from 'lib/trap'
|
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.
|
* 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(
|
function moreNodePathFromSourceRange(
|
||||||
node: Expr | ExpressionStatement | VariableDeclaration | ReturnStatement,
|
node:
|
||||||
|
| Expr
|
||||||
|
| ImportStatement
|
||||||
|
| ExpressionStatement
|
||||||
|
| VariableDeclaration
|
||||||
|
| ReturnStatement,
|
||||||
sourceRange: Selection['range'],
|
sourceRange: Selection['range'],
|
||||||
previousPath: PathToNode = [['body', '']]
|
previousPath: PathToNode = [['body', '']]
|
||||||
): PathToNode {
|
): PathToNode {
|
||||||
|
Before Width: | Height: | Size: 357 KiB After Width: | Height: | Size: 378 KiB |
Before Width: | Height: | Size: 577 KiB After Width: | Height: | Size: 613 KiB |
@ -426,6 +426,7 @@ export const _executor = async (
|
|||||||
baseUnit,
|
baseUnit,
|
||||||
engineCommandManager,
|
engineCommandManager,
|
||||||
fileSystemManager,
|
fileSystemManager,
|
||||||
|
undefined,
|
||||||
isMock
|
isMock
|
||||||
)
|
)
|
||||||
return execStateFromRaw(execState)
|
return execStateFromRaw(execState)
|
||||||
|
39
src/lib/machine-api.d.ts
vendored
@ -55,6 +55,23 @@ export interface paths {
|
|||||||
patch?: never
|
patch?: never
|
||||||
trace?: 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': {
|
'/ping': {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never
|
query?: never
|
||||||
@ -278,6 +295,28 @@ export interface operations {
|
|||||||
'5XX': components['responses']['Error']
|
'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: {
|
ping: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never
|
query?: never
|
||||||
|
@ -287,7 +287,10 @@ app.on('ready', () => {
|
|||||||
|
|
||||||
autoUpdater.on('update-downloaded', (info) => {
|
autoUpdater.on('update-downloaded', (info) => {
|
||||||
console.log('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', () => {
|
ipcMain.handle('app.restart', () => {
|
||||||
|
@ -16,11 +16,12 @@ const startDeviceFlow = (host: string): Promise<string> =>
|
|||||||
ipcRenderer.invoke('startDeviceFlow', host)
|
ipcRenderer.invoke('startDeviceFlow', host)
|
||||||
const loginWithDeviceFlow = (): Promise<string> =>
|
const loginWithDeviceFlow = (): Promise<string> =>
|
||||||
ipcRenderer.invoke('loginWithDeviceFlow')
|
ipcRenderer.invoke('loginWithDeviceFlow')
|
||||||
|
const onUpdateDownloaded = (
|
||||||
|
callback: (value: { version: string; releaseNotes: string }) => void
|
||||||
|
) => ipcRenderer.on('update-downloaded', (_event, value) => callback(value))
|
||||||
const onUpdateDownloadStart = (
|
const onUpdateDownloadStart = (
|
||||||
callback: (value: { version: string }) => void
|
callback: (value: { version: string }) => void
|
||||||
) => ipcRenderer.on('update-download-start', (_event, value) => callback(value))
|
) => 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) =>
|
const onUpdateError = (callback: (value: Error) => void) =>
|
||||||
ipcRenderer.on('update-error', (_event, value) => callback(value))
|
ipcRenderer.on('update-error', (_event, value) => callback(value))
|
||||||
const appRestart = () => ipcRenderer.invoke('app.restart')
|
const appRestart = () => ipcRenderer.invoke('app.restart')
|
||||||
|
64
src/wasm-lib/Cargo.lock
generated
@ -1394,9 +1394,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.2"
|
version = "0.25.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
|
checksum = "d97eb9a8e0cd5b76afea91d7eecd5cf8338cd44ced04256cf1f800474b227c52"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder-lite",
|
"byteorder-lite",
|
||||||
@ -1533,16 +1533,16 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.71"
|
version = "0.3.72"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cb94a0ffd3f3ee755c20f7d8752f45cac88605a4dcf808abcff72873296ec7b"
|
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
version = "0.2.20"
|
version = "0.2.22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"approx 0.5.1",
|
"approx 0.5.1",
|
||||||
@ -1617,7 +1617,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-test-server"
|
name = "kcl-test-server"
|
||||||
version = "0.1.12"
|
version = "0.1.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"hyper 0.14.30",
|
"hyper 0.14.30",
|
||||||
@ -2337,18 +2337,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.86"
|
version = "1.0.88"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3"
|
name = "pyo3"
|
||||||
version = "0.22.3"
|
version = "0.22.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "15ee168e30649f7f234c3d49ef5a7a6cbf5134289bc46c29ff3155fa3221c225"
|
checksum = "3d922163ba1f79c04bc49073ba7b32fd5a8d3b76a87c955921234b8e77333c51"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"indoc",
|
"indoc",
|
||||||
@ -2364,9 +2364,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-build-config"
|
name = "pyo3-build-config"
|
||||||
version = "0.22.3"
|
version = "0.22.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e61cef80755fe9e46bb8a0b8f20752ca7676dcc07a5277d8b7768c6172e529b3"
|
checksum = "bc38c5feeb496c8321091edf3d63e9a6829eab4b863b4a6a65f26f3e9cc6b179"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"target-lexicon",
|
"target-lexicon",
|
||||||
@ -2374,9 +2374,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-ffi"
|
name = "pyo3-ffi"
|
||||||
version = "0.22.3"
|
version = "0.22.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67ce096073ec5405f5ee2b8b31f03a68e02aa10d5d4f565eca04acc41931fa1c"
|
checksum = "94845622d88ae274d2729fcefc850e63d7a3ddff5e3ce11bd88486db9f1d357d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"pyo3-build-config",
|
"pyo3-build-config",
|
||||||
@ -2384,9 +2384,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-macros"
|
name = "pyo3-macros"
|
||||||
version = "0.22.3"
|
version = "0.22.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2440c6d12bc8f3ae39f1e775266fa5122fd0c8891ce7520fa6048e683ad3de28"
|
checksum = "e655aad15e09b94ffdb3ce3d217acf652e26bbc37697ef012f5e5e348c716e5e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"pyo3-macros-backend",
|
"pyo3-macros-backend",
|
||||||
@ -2396,9 +2396,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3-macros-backend"
|
name = "pyo3-macros-backend"
|
||||||
version = "0.22.3"
|
version = "0.22.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1be962f0e06da8f8465729ea2cb71a416d2257dff56cbe40a70d3e62a93ae5d1"
|
checksum = "ae1e3f09eecd94618f60a455a23def79f79eba4dc561a97324bf9ac8c6df30ce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -3829,9 +3829,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.10.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"serde",
|
"serde",
|
||||||
@ -3907,9 +3907,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.94"
|
version = "0.2.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef073ced962d62984fb38a36e5fdc1a2b23c9e0e1fa0689bb97afa4202ef6887"
|
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@ -3918,9 +3918,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.94"
|
version = "0.2.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4bfab14ef75323f4eb75fa52ee0a3fb59611977fd3240da19b2cf36ff85030e"
|
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"log",
|
"log",
|
||||||
@ -3946,9 +3946,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.94"
|
version = "0.2.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7bec9830f60924d9ceb3ef99d55c155be8afa76954edffbb5936ff4509474e7"
|
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@ -3956,9 +3956,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.94"
|
version = "0.2.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c74f6e152a76a2ad448e223b0fc0b6b5747649c3d769cc6bf45737bf97d0ed6"
|
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -3969,9 +3969,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.94"
|
version = "0.2.95"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a42f6c679374623f295a8623adfe63d9284091245c3504bde47c17a3ce2777d9"
|
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-lib"
|
name = "wasm-lib"
|
||||||
@ -4032,9 +4032,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.70"
|
version = "0.3.72"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
|
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
@ -18,31 +18,31 @@ kittycad.workspace = true
|
|||||||
serde_json = "1.0.128"
|
serde_json = "1.0.128"
|
||||||
tokio = { version = "1.40.0", features = ["sync"] }
|
tokio = { version = "1.40.0", features = ["sync"] }
|
||||||
toml = "0.8.19"
|
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 = "0.2.91"
|
||||||
wasm-bindgen-futures = "0.4.44"
|
wasm-bindgen-futures = "0.4.44"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = "1"
|
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 = { workspace = true, default-features = true }
|
||||||
kittycad-modeling-cmds = { workspace = true }
|
kittycad-modeling-cmds = { workspace = true }
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
reqwest = { version = "0.12", default-features = false }
|
reqwest = { version = "0.12", default-features = false }
|
||||||
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
|
tokio = { version = "1.40.0", features = ["rt-multi-thread", "macros", "time"] }
|
||||||
twenty-twenty = "0.8"
|
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]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
console_error_panic_hook = "0.1.7"
|
console_error_panic_hook = "0.1.7"
|
||||||
futures = "0.3.31"
|
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"] }
|
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-bindgen-futures = { version = "0.4.44", features = ["futures-core-03-stream"] }
|
||||||
wasm-streams = "0.4.1"
|
wasm-streams = "0.4.1"
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
|
[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
|
||||||
version = "0.3.69"
|
version = "0.3.72"
|
||||||
features = [
|
features = [
|
||||||
"console",
|
"console",
|
||||||
"HtmlTextAreaElement",
|
"HtmlTextAreaElement",
|
||||||
|
@ -762,7 +762,7 @@ fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> pr
|
|||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -17,7 +17,7 @@ mod test_examples_someFn {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -17,7 +17,7 @@ mod test_examples_someFn {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -17,7 +17,7 @@ mod test_examples_show {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -51,7 +51,7 @@ mod test_examples_show {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -17,7 +17,7 @@ mod test_examples_show {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -18,7 +18,7 @@ mod test_examples_my_func {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -52,7 +52,7 @@ mod test_examples_my_func {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -18,7 +18,7 @@ mod test_examples_line_to {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -52,7 +52,7 @@ mod test_examples_line_to {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -17,7 +17,7 @@ mod test_examples_min {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
@ -51,7 +51,7 @@ mod test_examples_min {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -17,7 +17,7 @@ mod test_examples_show {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -17,7 +17,7 @@ mod test_examples_import {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -17,7 +17,7 @@ mod test_examples_import {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -17,7 +17,7 @@ mod test_examples_import {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -17,7 +17,7 @@ mod test_examples_show {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -17,7 +17,7 @@ mod test_examples_some_function {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: crate::executor::ContextType::Mock,
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 5)]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
use kcl_lib::ast::types::{
|
use kcl_lib::ast::types::{
|
||||||
BodyItem, Expr, Identifier, Literal, LiteralValue, NonCodeMeta, Program, VariableDeclaration, VariableDeclarator,
|
BodyItem, Expr, Identifier, ItemVisibility, Literal, LiteralValue, NonCodeMeta, Program, VariableDeclaration,
|
||||||
VariableKind,
|
VariableDeclarator, VariableKind,
|
||||||
};
|
};
|
||||||
use kcl_macros::parse;
|
use kcl_macros::parse;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
@ -33,6 +33,7 @@ fn basic() {
|
|||||||
})),
|
})),
|
||||||
digest: None,
|
digest: None,
|
||||||
}],
|
}],
|
||||||
|
visibility: ItemVisibility::Default,
|
||||||
kind: VariableKind::Const,
|
kind: VariableKind::Const,
|
||||||
digest: None,
|
digest: None,
|
||||||
})],
|
})],
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-test-server"
|
name = "kcl-test-server"
|
||||||
description = "A test server for KCL"
|
description = "A test server for KCL"
|
||||||
version = "0.1.12"
|
version = "0.1.13"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
@ -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 users know if the test is taking a long time.
|
||||||
let (done_tx, done_rx) = oneshot::channel::<()>();
|
let (done_tx, done_rx) = oneshot::channel::<()>();
|
||||||
let timer = time_until(done_rx);
|
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,
|
Ok(sn) => sn,
|
||||||
Err(e) => return kcl_err(e),
|
Err(e) => return kcl_err(e),
|
||||||
};
|
};
|
||||||
|
@ -20,4 +20,4 @@ kcl-lib = { path = "../kcl" }
|
|||||||
kittycad = { workspace = true, features = ["clap"] }
|
kittycad = { workspace = true, features = ["clap"] }
|
||||||
kittycad-modeling-cmds = { workspace = true }
|
kittycad-modeling-cmds = { workspace = true }
|
||||||
tokio = { version = "1.38", features = ["full", "time", "rt", "tracing"] }
|
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"] }
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use kcl_lib::{
|
use kcl_lib::{
|
||||||
|
engine::ExecutionKind,
|
||||||
errors::KclError,
|
errors::KclError,
|
||||||
executor::{DefaultPlanes, IdGenerator},
|
executor::{DefaultPlanes, IdGenerator},
|
||||||
};
|
};
|
||||||
@ -26,6 +27,7 @@ pub struct EngineConnection {
|
|||||||
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::executor::SourceRange)>>>,
|
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, kcl_lib::executor::SourceRange)>>>,
|
||||||
core_test: Arc<Mutex<String>>,
|
core_test: Arc<Mutex<String>>,
|
||||||
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
|
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
|
||||||
|
execution_kind: Arc<Mutex<ExecutionKind>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EngineConnection {
|
impl EngineConnection {
|
||||||
@ -39,6 +41,7 @@ impl EngineConnection {
|
|||||||
batch_end: Arc::new(Mutex::new(IndexMap::new())),
|
batch_end: Arc::new(Mutex::new(IndexMap::new())),
|
||||||
core_test: result,
|
core_test: result,
|
||||||
default_planes: Default::default(),
|
default_planes: Default::default(),
|
||||||
|
execution_kind: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,6 +363,18 @@ impl kcl_lib::engine::EngineManager for EngineConnection {
|
|||||||
self.batch_end.clone()
|
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(
|
async fn default_planes(
|
||||||
&self,
|
&self,
|
||||||
id_generator: &mut IdGenerator,
|
id_generator: &mut IdGenerator,
|
||||||
|
@ -23,7 +23,7 @@ pub async fn kcl_to_engine_core(code: &str) -> Result<String> {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: kcl_lib::executor::ContextType::MockCustomForwarded,
|
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();
|
let result = result.lock().expect("mutex lock").clone();
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
description = "KittyCAD Language implementation and tools"
|
description = "KittyCAD Language implementation and tools"
|
||||||
version = "0.2.20"
|
version = "0.2.22"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
@ -26,7 +26,7 @@ futures = { version = "0.3.31" }
|
|||||||
git_rev = "0.1.0"
|
git_rev = "0.1.0"
|
||||||
gltf-json = "1.4.1"
|
gltf-json = "1.4.1"
|
||||||
http = { workspace = true }
|
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"] }
|
indexmap = { version = "2.6.0", features = ["serde"] }
|
||||||
kittycad = { workspace = true }
|
kittycad = { workspace = true }
|
||||||
kittycad-modeling-cmds = { workspace = true }
|
kittycad-modeling-cmds = { workspace = true }
|
||||||
@ -34,7 +34,7 @@ lazy_static = "1.5.0"
|
|||||||
measurements = "0.11.0"
|
measurements = "0.11.0"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
parse-display = "0.9.1"
|
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"] }
|
reqwest = { version = "0.12", default-features = false, features = ["stream", "rustls-tls"] }
|
||||||
ropey = "1.6.1"
|
ropey = "1.6.1"
|
||||||
schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1", "preserve_order"] }
|
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"] }
|
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"] }
|
url = { version = "2.5.2", features = ["serde"] }
|
||||||
urlencoding = "2.1.3"
|
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"] }
|
validator = { version = "0.18.1", features = ["derive"] }
|
||||||
winnow = "0.6.18"
|
winnow = "0.6.18"
|
||||||
zip = { version = "2.0.0", default-features = false }
|
zip = { version = "2.0.0", default-features = false }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[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"] }
|
tokio = { version = "1.40.0", features = ["sync", "time"] }
|
||||||
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
|
tower-lsp = { version = "0.20.0", default-features = false, features = ["runtime-agnostic"] }
|
||||||
wasm-bindgen = "0.2.91"
|
wasm-bindgen = "0.2.91"
|
||||||
wasm-bindgen-futures = "0.4.44"
|
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]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
approx = "0.5"
|
approx = "0.5"
|
||||||
@ -93,7 +93,7 @@ criterion = { version = "0.5.1", features = ["async_tokio"] }
|
|||||||
expectorate = "1.1.0"
|
expectorate = "1.1.0"
|
||||||
handlebars = "6.1.0"
|
handlebars = "6.1.0"
|
||||||
iai = "0.1"
|
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"] }
|
insta = { version = "1.40.0", features = ["json"] }
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
|
@ -48,7 +48,10 @@ pub async fn modify_ast_for_sketch(
|
|||||||
|
|
||||||
// Get the information about the sketch.
|
// Get the information about the sketch.
|
||||||
if let Some(ast_sketch) = program.get_variable(sketch_name) {
|
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 {
|
match &constraint_level {
|
||||||
ConstraintLevel::None { source_ranges: _ } => {}
|
ConstraintLevel::None { source_ranges: _ } => {}
|
||||||
ConstraintLevel::Ignore { source_ranges: _ } => {}
|
ConstraintLevel::Ignore { source_ranges: _ } => {}
|
||||||
|
740
src/wasm-lib/kcl/src/ast/types/execute.rs
Normal 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(
|
||||||
|
¶m.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(¶m.identifier.name, arg.clone(), param.identifier.clone().into())?;
|
||||||
|
} else {
|
||||||
|
fn_memory.add(
|
||||||
|
¶m.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,
|
||||||
|
}
|
||||||
|
}
|
@ -784,6 +784,9 @@ fn test_generate_stdlib_markdown_docs() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_generate_stdlib_json_schema() {
|
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 stdlib = StdLib::new();
|
||||||
let combined = stdlib.combined();
|
let combined = stdlib.combined();
|
||||||
|
|
||||||
|
@ -859,7 +859,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
snippet,
|
snippet,
|
||||||
r#"patternCircular3d({
|
r#"patternCircular3d({
|
||||||
repetitions: ${0:10},
|
instances: ${0:10},
|
||||||
axis: [${1:3.14}, ${2:3.14}, ${3:3.14}],
|
axis: [${1:3.14}, ${2:3.14}, ${3:3.14}],
|
||||||
center: [${4:3.14}, ${5:3.14}, ${6:3.14}],
|
center: [${4:3.14}, ${5:3.14}, ${6:3.14}],
|
||||||
arcDegrees: ${7:3.14},
|
arcDegrees: ${7:3.14},
|
||||||
@ -921,7 +921,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
snippet,
|
snippet,
|
||||||
r#"patternLinear2d({
|
r#"patternLinear2d({
|
||||||
repetitions: ${0:10},
|
instances: ${0:10},
|
||||||
distance: ${1:3.14},
|
distance: ${1:3.14},
|
||||||
axis: [${2:3.14}, ${3:3.14}],
|
axis: [${2:3.14}, ${3:3.14}],
|
||||||
}, ${4:%})${}"#
|
}, ${4:%})${}"#
|
||||||
|
@ -24,6 +24,8 @@ use crate::{
|
|||||||
executor::{DefaultPlanes, IdGenerator},
|
executor::{DefaultPlanes, IdGenerator},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::ExecutionKind;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum SocketHealth {
|
enum SocketHealth {
|
||||||
Active,
|
Active,
|
||||||
@ -46,6 +48,8 @@ pub struct EngineConnection {
|
|||||||
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
|
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
|
||||||
/// If the server sends session data, it'll be copied to here.
|
/// If the server sends session data, it'll be copied to here.
|
||||||
session_data: Arc<Mutex<Option<ModelingSessionData>>>,
|
session_data: Arc<Mutex<Option<ModelingSessionData>>>,
|
||||||
|
|
||||||
|
execution_kind: Arc<Mutex<ExecutionKind>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TcpRead {
|
pub struct TcpRead {
|
||||||
@ -300,6 +304,7 @@ impl EngineConnection {
|
|||||||
batch_end: Arc::new(Mutex::new(IndexMap::new())),
|
batch_end: Arc::new(Mutex::new(IndexMap::new())),
|
||||||
default_planes: Default::default(),
|
default_planes: Default::default(),
|
||||||
session_data,
|
session_data,
|
||||||
|
execution_kind: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -314,6 +319,18 @@ impl EngineManager for EngineConnection {
|
|||||||
self.batch_end.clone()
|
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(
|
async fn default_planes(
|
||||||
&self,
|
&self,
|
||||||
id_generator: &mut IdGenerator,
|
id_generator: &mut IdGenerator,
|
||||||
|
@ -22,10 +22,13 @@ use crate::{
|
|||||||
executor::{DefaultPlanes, IdGenerator},
|
executor::{DefaultPlanes, IdGenerator},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::ExecutionKind;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EngineConnection {
|
pub struct EngineConnection {
|
||||||
batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>,
|
batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>,
|
||||||
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>,
|
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (WebSocketRequest, crate::executor::SourceRange)>>>,
|
||||||
|
execution_kind: Arc<Mutex<ExecutionKind>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EngineConnection {
|
impl EngineConnection {
|
||||||
@ -33,6 +36,7 @@ impl EngineConnection {
|
|||||||
Ok(EngineConnection {
|
Ok(EngineConnection {
|
||||||
batch: Arc::new(Mutex::new(Vec::new())),
|
batch: Arc::new(Mutex::new(Vec::new())),
|
||||||
batch_end: Arc::new(Mutex::new(IndexMap::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()
|
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(
|
async fn default_planes(
|
||||||
&self,
|
&self,
|
||||||
_id_generator: &mut IdGenerator,
|
_id_generator: &mut IdGenerator,
|
||||||
|
@ -9,6 +9,7 @@ use kittycad_modeling_cmds as kcmc;
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
engine::ExecutionKind,
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
executor::{DefaultPlanes, IdGenerator},
|
executor::{DefaultPlanes, IdGenerator},
|
||||||
};
|
};
|
||||||
@ -42,6 +43,7 @@ pub struct EngineConnection {
|
|||||||
manager: Arc<EngineCommandManager>,
|
manager: Arc<EngineCommandManager>,
|
||||||
batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>,
|
batch: Arc<Mutex<Vec<(WebSocketRequest, crate::executor::SourceRange)>>>,
|
||||||
batch_end: Arc<Mutex<IndexMap<uuid::Uuid, (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.
|
// Safety: WebAssembly will only ever run in a single-threaded context.
|
||||||
@ -54,6 +56,7 @@ impl EngineConnection {
|
|||||||
manager: Arc::new(manager),
|
manager: Arc::new(manager),
|
||||||
batch: Arc::new(Mutex::new(Vec::new())),
|
batch: Arc::new(Mutex::new(Vec::new())),
|
||||||
batch_end: Arc::new(Mutex::new(IndexMap::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()
|
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(
|
async fn default_planes(
|
||||||
&self,
|
&self,
|
||||||
_id_generator: &mut IdGenerator,
|
_id_generator: &mut IdGenerator,
|
||||||
|
@ -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();
|
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]
|
#[async_trait::async_trait]
|
||||||
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
||||||
/// Get the batch of commands to be sent to the engine.
|
/// Get the batch of commands to be sent to the engine.
|
||||||
@ -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.
|
/// 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)>>>;
|
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.
|
/// Get the default planes.
|
||||||
async fn default_planes(
|
async fn default_planes(
|
||||||
&self,
|
&self,
|
||||||
@ -102,6 +126,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
source_range: crate::executor::SourceRange,
|
source_range: crate::executor::SourceRange,
|
||||||
cmd: &ModelingCmd,
|
cmd: &ModelingCmd,
|
||||||
) -> Result<(), crate::errors::KclError> {
|
) -> Result<(), crate::errors::KclError> {
|
||||||
|
let execution_kind = self.execution_kind();
|
||||||
|
if execution_kind.is_isolated() {
|
||||||
|
return Err(KclError::Semantic(KclErrorDetails { message: "Cannot send modeling commands while importing. Wrap your code in a function if you want to import the file.".to_owned(), source_ranges: vec![source_range] }));
|
||||||
|
}
|
||||||
let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
|
let req = WebSocketRequest::ModelingCmdReq(ModelingCmdReq {
|
||||||
cmd: cmd.clone(),
|
cmd: cmd.clone(),
|
||||||
cmd_id: id.into(),
|
cmd_id: id.into(),
|
||||||
|
@ -14,6 +14,8 @@ pub enum KclError {
|
|||||||
Syntax(KclErrorDetails),
|
Syntax(KclErrorDetails),
|
||||||
#[error("semantic: {0:?}")]
|
#[error("semantic: {0:?}")]
|
||||||
Semantic(KclErrorDetails),
|
Semantic(KclErrorDetails),
|
||||||
|
#[error("import cycle: {0:?}")]
|
||||||
|
ImportCycle(KclErrorDetails),
|
||||||
#[error("type: {0:?}")]
|
#[error("type: {0:?}")]
|
||||||
Type(KclErrorDetails),
|
Type(KclErrorDetails),
|
||||||
#[error("unimplemented: {0:?}")]
|
#[error("unimplemented: {0:?}")]
|
||||||
@ -52,6 +54,7 @@ impl KclError {
|
|||||||
KclError::Lexical(_) => "lexical",
|
KclError::Lexical(_) => "lexical",
|
||||||
KclError::Syntax(_) => "syntax",
|
KclError::Syntax(_) => "syntax",
|
||||||
KclError::Semantic(_) => "semantic",
|
KclError::Semantic(_) => "semantic",
|
||||||
|
KclError::ImportCycle(_) => "import cycle",
|
||||||
KclError::Type(_) => "type",
|
KclError::Type(_) => "type",
|
||||||
KclError::Unimplemented(_) => "unimplemented",
|
KclError::Unimplemented(_) => "unimplemented",
|
||||||
KclError::Unexpected(_) => "unexpected",
|
KclError::Unexpected(_) => "unexpected",
|
||||||
@ -68,6 +71,7 @@ impl KclError {
|
|||||||
KclError::Lexical(e) => e.source_ranges.clone(),
|
KclError::Lexical(e) => e.source_ranges.clone(),
|
||||||
KclError::Syntax(e) => e.source_ranges.clone(),
|
KclError::Syntax(e) => e.source_ranges.clone(),
|
||||||
KclError::Semantic(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::Type(e) => e.source_ranges.clone(),
|
||||||
KclError::Unimplemented(e) => e.source_ranges.clone(),
|
KclError::Unimplemented(e) => e.source_ranges.clone(),
|
||||||
KclError::Unexpected(e) => e.source_ranges.clone(),
|
KclError::Unexpected(e) => e.source_ranges.clone(),
|
||||||
@ -85,6 +89,7 @@ impl KclError {
|
|||||||
KclError::Lexical(e) => &e.message,
|
KclError::Lexical(e) => &e.message,
|
||||||
KclError::Syntax(e) => &e.message,
|
KclError::Syntax(e) => &e.message,
|
||||||
KclError::Semantic(e) => &e.message,
|
KclError::Semantic(e) => &e.message,
|
||||||
|
KclError::ImportCycle(e) => &e.message,
|
||||||
KclError::Type(e) => &e.message,
|
KclError::Type(e) => &e.message,
|
||||||
KclError::Unimplemented(e) => &e.message,
|
KclError::Unimplemented(e) => &e.message,
|
||||||
KclError::Unexpected(e) => &e.message,
|
KclError::Unexpected(e) => &e.message,
|
||||||
@ -102,6 +107,7 @@ impl KclError {
|
|||||||
KclError::Lexical(e) => e.source_ranges = source_ranges,
|
KclError::Lexical(e) => e.source_ranges = source_ranges,
|
||||||
KclError::Syntax(e) => e.source_ranges = source_ranges,
|
KclError::Syntax(e) => e.source_ranges = source_ranges,
|
||||||
KclError::Semantic(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::Type(e) => e.source_ranges = source_ranges,
|
||||||
KclError::Unimplemented(e) => e.source_ranges = source_ranges,
|
KclError::Unimplemented(e) => e.source_ranges = source_ranges,
|
||||||
KclError::Unexpected(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::Lexical(e) => e.source_ranges.extend(source_ranges),
|
||||||
KclError::Syntax(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::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::Type(e) => e.source_ranges.extend(source_ranges),
|
||||||
KclError::Unimplemented(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),
|
KclError::Unexpected(e) => e.source_ranges.extend(source_ranges),
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
//! The executor for the AST.
|
//! The executor for the AST.
|
||||||
|
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
@ -23,12 +26,12 @@ type Point3D = kcmc::shared::Point3d<f64>;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::types::{
|
ast::types::{
|
||||||
human_friendly_type, BodyItem, Expr, ExpressionStatement, FunctionExpression, KclNone, Program,
|
human_friendly_type, BodyItem, Expr, ExpressionStatement, FunctionExpression, ImportStatement, ItemVisibility,
|
||||||
ReturnStatement, TagDeclarator,
|
KclNone, Program, ReturnStatement, TagDeclarator,
|
||||||
},
|
},
|
||||||
engine::EngineManager,
|
engine::{EngineManager, ExecutionKind},
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
fs::FileManager,
|
fs::{FileManager, FileSystem},
|
||||||
settings::types::UnitLength,
|
settings::types::UnitLength,
|
||||||
std::{FnAsArg, StdLib},
|
std::{FnAsArg, StdLib},
|
||||||
};
|
};
|
||||||
@ -47,6 +50,14 @@ pub struct ExecState {
|
|||||||
/// The current value of the pipe operator returned from the previous
|
/// The current value of the pipe operator returned from the previous
|
||||||
/// expression. If we're not currently in a pipeline, this will be None.
|
/// expression. If we're not currently in a pipeline, this will be None.
|
||||||
pub pipe_value: Option<KclValue>,
|
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)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
@ -391,6 +402,20 @@ impl KclValue {
|
|||||||
KclValue::Face(_) => "Face",
|
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 {
|
impl From<SketchSet> for KclValue {
|
||||||
@ -452,6 +477,15 @@ pub enum Geometries {
|
|||||||
Solids(Vec<Box<Solid>>),
|
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.
|
/// A sketch or a group of sketches.
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
#[ts(export)]
|
#[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 {
|
impl From<&ExpressionStatement> for Metadata {
|
||||||
fn from(exp_statement: &ExpressionStatement) -> Self {
|
fn from(exp_statement: &ExpressionStatement) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -1958,8 +2000,9 @@ impl ExecutorContext {
|
|||||||
program: &crate::ast::types::Program,
|
program: &crate::ast::types::Program,
|
||||||
memory: Option<ProgramMemory>,
|
memory: Option<ProgramMemory>,
|
||||||
id_generator: IdGenerator,
|
id_generator: IdGenerator,
|
||||||
|
project_directory: Option<String>,
|
||||||
) -> Result<ExecState, KclError> {
|
) -> Result<ExecState, KclError> {
|
||||||
self.run_with_session_data(program, memory, id_generator)
|
self.run_with_session_data(program, memory, id_generator, project_directory)
|
||||||
.await
|
.await
|
||||||
.map(|x| x.0)
|
.map(|x| x.0)
|
||||||
}
|
}
|
||||||
@ -1971,6 +2014,7 @@ impl ExecutorContext {
|
|||||||
program: &crate::ast::types::Program,
|
program: &crate::ast::types::Program,
|
||||||
memory: Option<ProgramMemory>,
|
memory: Option<ProgramMemory>,
|
||||||
id_generator: IdGenerator,
|
id_generator: IdGenerator,
|
||||||
|
project_directory: Option<String>,
|
||||||
) -> Result<(ExecState, Option<ModelingSessionData>), KclError> {
|
) -> Result<(ExecState, Option<ModelingSessionData>), KclError> {
|
||||||
let memory = if let Some(memory) = memory {
|
let memory = if let Some(memory) = memory {
|
||||||
memory.clone()
|
memory.clone()
|
||||||
@ -1980,6 +2024,7 @@ impl ExecutorContext {
|
|||||||
let mut exec_state = ExecState {
|
let mut exec_state = ExecState {
|
||||||
memory,
|
memory,
|
||||||
id_generator,
|
id_generator,
|
||||||
|
project_directory,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
// Before we even start executing the program, set the units.
|
// Before we even start executing the program, set the units.
|
||||||
@ -2018,6 +2063,91 @@ impl ExecutorContext {
|
|||||||
// Iterate over the body of the program.
|
// Iterate over the body of the program.
|
||||||
for statement in &program.body {
|
for statement in &program.body {
|
||||||
match statement {
|
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) => {
|
BodyItem::ExpressionStatement(expression_statement) => {
|
||||||
let metadata = Metadata::from(expression_statement);
|
let metadata = Metadata::from(expression_statement);
|
||||||
last_expr = Some(
|
last_expr = Some(
|
||||||
@ -2044,7 +2174,21 @@ impl ExecutorContext {
|
|||||||
StatementKind::Declaration { name: &var_name },
|
StatementKind::Declaration { name: &var_name },
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
let is_function = memory_item.is_function();
|
||||||
exec_state.memory.add(&var_name, memory_item, source_range)?;
|
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;
|
last_expr = None;
|
||||||
}
|
}
|
||||||
@ -2130,6 +2274,7 @@ impl ExecutorContext {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?,
|
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::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?,
|
||||||
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
|
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
|
||||||
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
|
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
|
||||||
@ -2148,8 +2293,9 @@ impl ExecutorContext {
|
|||||||
&self,
|
&self,
|
||||||
program: &Program,
|
program: &Program,
|
||||||
id_generator: IdGenerator,
|
id_generator: IdGenerator,
|
||||||
|
project_directory: Option<String>,
|
||||||
) -> Result<TakeSnapshot> {
|
) -> Result<TakeSnapshot> {
|
||||||
let _ = self.run(program, None, id_generator).await?;
|
let _ = self.run(program, None, id_generator, project_directory).await?;
|
||||||
|
|
||||||
// Zoom to fit.
|
// Zoom to fit.
|
||||||
self.engine
|
self.engine
|
||||||
@ -2294,7 +2440,7 @@ mod tests {
|
|||||||
settings: Default::default(),
|
settings: Default::default(),
|
||||||
context_type: ContextType::Mock,
|
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)
|
Ok(exec_state.memory)
|
||||||
}
|
}
|
||||||
|
@ -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>(
|
async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
|
||||||
&self,
|
&self,
|
||||||
path: P,
|
path: P,
|
||||||
|
@ -23,6 +23,13 @@ pub trait FileSystem: Clone {
|
|||||||
source_range: crate::executor::SourceRange,
|
source_range: crate::executor::SourceRange,
|
||||||
) -> Result<Vec<u8>, crate::errors::KclError>;
|
) -> 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.
|
/// Check if a file exists on the local file system.
|
||||||
async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
|
async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
|
||||||
&self,
|
&self,
|
||||||
|
@ -78,6 +78,22 @@ impl FileSystem for FileManager {
|
|||||||
Ok(bytes)
|
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>(
|
async fn exists<P: AsRef<std::path::Path> + std::marker::Send + std::marker::Sync>(
|
||||||
&self,
|
&self,
|
||||||
path: P,
|
path: P,
|
||||||
|
@ -596,7 +596,7 @@ impl Backend {
|
|||||||
.clear_scene(&mut id_generator, SourceRange::default())
|
.clear_scene(&mut id_generator, SourceRange::default())
|
||||||
.await?;
|
.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,
|
Ok(exec_state) => exec_state,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.memory_map.remove(params.uri.as_str());
|
self.memory_map.remove(params.uri.as_str());
|
||||||
@ -1123,7 +1123,7 @@ impl LanguageServer for Backend {
|
|||||||
return Ok(None);
|
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);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,6 +12,13 @@ pub(crate) mod parser_impl;
|
|||||||
pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%";
|
pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%";
|
||||||
pub const PIPE_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 struct Parser {
|
||||||
pub tokens: Vec<Token>,
|
pub tokens: Vec<Token>,
|
||||||
pub unknown_tokens: Vec<Token>,
|
pub unknown_tokens: Vec<Token>,
|
||||||
|
@ -10,12 +10,12 @@ use winnow::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::types::{
|
ast::types::{
|
||||||
ArrayExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, CommentStyle, ElseIf,
|
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
|
||||||
Expr, ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier, IfExpression, Literal,
|
CommentStyle, ElseIf, Expr, ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier,
|
||||||
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, NonCodeMeta, NonCodeNode, NonCodeValue,
|
IfExpression, ImportItem, ImportStatement, ItemVisibility, Literal, LiteralIdentifier, LiteralValue,
|
||||||
ObjectExpression, ObjectProperty, Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement,
|
MemberExpression, MemberObject, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression, ObjectProperty,
|
||||||
TagDeclarator, UnaryExpression, UnaryOperator, ValueMeta, VariableDeclaration, VariableDeclarator,
|
Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement, TagDeclarator, UnaryExpression,
|
||||||
VariableKind,
|
UnaryOperator, ValueMeta, VariableDeclaration, VariableDeclarator, VariableKind,
|
||||||
},
|
},
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
executor::SourceRange,
|
executor::SourceRange,
|
||||||
@ -303,6 +303,12 @@ fn binary_operator(i: TokenSlice) -> PResult<BinaryOperator> {
|
|||||||
"*" => BinaryOperator::Mul,
|
"*" => BinaryOperator::Mul,
|
||||||
"%" => BinaryOperator::Mod,
|
"%" => BinaryOperator::Mod,
|
||||||
"^" => BinaryOperator::Pow,
|
"^" => BinaryOperator::Pow,
|
||||||
|
"==" => BinaryOperator::Eq,
|
||||||
|
"!=" => BinaryOperator::Neq,
|
||||||
|
">" => BinaryOperator::Gt,
|
||||||
|
">=" => BinaryOperator::Gte,
|
||||||
|
"<" => BinaryOperator::Lt,
|
||||||
|
"<=" => BinaryOperator::Lte,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(KclError::Syntax(KclErrorDetails {
|
return Err(KclError::Syntax(KclErrorDetails {
|
||||||
source_ranges: token.as_source_ranges(),
|
source_ranges: token.as_source_ranges(),
|
||||||
@ -330,6 +336,7 @@ fn operand(i: TokenSlice) -> PResult<BinaryPart> {
|
|||||||
| Expr::PipeExpression(_)
|
| Expr::PipeExpression(_)
|
||||||
| Expr::PipeSubstitution(_)
|
| Expr::PipeSubstitution(_)
|
||||||
| Expr::ArrayExpression(_)
|
| Expr::ArrayExpression(_)
|
||||||
|
| Expr::ArrayRangeExpression(_)
|
||||||
| Expr::ObjectExpression(_) => {
|
| Expr::ObjectExpression(_) => {
|
||||||
return Err(KclError::Syntax(KclErrorDetails {
|
return Err(KclError::Syntax(KclErrorDetails {
|
||||||
source_ranges,
|
source_ranges,
|
||||||
@ -460,8 +467,13 @@ pub enum NonCodeOr<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a KCL array of elements.
|
/// Parse a KCL array of elements.
|
||||||
fn array(i: TokenSlice) -> PResult<ArrayExpression> {
|
fn array(i: TokenSlice) -> PResult<Expr> {
|
||||||
alt((array_empty, array_elem_by_elem, array_end_start)).parse_next(i)
|
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.
|
/// 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;
|
let start = open_bracket(i)?.start;
|
||||||
ignore_whitespace(i);
|
ignore_whitespace(i);
|
||||||
let elements = integer_range
|
let start_element = Box::new(expression.parse_next(i)?);
|
||||||
.context(expected("array contents, a numeric range (like 0..10)"))
|
ignore_whitespace(i);
|
||||||
.parse_next(i)?;
|
double_period.parse_next(i)?;
|
||||||
|
ignore_whitespace(i);
|
||||||
|
let end_element = Box::new(expression.parse_next(i)?);
|
||||||
ignore_whitespace(i);
|
ignore_whitespace(i);
|
||||||
let end = close_bracket(i)?.end;
|
let end = close_bracket(i)?.end;
|
||||||
Ok(ArrayExpression {
|
Ok(ArrayRangeExpression {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
elements,
|
start_element,
|
||||||
non_code_meta: Default::default(),
|
end_element,
|
||||||
|
end_inclusive: true,
|
||||||
digest: None,
|
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> {
|
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)?;
|
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
|
colon
|
||||||
.context(expected(
|
.context(expected(
|
||||||
"a colon, which separates the property's key from the value you're setting it to, e.g. 'height: 4'",
|
"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.
|
// 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.
|
// If there is a comment, it may be preceded by whitespace.
|
||||||
let item = dispatch! {peek(any);
|
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),
|
(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" =>
|
Token { ref value, .. } if value == "return" =>
|
||||||
(return_stmt.map(BodyItem::ReturnStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
|
(return_stmt.map(BodyItem::ReturnStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
|
||||||
token if !token.is_code_token() => {
|
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`.
|
/// Parse a return statement of a user-defined function, e.g. `return x`.
|
||||||
pub fn return_stmt(i: TokenSlice) -> PResult<ReturnStatement> {
|
pub fn return_stmt(i: TokenSlice) -> PResult<ReturnStatement> {
|
||||||
let start = any
|
let start = any
|
||||||
@ -1189,7 +1293,7 @@ fn expr_allowed_in_pipe_expr(i: TokenSlice) -> PResult<Expr> {
|
|||||||
literal.map(Box::new).map(Expr::Literal),
|
literal.map(Box::new).map(Expr::Literal),
|
||||||
fn_call.map(Box::new).map(Expr::CallExpression),
|
fn_call.map(Box::new).map(Expr::CallExpression),
|
||||||
identifier.map(Box::new).map(Expr::Identifier),
|
identifier.map(Box::new).map(Expr::Identifier),
|
||||||
array.map(Box::new).map(Expr::ArrayExpression),
|
array,
|
||||||
object.map(Box::new).map(Expr::ObjectExpression),
|
object.map(Box::new).map(Expr::ObjectExpression),
|
||||||
pipe_sub.map(Box::new).map(Expr::PipeSubstitution),
|
pipe_sub.map(Box::new).map(Expr::PipeSubstitution),
|
||||||
function_expression.map(Box::new).map(Expr::FunctionExpression),
|
function_expression.map(Box::new).map(Expr::FunctionExpression),
|
||||||
@ -1217,6 +1321,19 @@ fn possible_operands(i: TokenSlice) -> PResult<Expr> {
|
|||||||
.parse_next(i)
|
.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)> {
|
fn declaration_keyword(i: TokenSlice) -> PResult<(VariableKind, Token)> {
|
||||||
let res = any
|
let res = any
|
||||||
.verify_map(|token: Token| token.declaration_keyword().map(|kw| (kw, token)))
|
.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.
|
/// Parse a variable/constant declaration.
|
||||||
fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
|
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)?;
|
let decl_token = opt(declaration_keyword).parse_next(i)?;
|
||||||
if decl_token.is_some() {
|
if decl_token.is_some() {
|
||||||
// If there was a declaration keyword like `fn`, then it must be followed by some spaces.
|
// 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",
|
"an identifier, which becomes name you're binding the value to",
|
||||||
))
|
))
|
||||||
.parse_next(i)?;
|
.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)
|
(*kind, token.start, token.end)
|
||||||
} else {
|
} else {
|
||||||
(VariableKind::Const, id.start(), id.end())
|
(VariableKind::Const, id.start(), id.end())
|
||||||
};
|
};
|
||||||
|
if let Some(token) = visibility_token {
|
||||||
|
start = token.start;
|
||||||
|
}
|
||||||
|
|
||||||
ignore_whitespace(i);
|
ignore_whitespace(i);
|
||||||
equals(i)?;
|
equals(i)?;
|
||||||
@ -1291,6 +1414,7 @@ fn declaration(i: TokenSlice) -> PResult<VariableDeclaration> {
|
|||||||
init: val,
|
init: val,
|
||||||
digest: None,
|
digest: None,
|
||||||
}],
|
}],
|
||||||
|
visibility,
|
||||||
kind,
|
kind,
|
||||||
digest: None,
|
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.
|
/// Parse the given brace symbol.
|
||||||
fn some_brace(symbol: &'static str, i: TokenSlice) -> PResult<Token> {
|
fn some_brace(symbol: &'static str, i: TokenSlice) -> PResult<Token> {
|
||||||
one_of((TokenType::Brace, symbol))
|
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]
|
#[test]
|
||||||
fn test_error_keyword_in_variable() {
|
fn test_error_keyword_in_variable() {
|
||||||
let some_program_string = r#"const let = "thing""#;
|
let some_program_string = r#"const let = "thing""#;
|
||||||
@ -3705,7 +3693,10 @@ const my14 = 4 ^ 2 - 3 ^ 2 * 2
|
|||||||
5
|
5
|
||||||
}"#
|
}"#
|
||||||
);
|
);
|
||||||
|
snapshot_test!(be, "let x = 3 == 3");
|
||||||
|
snapshot_test!(bf, "let x = 3 != 3");
|
||||||
snapshot_test!(bg, r#"x = 4"#);
|
snapshot_test!(bg, r#"x = 4"#);
|
||||||
|
snapshot_test!(bh, "const obj = {center : [10, 10], radius: 5}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
@ -24,111 +24,29 @@ expression: actual
|
|||||||
"digest": null
|
"digest": null
|
||||||
},
|
},
|
||||||
"init": {
|
"init": {
|
||||||
"type": "ArrayExpression",
|
"type": "ArrayRangeExpression",
|
||||||
"type": "ArrayExpression",
|
"type": "ArrayRangeExpression",
|
||||||
"start": 16,
|
"start": 16,
|
||||||
"end": 23,
|
"end": 23,
|
||||||
"elements": [
|
"startElement": {
|
||||||
{
|
"type": "Literal",
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
"type": "Literal",
|
"start": 17,
|
||||||
"start": 17,
|
"end": 18,
|
||||||
"end": 18,
|
"value": 0,
|
||||||
"value": 0,
|
"raw": "0",
|
||||||
"raw": "0",
|
"digest": null
|
||||||
"digest": null
|
},
|
||||||
},
|
"endElement": {
|
||||||
{
|
"type": "Literal",
|
||||||
"type": "Literal",
|
"type": "Literal",
|
||||||
"type": "Literal",
|
"start": 20,
|
||||||
"start": 17,
|
"end": 22,
|
||||||
"end": 18,
|
"value": 10,
|
||||||
"value": 1,
|
"raw": "10",
|
||||||
"raw": "1",
|
"digest": null
|
||||||
"digest": null
|
},
|
||||||
},
|
"endInclusive": true,
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"digest": null
|
"digest": null
|
||||||
},
|
},
|
||||||
"digest": null
|
"digest": null
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
source: kcl/src/parser/parser_impl.rs
|
source: kcl/src/parser/parser_impl.rs
|
||||||
assertion_line: 3423
|
|
||||||
expression: actual
|
expression: actual
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
source: kcl/src/parser/parser_impl.rs
|
source: kcl/src/parser/parser_impl.rs
|
||||||
assertion_line: 3470
|
|
||||||
expression: actual
|
expression: actual
|
||||||
---
|
---
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -7,7 +7,7 @@ use serde::de::DeserializeOwned;
|
|||||||
use serde_json::Value as JValue;
|
use serde_json::Value as JValue;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::types::{parse_json_number_as_f64, TagDeclarator},
|
ast::types::{execute::parse_json_number_as_f64, TagDeclarator},
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
executor::{
|
executor::{
|
||||||
ExecState, ExecutorContext, ExtrudeSurface, KclValue, Metadata, Sketch, SketchSet, SketchSurface, Solid,
|
ExecState, ExecutorContext, ExtrudeSurface, KclValue, Metadata, Sketch, SketchSet, SketchSurface, Solid,
|
||||||
|
@ -28,6 +28,18 @@ pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kcl
|
|||||||
memory: *f.memory,
|
memory: *f.memory,
|
||||||
};
|
};
|
||||||
let new_array = inner_map(array, map_fn, exec_state, &args).await?;
|
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);
|
let uv = UserVal::new(vec![args.source_range.into()], new_array);
|
||||||
Ok(KclValue::UserVal(uv))
|
Ok(KclValue::UserVal(uv))
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
//! Standard library patterns.
|
//! Standard library patterns.
|
||||||
|
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use derive_docs::stdlib;
|
use derive_docs::stdlib;
|
||||||
use kcmc::{
|
use kcmc::{
|
||||||
@ -23,15 +25,18 @@ use crate::{
|
|||||||
std::{types::Uint, Args},
|
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.
|
/// Data for a linear pattern on a 2D sketch.
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct LinearPattern2dData {
|
pub struct LinearPattern2dData {
|
||||||
/// The number of repetitions. Must be greater than 0.
|
/// The number of total instances. Must be greater than or equal to 1.
|
||||||
/// This excludes the original entity. For example, if `repetitions` is 1,
|
/// This includes the original entity. For example, if instances is 2,
|
||||||
/// the original entity will be copied once.
|
/// there will be two copies -- the original, and one new copy.
|
||||||
pub repetitions: Uint,
|
/// If instances is 1, this has no effect.
|
||||||
|
pub instances: Uint,
|
||||||
/// The distance between each repetition. This can also be referred to as spacing.
|
/// The distance between each repetition. This can also be referred to as spacing.
|
||||||
pub distance: f64,
|
pub distance: f64,
|
||||||
/// The axis of the pattern. This is a 2D vector.
|
/// The axis of the pattern. This is a 2D vector.
|
||||||
@ -43,10 +48,11 @@ pub struct LinearPattern2dData {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct LinearPattern3dData {
|
pub struct LinearPattern3dData {
|
||||||
/// The number of repetitions. Must be greater than 0.
|
/// The number of total instances. Must be greater than or equal to 1.
|
||||||
/// This excludes the original entity. For example, if `repetitions` is 1,
|
/// This includes the original entity. For example, if instances is 2,
|
||||||
/// the original entity will be copied once.
|
/// there will be two copies -- the original, and one new copy.
|
||||||
pub repetitions: Uint,
|
/// If instances is 1, this has no effect.
|
||||||
|
pub instances: Uint,
|
||||||
/// The distance between each repetition. This can also be referred to as spacing.
|
/// The distance between each repetition. This can also be referred to as spacing.
|
||||||
pub distance: f64,
|
pub distance: f64,
|
||||||
/// The axis of the pattern.
|
/// The axis of the pattern.
|
||||||
@ -66,11 +72,12 @@ impl LinearPattern {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn repetitions(&self) -> u32 {
|
fn repetitions(&self) -> RepetitionsNeeded {
|
||||||
match self {
|
let n = match self {
|
||||||
LinearPattern::TwoD(lp) => lp.repetitions.u32(),
|
LinearPattern::TwoD(lp) => lp.instances.u32(),
|
||||||
LinearPattern::ThreeD(lp) => lp.repetitions.u32(),
|
LinearPattern::ThreeD(lp) => lp.instances.u32(),
|
||||||
}
|
};
|
||||||
|
RepetitionsNeeded::from(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn distance(&self) -> f64 {
|
pub fn distance(&self) -> f64 {
|
||||||
@ -278,6 +285,12 @@ async fn inner_pattern_transform<'a>(
|
|||||||
) -> Result<Vec<Box<Solid>>, KclError> {
|
) -> Result<Vec<Box<Solid>>, KclError> {
|
||||||
// Build the vec of transforms, one for each repetition.
|
// Build the vec of transforms, one for each repetition.
|
||||||
let mut transform = Vec::with_capacity(usize::try_from(total_instances).unwrap());
|
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 {
|
for i in 1..total_instances {
|
||||||
let t = make_transform(i, &transform_function, args.source_range, exec_state).await?;
|
let t = make_transform(i, &transform_function, args.source_range, exec_state).await?;
|
||||||
transform.push(t);
|
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 }, %)
|
/// |> circle({ center: [0, 0], radius: 1 }, %)
|
||||||
/// |> patternLinear2d({
|
/// |> patternLinear2d({
|
||||||
/// axis: [1, 0],
|
/// axis: [1, 0],
|
||||||
/// repetitions: 6,
|
/// instances: 7,
|
||||||
/// distance: 4
|
/// distance: 4
|
||||||
/// }, %)
|
/// }, %)
|
||||||
///
|
///
|
||||||
@ -573,7 +586,7 @@ pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result
|
|||||||
/// const example = extrude(1, exampleSketch)
|
/// const example = extrude(1, exampleSketch)
|
||||||
/// |> patternLinear3d({
|
/// |> patternLinear3d({
|
||||||
/// axis: [1, 0, 1],
|
/// axis: [1, 0, 1],
|
||||||
/// repetitions: 6,
|
/// instances: 7,
|
||||||
/// distance: 6
|
/// distance: 6
|
||||||
/// }, %)
|
/// }, %)
|
||||||
/// ```
|
/// ```
|
||||||
@ -629,13 +642,26 @@ async fn pattern_linear(
|
|||||||
) -> Result<Geometries, KclError> {
|
) -> Result<Geometries, KclError> {
|
||||||
let id = exec_state.id_generator.next_uuid();
|
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
|
let resp = args
|
||||||
.send_modeling_cmd(
|
.send_modeling_cmd(
|
||||||
id,
|
id,
|
||||||
ModelingCmd::from(mcmd::EntityLinearPattern {
|
ModelingCmd::from(mcmd::EntityLinearPattern {
|
||||||
axis: kcmc::shared::Point3d::from(data.axis()),
|
axis: kcmc::shared::Point3d::from(data.axis()),
|
||||||
entity_id: geometry.id(),
|
entity_id: geometry.id(),
|
||||||
num_repetitions: data.repetitions(),
|
num_repetitions,
|
||||||
spacing: LengthUnit(data.distance()),
|
spacing: LengthUnit(data.distance()),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -680,10 +706,11 @@ async fn pattern_linear(
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CircularPattern2dData {
|
pub struct CircularPattern2dData {
|
||||||
/// The number of repetitions. Must be greater than 0.
|
/// The number of total instances. Must be greater than or equal to 1.
|
||||||
/// This excludes the original entity. For example, if `repetitions` is 1,
|
/// This includes the original entity. For example, if instances is 2,
|
||||||
/// the original entity will be copied once.
|
/// there will be two copies -- the original, and one new copy.
|
||||||
pub repetitions: Uint,
|
/// If instances is 1, this has no effect.
|
||||||
|
pub instances: Uint,
|
||||||
/// The center about which to make the pattern. This is a 2D vector.
|
/// The center about which to make the pattern. This is a 2D vector.
|
||||||
pub center: [f64; 2],
|
pub center: [f64; 2],
|
||||||
/// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
|
/// The arc angle (in degrees) to place the repetitions. Must be greater than 0.
|
||||||
@ -697,10 +724,11 @@ pub struct CircularPattern2dData {
|
|||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CircularPattern3dData {
|
pub struct CircularPattern3dData {
|
||||||
/// The number of repetitions. Must be greater than 0.
|
/// The number of total instances. Must be greater than or equal to 1.
|
||||||
/// This excludes the original entity. For example, if `repetitions` is 1,
|
/// This includes the original entity. For example, if instances is 2,
|
||||||
/// the original entity will be copied once.
|
/// there will be two copies -- the original, and one new copy.
|
||||||
pub repetitions: Uint,
|
/// If instances is 1, this has no effect.
|
||||||
|
pub instances: Uint,
|
||||||
/// The axis around which to make the pattern. This is a 3D vector.
|
/// The axis around which to make the pattern. This is a 3D vector.
|
||||||
pub axis: [f64; 3],
|
pub axis: [f64; 3],
|
||||||
/// The center about which to make the pattern. This is a 3D vector.
|
/// The center about which to make the pattern. This is a 3D vector.
|
||||||
@ -716,6 +744,25 @@ pub enum CircularPattern {
|
|||||||
TwoD(CircularPattern2dData),
|
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 {
|
impl CircularPattern {
|
||||||
pub fn axis(&self) -> [f64; 3] {
|
pub fn axis(&self) -> [f64; 3] {
|
||||||
match self {
|
match self {
|
||||||
@ -731,11 +778,12 @@ impl CircularPattern {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn repetitions(&self) -> u32 {
|
fn repetitions(&self) -> RepetitionsNeeded {
|
||||||
match self {
|
let n = match self {
|
||||||
CircularPattern::TwoD(lp) => lp.repetitions.u32(),
|
CircularPattern::TwoD(lp) => lp.instances.u32(),
|
||||||
CircularPattern::ThreeD(lp) => lp.repetitions.u32(),
|
CircularPattern::ThreeD(lp) => lp.instances.u32(),
|
||||||
}
|
};
|
||||||
|
RepetitionsNeeded::from(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn arc_degrees(&self) -> f64 {
|
pub fn arc_degrees(&self) -> f64 {
|
||||||
@ -775,7 +823,7 @@ pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Resu
|
|||||||
/// |> close(%)
|
/// |> close(%)
|
||||||
/// |> patternCircular2d({
|
/// |> patternCircular2d({
|
||||||
/// center: [0, 0],
|
/// center: [0, 0],
|
||||||
/// repetitions: 12,
|
/// instances: 13,
|
||||||
/// arcDegrees: 360,
|
/// arcDegrees: 360,
|
||||||
/// rotateDuplicates: true
|
/// rotateDuplicates: true
|
||||||
/// }, %)
|
/// }, %)
|
||||||
@ -841,7 +889,7 @@ pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Resu
|
|||||||
/// |> patternCircular3d({
|
/// |> patternCircular3d({
|
||||||
/// axis: [1, -1, 0],
|
/// axis: [1, -1, 0],
|
||||||
/// center: [10, -20, 0],
|
/// center: [10, -20, 0],
|
||||||
/// repetitions: 10,
|
/// instances: 11,
|
||||||
/// arcDegrees: 360,
|
/// arcDegrees: 360,
|
||||||
/// rotateDuplicates: true
|
/// rotateDuplicates: true
|
||||||
/// }, %)
|
/// }, %)
|
||||||
@ -897,6 +945,18 @@ async fn pattern_circular(
|
|||||||
args: Args,
|
args: Args,
|
||||||
) -> Result<Geometries, KclError> {
|
) -> Result<Geometries, KclError> {
|
||||||
let id = exec_state.id_generator.next_uuid();
|
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 center = data.center();
|
||||||
let resp = args
|
let resp = args
|
||||||
@ -910,7 +970,7 @@ async fn pattern_circular(
|
|||||||
y: LengthUnit(center[1]),
|
y: LengthUnit(center[1]),
|
||||||
z: LengthUnit(center[2]),
|
z: LengthUnit(center[2]),
|
||||||
},
|
},
|
||||||
num_repetitions: data.repetitions(),
|
num_repetitions,
|
||||||
arc_degrees: data.arc_degrees(),
|
arc_degrees: data.arc_degrees(),
|
||||||
rotate_duplicates: data.rotate_duplicates(),
|
rotate_duplicates: data.rotate_duplicates(),
|
||||||
}),
|
}),
|
||||||
|
@ -30,7 +30,7 @@ async fn do_execute_and_snapshot(ctx: &ExecutorContext, code: &str) -> anyhow::R
|
|||||||
let program = parser.ast()?;
|
let program = parser.ast()?;
|
||||||
|
|
||||||
let snapshot = ctx
|
let snapshot = ctx
|
||||||
.execute_and_prepare_snapshot(&program, IdGenerator::default())
|
.execute_and_prepare_snapshot(&program, IdGenerator::default(), None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Create a temporary file to write the output to.
|
// Create a temporary file to write the output to.
|
||||||
|