Compare commits
81 Commits
revert-706
...
v1.0.1
Author | SHA1 | Date | |
---|---|---|---|
034366e65e | |||
cd537cd9c2 | |||
22f92942f6 | |||
4eee50d79e | |||
125b2c44d4 | |||
db9e35d686 | |||
d0958220fe | |||
fa4b3cfd1b | |||
8972f8f109 | |||
85ccc6900c | |||
4e2deca5d8 | |||
04a2c184d7 | |||
2c7701e2d4 | |||
ae569b61db | |||
0a0e6abd3f | |||
9c7aee32bd | |||
ed979d807b | |||
f5c244dbb1 | |||
0ea1e9a6da | |||
a36530d6df | |||
825d34718a | |||
d90d445d84 | |||
5976a0cba6 | |||
b50f2f5a2a | |||
f877b52898 | |||
4a585db637 | |||
4d404bf137 | |||
e644b7e1fc | |||
aff1684064 | |||
d9afc50f91 | |||
eb7b4ccda6 | |||
bbf4f1d251 | |||
3cc7859ca5 | |||
ab63345c57 | |||
3df02e02fa | |||
35f5c62633 | |||
0f0fc39d07 | |||
a13b6b2b70 | |||
4212b95232 | |||
38a73a603b | |||
c48d9fd4d7 | |||
0753987b5a | |||
815ff7dc2b | |||
46684d420d | |||
eca09984a3 | |||
ce63c6423e | |||
09699afe82 | |||
36c8ad439d | |||
5dc77ceed5 | |||
c7baa26b2d | |||
4d0454abcd | |||
1dafbf105e | |||
773f013115 | |||
c5cd460595 | |||
845352046b | |||
597f1087f9 | |||
511334683a | |||
223a4ad45d | |||
edf31ec1d3 | |||
1539557005 | |||
1d3ba4e3ac | |||
4110aa00db | |||
7eb52cda36 | |||
7872fb9cbd | |||
651181e62c | |||
38a245f2fc | |||
1b4289f93f | |||
d0697c24fd | |||
8c24e29081 | |||
2b9d26e2ff | |||
ab148a7654 | |||
553e650fbe | |||
9690a24c68 | |||
978d5d44a2 | |||
9df476543a | |||
cf303ebe97 | |||
b1d1d89ca5 | |||
3a599d0a0a | |||
8340f6b906 | |||
ddb034b14d | |||
bfa2f67393 |
2
.github/ci-cd-scripts/upload-results.sh
vendored
@ -6,6 +6,7 @@ if [ -z "${TAB_API_URL:-}" ] || [ -z "${TAB_API_KEY:-}" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
project="https://github.com/KittyCAD/modeling-app"
|
project="https://github.com/KittyCAD/modeling-app"
|
||||||
|
suite="${CI_SUITE:-unit}"
|
||||||
branch="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME:-}}"
|
branch="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME:-}}"
|
||||||
commit="${CI_COMMIT_SHA:-${GITHUB_SHA:-}}"
|
commit="${CI_COMMIT_SHA:-${GITHUB_SHA:-}}"
|
||||||
|
|
||||||
@ -13,6 +14,7 @@ echo "Uploading batch results"
|
|||||||
curl --silent --request POST \
|
curl --silent --request POST \
|
||||||
--header "X-API-Key: ${TAB_API_KEY}" \
|
--header "X-API-Key: ${TAB_API_KEY}" \
|
||||||
--form "project=${project}" \
|
--form "project=${project}" \
|
||||||
|
--form "suite=${suite}" \
|
||||||
--form "branch=${branch}" \
|
--form "branch=${branch}" \
|
||||||
--form "commit=${commit}" \
|
--form "commit=${commit}" \
|
||||||
--form "tests=@test-results/junit.xml" \
|
--form "tests=@test-results/junit.xml" \
|
||||||
|
13
.github/workflows/cargo-test.yml
vendored
@ -1,16 +1,22 @@
|
|||||||
|
name: cargo test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: 0 * * * * # hourly
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
name: cargo test
|
|
||||||
jobs:
|
jobs:
|
||||||
build-test-artifacts:
|
build-test-artifacts:
|
||||||
name: Build test artifacts
|
name: Build test artifacts
|
||||||
@ -88,6 +94,7 @@ jobs:
|
|||||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
|
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
|
||||||
ZOO_HOST: https://api.dev.zoo.dev
|
ZOO_HOST: https://api.dev.zoo.dev
|
||||||
RUST_BACKTRACE: full
|
RUST_BACKTRACE: full
|
||||||
|
RUST_MIN_STACK: 10485760000
|
||||||
- name: Commit differences
|
- name: Commit differences
|
||||||
if: steps.path-changes.outputs.outside-kcl-samples == 'false' && steps.cargo-test-kcl-samples.outcome == 'failure'
|
if: steps.path-changes.outputs.outside-kcl-samples == 'false' && steps.cargo-test-kcl-samples.outcome == 'failure'
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -119,6 +126,7 @@ jobs:
|
|||||||
# Configure nextest when it's run by insta (via just).
|
# Configure nextest when it's run by insta (via just).
|
||||||
NEXTEST_PROFILE: ci
|
NEXTEST_PROFILE: ci
|
||||||
RUST_BACKTRACE: full
|
RUST_BACKTRACE: full
|
||||||
|
RUST_MIN_STACK: 10485760000
|
||||||
- name: Build and archive tests
|
- name: Build and archive tests
|
||||||
run: |
|
run: |
|
||||||
cd rust
|
cd rust
|
||||||
@ -182,6 +190,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
|
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
|
||||||
ZOO_HOST: https://api.dev.zoo.dev
|
ZOO_HOST: https://api.dev.zoo.dev
|
||||||
|
RUST_MIN_STACK: 10485760000
|
||||||
- name: Upload results
|
- name: Upload results
|
||||||
if: always()
|
if: always()
|
||||||
run: .github/ci-cd-scripts/upload-results.sh
|
run: .github/ci-cd-scripts/upload-results.sh
|
||||||
@ -190,6 +199,7 @@ jobs:
|
|||||||
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
|
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
|
||||||
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
|
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
CI_SUITE: e2e:kcl
|
||||||
run-internal-kcl-samples:
|
run-internal-kcl-samples:
|
||||||
name: cargo test (internal-kcl-samples)
|
name: cargo test (internal-kcl-samples)
|
||||||
runs-on:
|
runs-on:
|
||||||
@ -238,6 +248,7 @@ jobs:
|
|||||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
|
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
|
||||||
ZOO_HOST: https://api.dev.zoo.dev
|
ZOO_HOST: https://api.dev.zoo.dev
|
||||||
MODELING_APP_INTERNAL_SAMPLES_SECRET: ${{secrets.MODELING_APP_INTERNAL_SAMPLES_SECRET}}
|
MODELING_APP_INTERNAL_SAMPLES_SECRET: ${{secrets.MODELING_APP_INTERNAL_SAMPLES_SECRET}}
|
||||||
|
RUST_MIN_STACK: 10485760000
|
||||||
run-wasm-tests:
|
run-wasm-tests:
|
||||||
name: Run wasm tests
|
name: Run wasm tests
|
||||||
strategy:
|
strategy:
|
||||||
|
19
.github/workflows/e2e-tests.yml
vendored
@ -1,4 +1,5 @@
|
|||||||
name: E2E Tests
|
name: E2E Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@ -143,7 +144,7 @@ jobs:
|
|||||||
- name: Install browsers
|
- name: Install browsers
|
||||||
run: npm run playwright install --with-deps
|
run: npm run playwright install --with-deps
|
||||||
|
|
||||||
- name: Capture snapshots
|
- name: Test snapshots
|
||||||
uses: nick-fields/retry@v3.0.2
|
uses: nick-fields/retry@v3.0.2
|
||||||
with:
|
with:
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -156,6 +157,19 @@ jobs:
|
|||||||
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
|
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
|
||||||
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
|
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
CI_SUITE: e2e:snapshots
|
||||||
|
TARGET: web
|
||||||
|
|
||||||
|
- name: Update snapshots
|
||||||
|
if: always()
|
||||||
|
run: npm run test:snapshots -- --last-failed --update-snapshots
|
||||||
|
env:
|
||||||
|
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
|
TAB_API_URL: ${{ secrets.TAB_API_URL }}
|
||||||
|
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
|
||||||
|
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
CI_SUITE: e2e:snapshots
|
||||||
TARGET: web
|
TARGET: web
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
@ -173,7 +187,7 @@ jobs:
|
|||||||
id: git-check
|
id: git-check
|
||||||
run: |
|
run: |
|
||||||
git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots
|
git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots
|
||||||
if git status | grep -q "Changes to be committed"
|
if git status | grep --quiet "Changes to be committed"
|
||||||
then echo "modified=true" >> $GITHUB_OUTPUT
|
then echo "modified=true" >> $GITHUB_OUTPUT
|
||||||
else echo "modified=false" >> $GITHUB_OUTPUT
|
else echo "modified=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
@ -306,6 +320,7 @@ jobs:
|
|||||||
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
|
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
|
||||||
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
|
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
CI_SUITE: e2e:desktop
|
||||||
TARGET: desktop
|
TARGET: desktop
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
|
@ -122,12 +122,11 @@ https://github.com/KittyCAD/modeling-app/issues/new
|
|||||||
|
|
||||||
#### 2. Push a new tag
|
#### 2. Push a new tag
|
||||||
|
|
||||||
Create a new tag and push it to the repo. The `semantic-release.sh` script will automatically bump the minor part, which we use the most. For instance going from `v0.27.0` to `v0.28.0`.
|
Decide on a `v`-prefixed semver `VERSION` (eg. `v1.2.3`) with the team and tag the repo, eg. on latest main:
|
||||||
|
|
||||||
```
|
```
|
||||||
VERSION=$(./scripts/semantic-release.sh)
|
|
||||||
git tag $VERSION
|
git tag $VERSION
|
||||||
git push origin --tags
|
git push origin $VERSION
|
||||||
```
|
```
|
||||||
|
|
||||||
This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files.
|
This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files.
|
||||||
|
@ -4,7 +4,7 @@ Compared to other CAD software, getting Zoo Design Studio up and running is quic
|
|||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
|
||||||
1. Download the [Zoo Design Studio installer](https://zoo.dev/modeling-app/download) for Windows and for your processor type.
|
1. Download the [Zoo Design Studio installer](https://zoo.dev/design-studio/download) for Windows and for your processor type.
|
||||||
|
|
||||||
2. Once downloaded, run the installer `Zoo Design Studio-{version}-{arch}-win.exe` which should take a few seconds.
|
2. Once downloaded, run the installer `Zoo Design Studio-{version}-{arch}-win.exe` which should take a few seconds.
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ Compared to other CAD software, getting Zoo Design Studio up and running is quic
|
|||||||
|
|
||||||
## macOS
|
## macOS
|
||||||
|
|
||||||
1. Download the [Zoo Design Studio installer](https://zoo.dev/modeling-app/download) for macOS and for your processor type.
|
1. Download the [Zoo Design Studio installer](https://zoo.dev/design-studio/download) for macOS and for your processor type.
|
||||||
|
|
||||||
2. Once downloaded, open the disk image `Zoo Design Studio-{version}-{arch}-mac.dmg` and drag the applications to your `Applications` directory.
|
2. Once downloaded, open the disk image `Zoo Design Studio-{version}-{arch}-mac.dmg` and drag the applications to your `Applications` directory.
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ Compared to other CAD software, getting Zoo Design Studio up and running is quic
|
|||||||
|
|
||||||
## Linux
|
## Linux
|
||||||
|
|
||||||
1. Download the [Zoo Design Studio installer](https://zoo.dev/modeling-app/download) for Linux and for your processor type.
|
1. Download the [Zoo Design Studio installer](https://zoo.dev/design-studio/download) for Linux and for your processor type.
|
||||||
|
|
||||||
2. Install the dependencies needed to run the [AppImage format](https://appimage.org/).
|
2. Install the dependencies needed to run the [AppImage format](https://appimage.org/).
|
||||||
- On Ubuntu, install the FUSE library with these commands in a terminal.
|
- On Ubuntu, install the FUSE library with these commands in a terminal.
|
||||||
|
10
README.md
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Zoo Design Studio
|
# Zoo Design Studio
|
||||||
|
|
||||||
[zoo.dev/modeling-app](https://zoo.dev/modeling-app)
|
[zoo.dev/design-studio](https://zoo.dev/design-studio)
|
||||||
|
|
||||||
A CAD application from the future, brought to you by the [Zoo team](https://zoo.dev).
|
A CAD application from the future, brought to you by the [Zoo team](https://zoo.dev).
|
||||||
|
|
||||||
@ -40,12 +40,8 @@ The 3D view in Design Studio is just a video stream from our hosted geometry eng
|
|||||||
|
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
We recommend downloading the latest application binary from our [releases](https://github.com/KittyCAD/modeling-app/releases) page. If you don't see your platform or architecture supported there, please file an issue.
|
We recommend downloading the latest application binary from our [website](https://zoo.dev/design-studio/download). If you don't see your platform or architecture supported there, please file an issue. See the [installation guide](INSTALL.md) for additional instructions.
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
Finally, if you'd like to run a development build or contribute to the project, please visit our [contributor guide](CONTRIBUTING.md) to get started.
|
Finally, if you'd like to run a development build or contribute to the project, please visit our [contributor guide](CONTRIBUTING.md) to get started. To contribute to the KittyCAD Language, see the dedicated [readme](rust/kcl-lib/README.md) for KCL.
|
||||||
|
|
||||||
## KCL
|
|
||||||
|
|
||||||
To contribute to the KittyCAD Language, see the [README](https://github.com/KittyCAD/modeling-app/tree/main/rust/kcl-lib) for KCL.
|
|
||||||
|
@ -4,7 +4,7 @@ excerpt: "Documentation of the KCL language for the Zoo Design Studio."
|
|||||||
layout: manual
|
layout: manual
|
||||||
---
|
---
|
||||||
|
|
||||||
This is a reference for KCL. If you are learning KCL, you may prefer the [guide]() which explains
|
This is a reference for KCL. If you are learning KCL, you may prefer the [guide](https://zoo.dev/docs/kcl-book/intro.html) which explains
|
||||||
things in a more tutorial fashion. See also our documentation of the [standard library](/docs/kcl-std).
|
things in a more tutorial fashion. See also our documentation of the [standard library](/docs/kcl-std).
|
||||||
|
|
||||||
## Topics
|
## Topics
|
||||||
|
@ -27,9 +27,6 @@ import increment from "util.kcl"
|
|||||||
answer = increment(41)
|
answer = increment(41)
|
||||||
```
|
```
|
||||||
|
|
||||||
Imported files _must_ be in the same project so that units are uniform across
|
|
||||||
modules. This means that it must be in the same directory.
|
|
||||||
|
|
||||||
Import statements must be at the top-level of a file. It is not allowed to have
|
Import statements must be at the top-level of a file. It is not allowed to have
|
||||||
an `import` statement inside a function or in the body of an if‑else.
|
an `import` statement inside a function or in the body of an if‑else.
|
||||||
|
|
||||||
@ -58,6 +55,9 @@ Imported symbols can be renamed for convenience or to avoid name collisions.
|
|||||||
import increment as inc, decrement as dec from "util.kcl"
|
import increment as inc, decrement as dec from "util.kcl"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can import files from the current directory or from subdirectories, but if importing from a
|
||||||
|
subdirectory you can only import `main.kcl`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Functions vs `clone`
|
## Functions vs `clone`
|
||||||
@ -229,6 +229,19 @@ The final statement is what's important because it's the return value of the
|
|||||||
entire module. The module is expected to return a single object that can be used
|
entire module. The module is expected to return a single object that can be used
|
||||||
as a variable by the file that imports it.
|
as a variable by the file that imports it.
|
||||||
|
|
||||||
|
The name of the file or subdirectory is used as the name of the variable within the importing program.
|
||||||
|
If you want to use a different name, you can do so by using the `as` keyword:
|
||||||
|
|
||||||
|
```kcl,norun
|
||||||
|
import "cube.kcl" // Introduces a new variable called `cube`.
|
||||||
|
import "cube.kcl" as block // Introduces a new variable called `block`.
|
||||||
|
import "cube/main.kcl" // Introduces a new variable called `cube`.
|
||||||
|
import "cube/main.kcl" as block // Introduces a new variable called `block`.
|
||||||
|
```
|
||||||
|
|
||||||
|
If the filename includes hyphens (`-`) or starts with an underscore (`_`), then you must specify a
|
||||||
|
variable name.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Multiple instances of the same import
|
## Multiple instances of the same import
|
||||||
|
@ -11,7 +11,8 @@ layout: manual
|
|||||||
circle(
|
circle(
|
||||||
@sketch_or_surface: Sketch | Plane | Face,
|
@sketch_or_surface: Sketch | Plane | Face,
|
||||||
center: Point2d,
|
center: Point2d,
|
||||||
radius: number(Length),
|
radius?: number(Length),
|
||||||
|
diameter?: number(Length),
|
||||||
tag?: tag,
|
tag?: tag,
|
||||||
): Sketch
|
): Sketch
|
||||||
```
|
```
|
||||||
@ -25,7 +26,8 @@ the provided (x, y) origin point.
|
|||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `sketch_or_surface` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) or [`Plane`](/docs/kcl-std/types/std-types-Plane) or [`Face`](/docs/kcl-std/types/std-types-Face) | Sketch to extend, or plane or surface to sketch on. | Yes |
|
| `sketch_or_surface` | [`Sketch`](/docs/kcl-std/types/std-types-Sketch) or [`Plane`](/docs/kcl-std/types/std-types-Plane) or [`Face`](/docs/kcl-std/types/std-types-Face) | Sketch to extend, or plane or surface to sketch on. | Yes |
|
||||||
| `center` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | The center of the circle. | Yes |
|
| `center` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | The center of the circle. | Yes |
|
||||||
| `radius` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The radius of the circle. | Yes |
|
| `radius` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The radius of the circle. Incompatible with `diameter`. | No |
|
||||||
|
| `diameter` | [`number(Length)`](/docs/kcl-std/types/std-types-number) | The diameter of the circle. Incompatible with `radius`. | No |
|
||||||
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Create a new tag which refers to this circle. | No |
|
| [`tag`](/docs/kcl-std/types/std-types-tag) | [`tag`](/docs/kcl-std/types/std-types-tag) | Create a new tag which refers to this circle. | No |
|
||||||
|
|
||||||
### Returns
|
### Returns
|
||||||
@ -51,7 +53,7 @@ exampleSketch = startSketchOn(XZ)
|
|||||||
|> line(end = [0, 30])
|
|> line(end = [0, 30])
|
||||||
|> line(end = [-30, 0])
|
|> line(end = [-30, 0])
|
||||||
|> close()
|
|> close()
|
||||||
|> subtract2d(tool = circle(center = [0, 15], radius = 5))
|
|> subtract2d(tool = circle(center = [0, 15], diameter = 10))
|
||||||
|
|
||||||
example = extrude(exampleSketch, length = 5)
|
example = extrude(exampleSketch, length = 5)
|
||||||
```
|
```
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
---
|
---
|
||||||
title: "patternTransform2d"
|
title: "patternTransform2d"
|
||||||
subtitle: "Function in std::sketch"
|
subtitle: "Function in std::sketch"
|
||||||
excerpt: "Just like patternTransform, but works on 2D sketches not 3D solids."
|
excerpt: "Just like `patternTransform`, but works on 2D sketches not 3D solids."
|
||||||
layout: manual
|
layout: manual
|
||||||
---
|
---
|
||||||
|
|
||||||
Just like patternTransform, but works on 2D sketches not 3D solids.
|
Just like `patternTransform`, but works on 2D sketches not 3D solids.
|
||||||
|
|
||||||
```kcl
|
```kcl
|
||||||
patternTransform2d(
|
patternTransform2d(
|
||||||
@sketches: [Sketch],
|
@sketches: [Sketch; 1+],
|
||||||
instances: number,
|
instances: number(_),
|
||||||
transform: FunctionSource,
|
transform: fn(number(_)): { },
|
||||||
useOriginal?: bool,
|
useOriginal?: boolean,
|
||||||
): [Sketch]
|
): [Sketch; 1+]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@ -22,14 +22,14 @@ patternTransform2d(
|
|||||||
|
|
||||||
| Name | Type | Description | Required |
|
| Name | Type | Description | Required |
|
||||||
|----------|------|-------------|----------|
|
|----------|------|-------------|----------|
|
||||||
| `sketches` | [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) | The sketch(es) to duplicate | Yes |
|
| `sketches` | [`[Sketch; 1+]`](/docs/kcl-std/types/std-types-Sketch) | The sketch(es) to duplicate. | Yes |
|
||||||
| `instances` | [`number`](/docs/kcl-std/types/std-types-number) | 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. | Yes |
|
| `instances` | [`number(_)`](/docs/kcl-std/types/std-types-number) | 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. | Yes |
|
||||||
| `transform` | `FunctionSource` | How each replica should be transformed. The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument `1`. This simplifies your math: the transform function can rely on id `0` being the original instance passed into the `patternTransform`. See the examples. | Yes |
|
| `transform` | [`fn(number(_)): { }`](/docs/kcl-std/types/std-types-fn) | How each replica should be transformed. The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument `1`. This simplifies your math: the transform function can rely on id `0` being the original instance passed into the `patternTransform`. See the examples. | Yes |
|
||||||
| `useOriginal` | [`bool`](/docs/kcl-std/types/std-types-bool) | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
| `useOriginal` | `boolean` | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. | No |
|
||||||
|
|
||||||
### Returns
|
### Returns
|
||||||
|
|
||||||
[`[Sketch]`](/docs/kcl-std/types/std-types-Sketch)
|
[`[Sketch; 1+]`](/docs/kcl-std/types/std-types-Sketch)
|
||||||
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
@ -65,7 +65,7 @@ layout: manual
|
|||||||
* [`line`](/docs/kcl-std/line)
|
* [`line`](/docs/kcl-std/line)
|
||||||
* [`loft`](/docs/kcl-std/loft)
|
* [`loft`](/docs/kcl-std/loft)
|
||||||
* [`patternCircular2d`](/docs/kcl-std/patternCircular2d)
|
* [`patternCircular2d`](/docs/kcl-std/patternCircular2d)
|
||||||
* [`patternTransform2d`](/docs/kcl-std/patternTransform2d)
|
* [`patternTransform2d`](/docs/kcl-std/functions/std-sketch-patternTransform2d)
|
||||||
* [`polygon`](/docs/kcl-std/polygon)
|
* [`polygon`](/docs/kcl-std/polygon)
|
||||||
* [`profileStart`](/docs/kcl-std/profileStart)
|
* [`profileStart`](/docs/kcl-std/profileStart)
|
||||||
* [`profileStartX`](/docs/kcl-std/profileStartX)
|
* [`profileStartX`](/docs/kcl-std/profileStartX)
|
||||||
@ -94,7 +94,7 @@ layout: manual
|
|||||||
* [`intersect`](/docs/kcl-std/intersect)
|
* [`intersect`](/docs/kcl-std/intersect)
|
||||||
* [`patternCircular3d`](/docs/kcl-std/patternCircular3d)
|
* [`patternCircular3d`](/docs/kcl-std/patternCircular3d)
|
||||||
* [`patternLinear3d`](/docs/kcl-std/patternLinear3d)
|
* [`patternLinear3d`](/docs/kcl-std/patternLinear3d)
|
||||||
* [`patternTransform`](/docs/kcl-std/patternTransform)
|
* [`patternTransform`](/docs/kcl-std/functions/std-solid-patternTransform)
|
||||||
* [`shell`](/docs/kcl-std/functions/std-solid-shell)
|
* [`shell`](/docs/kcl-std/functions/std-solid-shell)
|
||||||
* [`subtract`](/docs/kcl-std/subtract)
|
* [`subtract`](/docs/kcl-std/subtract)
|
||||||
* [`union`](/docs/kcl-std/union)
|
* [`union`](/docs/kcl-std/union)
|
||||||
|
@ -30,7 +30,7 @@ This module contains functions for creating and manipulating sketches, and makin
|
|||||||
* [`line`](/docs/kcl-std/line)
|
* [`line`](/docs/kcl-std/line)
|
||||||
* [`loft`](/docs/kcl-std/loft)
|
* [`loft`](/docs/kcl-std/loft)
|
||||||
* [`patternCircular2d`](/docs/kcl-std/patternCircular2d)
|
* [`patternCircular2d`](/docs/kcl-std/patternCircular2d)
|
||||||
* [`patternTransform2d`](/docs/kcl-std/patternTransform2d)
|
* [`patternTransform2d`](/docs/kcl-std/functions/std-sketch-patternTransform2d)
|
||||||
* [`polygon`](/docs/kcl-std/polygon)
|
* [`polygon`](/docs/kcl-std/polygon)
|
||||||
* [`profileStart`](/docs/kcl-std/profileStart)
|
* [`profileStart`](/docs/kcl-std/profileStart)
|
||||||
* [`profileStartX`](/docs/kcl-std/profileStartX)
|
* [`profileStartX`](/docs/kcl-std/profileStartX)
|
||||||
|
@ -18,7 +18,7 @@ This module contains functions for modifying solids, e.g., by adding a fillet or
|
|||||||
* [`intersect`](/docs/kcl-std/intersect)
|
* [`intersect`](/docs/kcl-std/intersect)
|
||||||
* [`patternCircular3d`](/docs/kcl-std/patternCircular3d)
|
* [`patternCircular3d`](/docs/kcl-std/patternCircular3d)
|
||||||
* [`patternLinear3d`](/docs/kcl-std/patternLinear3d)
|
* [`patternLinear3d`](/docs/kcl-std/patternLinear3d)
|
||||||
* [`patternTransform`](/docs/kcl-std/patternTransform)
|
* [`patternTransform`](/docs/kcl-std/functions/std-solid-patternTransform)
|
||||||
* [`shell`](/docs/kcl-std/functions/std-solid-shell)
|
* [`shell`](/docs/kcl-std/functions/std-solid-shell)
|
||||||
* [`subtract`](/docs/kcl-std/subtract)
|
* [`subtract`](/docs/kcl-std/subtract)
|
||||||
* [`union`](/docs/kcl-std/union)
|
* [`union`](/docs/kcl-std/union)
|
||||||
|
@ -11,7 +11,7 @@ Contains frequently used constants, functions for interacting with the KittyCAD
|
|||||||
|
|
||||||
The standard library is organised into modules (listed below), but most things are always available in KCL programs.
|
The standard library is organised into modules (listed below), but most things are always available in KCL programs.
|
||||||
|
|
||||||
You might also want the [KCL language reference](/docs/kcl-lang) or the [KCL guide]().
|
You might also want the [KCL language reference](/docs/kcl-lang) or the [KCL guide](https://zoo.dev/docs/kcl-book/intro.html).
|
||||||
|
|
||||||
## Modules
|
## Modules
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ patternLinear3d(
|
|||||||
| `solids` | [`[Solid]`](/docs/kcl-std/types/std-types-Solid) | The solid(s) to duplicate | Yes |
|
| `solids` | [`[Solid]`](/docs/kcl-std/types/std-types-Solid) | The solid(s) to duplicate | Yes |
|
||||||
| `instances` | [`number`](/docs/kcl-std/types/std-types-number) | 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. | Yes |
|
| `instances` | [`number`](/docs/kcl-std/types/std-types-number) | 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. | Yes |
|
||||||
| `distance` | [`number`](/docs/kcl-std/types/std-types-number) | Distance between each repetition. Also known as 'spacing'. | Yes |
|
| `distance` | [`number`](/docs/kcl-std/types/std-types-number) | Distance between each repetition. Also known as 'spacing'. | Yes |
|
||||||
| `axis` | [`Point3d`](/docs/kcl-std/types/std-types-Point3d) | The axis of the pattern. A 2D vector. | Yes |
|
| `axis` | [`Point3d`](/docs/kcl-std/types/std-types-Point3d) | The axis of the pattern. A 3D vector. | Yes |
|
||||||
| `useOriginal` | [`bool`](/docs/kcl-std/types/std-types-bool) | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
| `useOriginal` | [`bool`](/docs/kcl-std/types/std-types-bool) | If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false. | No |
|
||||||
|
|
||||||
### Returns
|
### Returns
|
||||||
|
21057
docs/kcl-std/std.json
@ -78,11 +78,10 @@ extrude001 = extrude(sketch001, length = 5)`
|
|||||||
|
|
||||||
// Delete a character to break the KCL
|
// Delete a character to break the KCL
|
||||||
await editor.openPane()
|
await editor.openPane()
|
||||||
await editor.scrollToText('bracketLeg1Sketch, length = thickness)')
|
await editor.scrollToText('extrude(%, length = width)')
|
||||||
await page
|
await page.getByText('extrude(%, length = width)').click()
|
||||||
.getByText('extrude(bracketLeg1Sketch, length = thickness)')
|
|
||||||
.click()
|
await page.keyboard.press(')')
|
||||||
await page.keyboard.press('Backspace')
|
|
||||||
|
|
||||||
// Ensure that a badge appears on the button
|
// Ensure that a badge appears on the button
|
||||||
await expect(codePaneButtonHolder).toContainText('notification')
|
await expect(codePaneButtonHolder).toContainText('notification')
|
||||||
@ -99,16 +98,11 @@ extrude001 = extrude(sketch001, length = 5)`
|
|||||||
|
|
||||||
await page.waitForTimeout(500)
|
await page.waitForTimeout(500)
|
||||||
|
|
||||||
// Ensure that a badge appears on the button
|
|
||||||
await expect(codePaneButtonHolder).toContainText('notification')
|
|
||||||
// Ensure we have no errors in the gutter.
|
|
||||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
|
||||||
|
|
||||||
// Open the code pane
|
// Open the code pane
|
||||||
await editor.openPane()
|
await editor.openPane()
|
||||||
|
|
||||||
// Go to our problematic code again (missing closing paren!)
|
// Go to our problematic code again
|
||||||
await editor.scrollToText('extrude(bracketLeg1Sketch, length = thickness')
|
await editor.scrollToText('extrude(%, length = w')
|
||||||
|
|
||||||
// Ensure that a badge appears on the button
|
// Ensure that a badge appears on the button
|
||||||
await expect(codePaneButtonHolder).toContainText('notification')
|
await expect(codePaneButtonHolder).toContainText('notification')
|
||||||
@ -235,6 +229,48 @@ extrude001 = extrude(sketch001, length = 5)`
|
|||||||
.first()
|
.first()
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('KCL errors with functions show hints for the entire backtrace', async ({
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
cmdBar,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
}) => {
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
const code = `fn check(@x) {
|
||||||
|
return assert(x, isGreaterThan = 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn middle(@x) {
|
||||||
|
return check(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
middle(1)
|
||||||
|
middle(0)
|
||||||
|
`
|
||||||
|
await test.step('Set the code with a KCL error', async () => {
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await editor.replaceCode('', code)
|
||||||
|
})
|
||||||
|
// This shows all the diagnostics in a way that doesn't require the mouse
|
||||||
|
// pointer hovering over a coordinate, which would be brittle.
|
||||||
|
await test.step('Open CodeMirror diagnostics list', async () => {
|
||||||
|
// Ensure keyboard focus is in the editor.
|
||||||
|
await page.getByText('fn check(').click()
|
||||||
|
await page.keyboard.press('ControlOrMeta+Shift+M')
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
page.getByText(`assert failed: Expected 0 to be greater than 0 but it wasn't
|
||||||
|
check()
|
||||||
|
middle()`)
|
||||||
|
).toBeVisible()
|
||||||
|
// There should be one hint inside middle() and one at the top level.
|
||||||
|
await expect(page.getByText('Part of the error backtrace')).toHaveCount(2)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
@ -45,15 +45,16 @@ test.describe('Command bar tests', () => {
|
|||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
commandName: 'Extrude',
|
commandName: 'Extrude',
|
||||||
currentArgKey: 'length',
|
currentArgKey: 'sketches',
|
||||||
currentArgValue: '5',
|
currentArgValue: '',
|
||||||
headerArguments: {
|
headerArguments: {
|
||||||
Profiles: '1 profile',
|
Profiles: '',
|
||||||
Length: '',
|
Length: '',
|
||||||
},
|
},
|
||||||
highlightedHeaderArg: 'length',
|
highlightedHeaderArg: 'Profiles',
|
||||||
})
|
})
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'review',
|
stage: 'review',
|
||||||
commandName: 'Extrude',
|
commandName: 'Extrude',
|
||||||
@ -684,4 +685,33 @@ c = 3 + a`
|
|||||||
highlightedHeaderArg: 'value',
|
highlightedHeaderArg: 'value',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Text-to-CAD command can be closed with escape while in prompt', async ({
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
await homePage.expectState({
|
||||||
|
projectCards: [],
|
||||||
|
sortBy: 'last-modified-desc',
|
||||||
|
})
|
||||||
|
await homePage.textToCadBtn.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
commandName: 'Text-to-CAD Create',
|
||||||
|
currentArgKey: 'prompt',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Method: 'New project',
|
||||||
|
NewProjectName: 'untitled',
|
||||||
|
Prompt: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'prompt',
|
||||||
|
})
|
||||||
|
await page.keyboard.press('Escape')
|
||||||
|
await cmdBar.toBeClosed()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'commandBarClosed',
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1001,7 +1001,7 @@ a1 = startSketchOn(offsetPlane(XY, offset = 10))
|
|||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`@settings(defaultLengthUnit = in)
|
`@settings(defaultLengthUnit = in)
|
||||||
sketch001 = startSketchOn(XZ)
|
sketch001 = startSketchOn(XZ)
|
||||||
|> startProfile(%, at = [3.14, 12])
|
|> startProfile(%, at = [0, 12])
|
||||||
|> xLine(%, length = 5) // lin`.replaceAll('\n', '')
|
|> xLine(%, length = 5) // lin`.replaceAll('\n', '')
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1076,7 +1076,7 @@ sketch001 = startSketchOn(XZ)
|
|||||||
await expect(page.locator('.cm-content')).toHaveText(
|
await expect(page.locator('.cm-content')).toHaveText(
|
||||||
`@settings(defaultLengthUnit = in)
|
`@settings(defaultLengthUnit = in)
|
||||||
sketch001 = startSketchOn(XZ)
|
sketch001 = startSketchOn(XZ)
|
||||||
|> startProfile(%, at = [3.14, 12])
|
|> startProfile(%, at = [0, 12])
|
||||||
|> xLine(%, length = 5) // lin`.replaceAll('\n', '')
|
|> xLine(%, length = 5) // lin`.replaceAll('\n', '')
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1134,6 +1134,7 @@ sketch001 = startSketchOn(XZ)
|
|||||||
// Wait for the selection to register (TODO: we need a definitive way to wait for this)
|
// Wait for the selection to register (TODO: we need a definitive way to wait for this)
|
||||||
await page.waitForTimeout(200)
|
await page.waitForTimeout(200)
|
||||||
await toolbar.extrudeButton.click()
|
await toolbar.extrudeButton.click()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
currentArgKey: 'length',
|
currentArgKey: 'length',
|
||||||
@ -1355,9 +1356,7 @@ sketch001 = startSketchOn(XZ)
|
|||||||
const u = await getUtils(page)
|
const u = await getUtils(page)
|
||||||
const projectLink = page.getByRole('link', { name: 'cube' })
|
const projectLink = page.getByRole('link', { name: 'cube' })
|
||||||
const gizmo = page.locator('[aria-label*=gizmo]')
|
const gizmo = page.locator('[aria-label*=gizmo]')
|
||||||
const resetCameraButton = page.getByRole('button', {
|
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
|
||||||
name: 'Reset view',
|
|
||||||
})
|
|
||||||
const locationToHaveColor = async (
|
const locationToHaveColor = async (
|
||||||
position: { x: number; y: number },
|
position: { x: number; y: number },
|
||||||
color: [number, number, number]
|
color: [number, number, number]
|
||||||
@ -1591,4 +1590,38 @@ sketch001 = startSketchOn(XZ)
|
|||||||
await expect(page.getByTestId('center-rectangle')).toBeVisible()
|
await expect(page.getByTestId('center-rectangle')).toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('syntax errors still show when reopening KCL pane', async ({
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
|
||||||
|
// Wait for connection, this is especially important for this test, because safeParse is invoked when
|
||||||
|
// connection is established which would interfere with the test if it happened during later steps.
|
||||||
|
await scene.connectionEstablished()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
// Code with no error
|
||||||
|
await u.codeLocator.fill(`x = 7`)
|
||||||
|
await page.waitForTimeout(200) // allow some time for the error to show potentially
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0)
|
||||||
|
|
||||||
|
// Code with error
|
||||||
|
await u.codeLocator.fill(`x 7`)
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(1)
|
||||||
|
|
||||||
|
// Close and reopen KCL code panel
|
||||||
|
await u.closeKclCodePanel()
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(0) // error disappears on close
|
||||||
|
await u.openKclCodePanel()
|
||||||
|
|
||||||
|
// Verify error is still visible
|
||||||
|
await expect(page.locator('.cm-lint-marker-error')).toHaveCount(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -238,6 +238,26 @@ test.describe('when using the file tree to', () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
`create new folders and that doesn't trigger a navigation`,
|
||||||
|
{ tag: ['@electron', '@macos', '@windows'] },
|
||||||
|
async ({ page, homePage, scene, toolbar, cmdBar }) => {
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
await toolbar.openPane('files')
|
||||||
|
const { createNewFolder } = await getUtils(page, test)
|
||||||
|
|
||||||
|
await createNewFolder('folder')
|
||||||
|
|
||||||
|
await createNewFolder('folder.kcl')
|
||||||
|
|
||||||
|
await test.step(`Postcondition: folders are created and we didn't navigate`, async () => {
|
||||||
|
await toolbar.expectFileTreeState(['folder', 'folder.kcl', 'main.kcl'])
|
||||||
|
await expect(toolbar.fileName).toHaveText('main.kcl')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'deleting all files recreates a default main.kcl with no code',
|
'deleting all files recreates a default main.kcl with no code',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
|
@ -105,14 +105,19 @@ export class CmdBarFixture {
|
|||||||
expectState = async (expected: CmdBarSerialised) => {
|
expectState = async (expected: CmdBarSerialised) => {
|
||||||
return expect.poll(() => this._serialiseCmdBar()).toEqual(expected)
|
return expect.poll(() => this._serialiseCmdBar()).toEqual(expected)
|
||||||
}
|
}
|
||||||
/** The method will use buttons OR press enter randomly to progress the cmdbar,
|
/**
|
||||||
* this could have unexpected results depending on what's focused
|
* This method is used to progress the command bar to the next step, defaulting to clicking the next button.
|
||||||
*
|
* Optionally, with the `shouldUseKeyboard` parameter, it will hit `Enter` to progress.
|
||||||
* TODO: This method assumes the user has a valid input to the current stage,
|
* * TODO: This method assumes the user has a valid input to the current stage,
|
||||||
* and assumes we are past the `pickCommand` step.
|
* and assumes we are past the `pickCommand` step.
|
||||||
*/
|
*/
|
||||||
progressCmdBar = async (shouldFuzzProgressMethod = true) => {
|
progressCmdBar = async (shouldUseKeyboard = false) => {
|
||||||
await this.page.waitForTimeout(2000)
|
await this.page.waitForTimeout(2000)
|
||||||
|
if (shouldUseKeyboard) {
|
||||||
|
await this.page.keyboard.press('Enter')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const arrowButton = this.page.getByRole('button', {
|
const arrowButton = this.page.getByRole('button', {
|
||||||
name: 'arrow right Continue',
|
name: 'arrow right Continue',
|
||||||
})
|
})
|
||||||
@ -308,6 +313,11 @@ export class CmdBarFixture {
|
|||||||
await expect(this.cmdBarElement).toBeVisible({ timeout: 10_000 })
|
await expect(this.cmdBarElement).toBeVisible({ timeout: 10_000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async toBeClosed() {
|
||||||
|
// Check that the command bar is closed
|
||||||
|
await expect(this.cmdBarElement).not.toBeVisible({ timeout: 10_000 })
|
||||||
|
}
|
||||||
|
|
||||||
async expectArgValue(value: string) {
|
async expectArgValue(value: string) {
|
||||||
// Check the placeholder project name exists
|
// Check the placeholder project name exists
|
||||||
const actualArgument = await this.cmdBarElement
|
const actualArgument = await this.cmdBarElement
|
||||||
|
@ -26,6 +26,7 @@ export class HomePageFixture {
|
|||||||
sortByNameBtn!: Locator
|
sortByNameBtn!: Locator
|
||||||
appHeader!: Locator
|
appHeader!: Locator
|
||||||
tutorialBtn!: Locator
|
tutorialBtn!: Locator
|
||||||
|
textToCadBtn!: Locator
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
@ -47,6 +48,7 @@ export class HomePageFixture {
|
|||||||
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
|
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
|
||||||
this.appHeader = this.page.getByTestId('app-header')
|
this.appHeader = this.page.getByTestId('app-header')
|
||||||
this.tutorialBtn = this.page.getByTestId('home-tutorial-button')
|
this.tutorialBtn = this.page.getByTestId('home-tutorial-button')
|
||||||
|
this.textToCadBtn = this.page.getByTestId('home-text-to-cad')
|
||||||
}
|
}
|
||||||
|
|
||||||
private _serialiseSortBy = async (): Promise<
|
private _serialiseSortBy = async (): Promise<
|
||||||
|
@ -61,6 +61,7 @@ class MyAPIReporter implements Reporter {
|
|||||||
const payload = {
|
const payload = {
|
||||||
// Required information
|
// Required information
|
||||||
project: 'https://github.com/KittyCAD/modeling-app',
|
project: 'https://github.com/KittyCAD/modeling-app',
|
||||||
|
suite: process.env.CI_SUITE || 'e2e',
|
||||||
branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || '',
|
branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || '',
|
||||||
commit: process.env.CI_COMMIT_SHA || process.env.GITHUB_SHA || '',
|
commit: process.env.CI_COMMIT_SHA || process.env.GITHUB_SHA || '',
|
||||||
test: test.titlePath().slice(2).join(' › '),
|
test: test.titlePath().slice(2).join(' › '),
|
||||||
|
@ -70,22 +70,28 @@ test.describe('Point-and-click assemblies tests', () => {
|
|||||||
await test.step('Setup parts and expect empty assembly scene', async () => {
|
await test.step('Setup parts and expect empty assembly scene', async () => {
|
||||||
const projectName = 'assembly'
|
const projectName = 'assembly'
|
||||||
await context.folderSetupFn(async (dir) => {
|
await context.folderSetupFn(async (dir) => {
|
||||||
const bracketDir = path.join(dir, projectName)
|
const projDir = path.join(dir, projectName)
|
||||||
await fsp.mkdir(bracketDir, { recursive: true })
|
const nestedProjDir = path.join(dir, projectName, 'nested', 'twice')
|
||||||
|
await fsp.mkdir(projDir, { recursive: true })
|
||||||
|
await fsp.mkdir(nestedProjDir, { recursive: true })
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
fsp.copyFile(
|
fsp.copyFile(
|
||||||
executorInputPath('cylinder.kcl'),
|
executorInputPath('cylinder.kcl'),
|
||||||
path.join(bracketDir, 'cylinder.kcl')
|
path.join(projDir, 'cylinder.kcl')
|
||||||
|
),
|
||||||
|
fsp.copyFile(
|
||||||
|
executorInputPath('cylinder.kcl'),
|
||||||
|
path.join(nestedProjDir, 'main.kcl')
|
||||||
),
|
),
|
||||||
fsp.copyFile(
|
fsp.copyFile(
|
||||||
executorInputPath('e2e-can-sketch-on-chamfer.kcl'),
|
executorInputPath('e2e-can-sketch-on-chamfer.kcl'),
|
||||||
path.join(bracketDir, 'bracket.kcl')
|
path.join(projDir, 'bracket.kcl')
|
||||||
),
|
),
|
||||||
fsp.copyFile(
|
fsp.copyFile(
|
||||||
testsInputPath('cube.step'),
|
testsInputPath('cube.step'),
|
||||||
path.join(bracketDir, 'cube.step')
|
path.join(projDir, 'cube.step')
|
||||||
),
|
),
|
||||||
fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''),
|
fsp.writeFile(path.join(projDir, 'main.kcl'), ''),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
@ -167,6 +173,25 @@ test.describe('Point-and-click assemblies tests', () => {
|
|||||||
await expect(
|
await expect(
|
||||||
page.getByText('This file is already imported')
|
page.getByText('This file is already imported')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
|
await cmdBar.closeCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Insert a nested kcl part', async () => {
|
||||||
|
await insertPartIntoAssembly(
|
||||||
|
'nested/twice/main.kcl',
|
||||||
|
'main',
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
page
|
||||||
|
)
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await page.waitForTimeout(10000)
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
import "nested/twice/main.kcl" as main
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -74,6 +74,15 @@ test.describe('Point-and-click tests', () => {
|
|||||||
|
|
||||||
await test.step('do extrude flow and check extrude code is added to editor', async () => {
|
await test.step('do extrude flow and check extrude code is added to editor', async () => {
|
||||||
await toolbar.extrudeButton.click()
|
await toolbar.extrudeButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'sketches',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: { Profiles: '', Length: '' },
|
||||||
|
highlightedHeaderArg: 'Profiles',
|
||||||
|
commandName: 'Extrude',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
currentArgKey: 'length',
|
currentArgKey: 'length',
|
||||||
@ -1645,6 +1654,15 @@ sketch002 = startSketchOn(plane001)
|
|||||||
|
|
||||||
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
|
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
|
||||||
await toolbar.loftButton.click()
|
await toolbar.loftButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'sketches',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: { Profiles: '' },
|
||||||
|
highlightedHeaderArg: 'Profiles',
|
||||||
|
commandName: 'Loft',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'review',
|
stage: 'review',
|
||||||
headerArguments: { Profiles: '2 profiles' },
|
headerArguments: { Profiles: '2 profiles' },
|
||||||
@ -1855,7 +1873,11 @@ sketch002 = startSketchOn(XZ)
|
|||||||
},
|
},
|
||||||
stage: 'review',
|
stage: 'review',
|
||||||
})
|
})
|
||||||
await cmdBar.progressCmdBar()
|
// Confirm we can submit from the review step with just `Enter`
|
||||||
|
await cmdBar.progressCmdBar(true)
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'commandBarClosed',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
@ -1995,7 +2017,7 @@ profile001 = ${circleCode}`
|
|||||||
},
|
},
|
||||||
stage: 'review',
|
stage: 'review',
|
||||||
})
|
})
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar(true)
|
||||||
await editor.expectEditor.toContain(sweepDeclaration)
|
await editor.expectEditor.toContain(sweepDeclaration)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -2088,6 +2110,18 @@ extrude001 = extrude(sketch001, length = -12)
|
|||||||
await test.step(`Apply fillet to the preselected edge`, async () => {
|
await test.step(`Apply fillet to the preselected edge`, async () => {
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await toolbar.filletButton.click()
|
await toolbar.filletButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Fillet',
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Radius: '',
|
||||||
|
},
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
commandName: 'Fillet',
|
commandName: 'Fillet',
|
||||||
highlightedHeaderArg: 'radius',
|
highlightedHeaderArg: 'radius',
|
||||||
@ -2617,6 +2651,18 @@ extrude001 = extrude(profile001, length = 5)
|
|||||||
await test.step(`Apply fillet`, async () => {
|
await test.step(`Apply fillet`, async () => {
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await toolbar.filletButton.click()
|
await toolbar.filletButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Fillet',
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Radius: '',
|
||||||
|
},
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
commandName: 'Fillet',
|
commandName: 'Fillet',
|
||||||
highlightedHeaderArg: 'radius',
|
highlightedHeaderArg: 'radius',
|
||||||
@ -2722,6 +2768,19 @@ extrude001 = extrude(sketch001, length = -12)
|
|||||||
await test.step(`Apply chamfer to the preselected edge`, async () => {
|
await test.step(`Apply chamfer to the preselected edge`, async () => {
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
await toolbar.chamferButton.click()
|
await toolbar.chamferButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Chamfer',
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Length: '',
|
||||||
|
},
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
commandName: 'Chamfer',
|
commandName: 'Chamfer',
|
||||||
highlightedHeaderArg: 'length',
|
highlightedHeaderArg: 'length',
|
||||||
@ -3205,6 +3264,8 @@ extrude001 = extrude(sketch001, length = 30)
|
|||||||
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
|
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
|
||||||
await toolbar.shellButton.click()
|
await toolbar.shellButton.click()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'review',
|
stage: 'review',
|
||||||
headerArguments: {
|
headerArguments: {
|
||||||
@ -3638,13 +3699,12 @@ tag=$rectangleSegmentC002,
|
|||||||
// revolve
|
// revolve
|
||||||
await editor.scrollToText(codeToSelection)
|
await editor.scrollToText(codeToSelection)
|
||||||
await page.getByText(codeToSelection).click()
|
await page.getByText(codeToSelection).click()
|
||||||
// Wait for the selection to register (TODO: we need a definitive way to wait for this)
|
|
||||||
await page.waitForTimeout(200)
|
|
||||||
await toolbar.revolveButton.click()
|
await toolbar.revolveButton.click()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
|
||||||
const newCodeToFind = `revolve001 = revolve(sketch002, angle = 360, axis = X)`
|
const newCodeToFind = `revolve001 = revolve(sketch002, angle = 360, axis = X)`
|
||||||
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
|
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
|
||||||
@ -4573,6 +4633,18 @@ path001 = startProfile(sketch001, at = [0, 0])
|
|||||||
|
|
||||||
await test.step('Go through command bar flow', async () => {
|
await test.step('Go through command bar flow', async () => {
|
||||||
await toolbar.extrudeButton.click()
|
await toolbar.extrudeButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'sketches',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Profiles: '',
|
||||||
|
Length: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'Profiles',
|
||||||
|
commandName: 'Extrude',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
currentArgKey: 'length',
|
currentArgKey: 'length',
|
||||||
@ -4655,6 +4727,19 @@ path001 = startProfile(sketch001, at = [0, 0])
|
|||||||
|
|
||||||
await test.step('Go through command bar flow', async () => {
|
await test.step('Go through command bar flow', async () => {
|
||||||
await toolbar.sweepButton.click()
|
await toolbar.sweepButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'sketches',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Profiles: '',
|
||||||
|
Path: '',
|
||||||
|
Sectional: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'Profiles',
|
||||||
|
commandName: 'Sweep',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
currentArgKey: 'path',
|
currentArgKey: 'path',
|
||||||
@ -4739,6 +4824,19 @@ path001 = startProfile(sketch001, at = [0, 0])
|
|||||||
await test.step('Go through command bar flow', async () => {
|
await test.step('Go through command bar flow', async () => {
|
||||||
await toolbar.closePane('code')
|
await toolbar.closePane('code')
|
||||||
await toolbar.revolveButton.click()
|
await toolbar.revolveButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'sketches',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Profiles: '',
|
||||||
|
AxisOrEdge: '',
|
||||||
|
Angle: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'Profiles',
|
||||||
|
commandName: 'Revolve',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
currentArgKey: 'axisOrEdge',
|
currentArgKey: 'axisOrEdge',
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
getPlaywrightDownloadDir,
|
getPlaywrightDownloadDir,
|
||||||
getUtils,
|
getUtils,
|
||||||
isOutOfViewInScrollContainer,
|
isOutOfViewInScrollContainer,
|
||||||
|
runningOnWindows,
|
||||||
} from '@e2e/playwright/test-utils'
|
} from '@e2e/playwright/test-utils'
|
||||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||||
|
|
||||||
@ -1979,7 +1980,6 @@ test(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Flaky
|
|
||||||
test(
|
test(
|
||||||
'Original project name persist after onboarding',
|
'Original project name persist after onboarding',
|
||||||
{ tag: '@electron' },
|
{ tag: '@electron' },
|
||||||
@ -2064,3 +2064,55 @@ test(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
'import from nested directory',
|
||||||
|
{ tag: ['@electron', '@windows', '@macos'] },
|
||||||
|
async ({ scene, cmdBar, context, page }) => {
|
||||||
|
await context.folderSetupFn(async (dir) => {
|
||||||
|
const bracketDir = path.join(dir, 'bracket')
|
||||||
|
await fsp.mkdir(bracketDir, { recursive: true })
|
||||||
|
const nestedDir = path.join(bracketDir, 'nested')
|
||||||
|
await fsp.mkdir(nestedDir, { recursive: true })
|
||||||
|
|
||||||
|
await fsp.copyFile(
|
||||||
|
executorInputPath('cylinder-inches.kcl'),
|
||||||
|
path.join(nestedDir, 'main.kcl')
|
||||||
|
)
|
||||||
|
await fsp.writeFile(
|
||||||
|
path.join(bracketDir, 'main.kcl'),
|
||||||
|
runningOnWindows()
|
||||||
|
? `import 'nested\\main.kcl' as thing\n\nthing`
|
||||||
|
: `import 'nested/main.kcl' as thing\n\nthing`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||||
|
const u = await getUtils(page)
|
||||||
|
|
||||||
|
const pointOnModel = { x: 630, y: 280 }
|
||||||
|
|
||||||
|
await test.step('Opening the bracket project should load the stream', async () => {
|
||||||
|
// expect to see the text bracket
|
||||||
|
await expect(page.getByText('bracket')).toBeVisible()
|
||||||
|
|
||||||
|
await page.getByText('bracket').click()
|
||||||
|
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Start Sketch' })
|
||||||
|
).toBeEnabled({
|
||||||
|
timeout: 20_000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// gray at this pixel means the stream has loaded in the most
|
||||||
|
// user way we can verify it (pixel color)
|
||||||
|
await expect
|
||||||
|
.poll(() => u.getGreatestPixDiff(pointOnModel, [125, 125, 125]), {
|
||||||
|
timeout: 10_000,
|
||||||
|
})
|
||||||
|
.toBeLessThan(15)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -1016,6 +1016,7 @@ profile001 = startProfile(sketch001, at = [${roundOff(scale * 69.6)}, ${roundOff
|
|||||||
|
|
||||||
// sketch selection should already have been made.
|
// sketch selection should already have been made.
|
||||||
// otherwise the cmdbar would be waiting for a selection.
|
// otherwise the cmdbar would be waiting for a selection.
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.expectState({
|
await cmdBar.expectState({
|
||||||
stage: 'arguments',
|
stage: 'arguments',
|
||||||
currentArgKey: 'length',
|
currentArgKey: 'length',
|
||||||
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
@ -557,6 +557,14 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
createNewFolder: async (name: string) => {
|
||||||
|
return test?.step(`Create a folder named ${name}`, async () => {
|
||||||
|
await page.getByTestId('create-folder-button').click()
|
||||||
|
await page.getByTestId('tree-input-field').fill(name)
|
||||||
|
await page.keyboard.press('Enter')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
cloneFile: async (name: string) => {
|
cloneFile: async (name: string) => {
|
||||||
return test?.step(`Cloning file '${name}'`, async () => {
|
return test?.step(`Cloning file '${name}'`, async () => {
|
||||||
await page
|
await page
|
||||||
|
@ -103,6 +103,8 @@ test.describe('Testing loading external models', () => {
|
|||||||
file: 'ball-bearing' + FILE_EXT,
|
file: 'ball-bearing' + FILE_EXT,
|
||||||
title: 'Ball Bearing',
|
title: 'Ball Bearing',
|
||||||
file1: 'ball-bearing-1' + FILE_EXT,
|
file1: 'ball-bearing-1' + FILE_EXT,
|
||||||
|
folderName: 'ball-bearing',
|
||||||
|
folderName1: 'ball-bearing-1',
|
||||||
}
|
}
|
||||||
const projectCard = page.getByRole('link', { name: 'bracket' })
|
const projectCard = page.getByRole('link', { name: 'bracket' })
|
||||||
const overwriteWarning = page.getByText(
|
const overwriteWarning = page.getByText(
|
||||||
@ -154,8 +156,10 @@ test.describe('Testing loading external models', () => {
|
|||||||
|
|
||||||
await test.step(`Ensure we made and opened a new file`, async () => {
|
await test.step(`Ensure we made and opened a new file`, async () => {
|
||||||
await editor.expectEditor.toContain('// ' + sampleOne.title)
|
await editor.expectEditor.toContain('// ' + sampleOne.title)
|
||||||
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
|
await expect(
|
||||||
await expect(projectMenuButton).toContainText(sampleOne.file)
|
page.getByTestId('file-tree-item').getByText(sampleOne.folderName)
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(projectMenuButton).toContainText('main.kcl')
|
||||||
})
|
})
|
||||||
|
|
||||||
await test.step(`Load a KCL sample with the command palette`, async () => {
|
await test.step(`Load a KCL sample with the command palette`, async () => {
|
||||||
@ -169,8 +173,10 @@ test.describe('Testing loading external models', () => {
|
|||||||
|
|
||||||
await test.step(`Ensure we made and opened a new file with a unique name`, async () => {
|
await test.step(`Ensure we made and opened a new file with a unique name`, async () => {
|
||||||
await editor.expectEditor.toContain('// ' + sampleOne.title)
|
await editor.expectEditor.toContain('// ' + sampleOne.title)
|
||||||
await expect(newlyCreatedFile(sampleOne.file1)).toBeVisible()
|
await expect(
|
||||||
await expect(projectMenuButton).toContainText(sampleOne.file1)
|
page.getByTestId('file-tree-item').getByText(sampleOne.folderName1)
|
||||||
|
).toBeVisible()
|
||||||
|
await expect(projectMenuButton).toContainText('main.kcl')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import fs from 'fs'
|
|
||||||
import { join } from 'path'
|
|
||||||
import type { Page } from '@playwright/test'
|
import type { Page } from '@playwright/test'
|
||||||
|
|
||||||
import { createProject, getUtils } from '@e2e/playwright/test-utils'
|
import { createProject, getUtils } from '@e2e/playwright/test-utils'
|
||||||
@ -403,106 +401,6 @@ test.describe('Text-to-CAD tests', () => {
|
|||||||
await expect(page.getByText(promptWithNewline)).toBeVisible()
|
await expect(page.getByText(promptWithNewline)).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
// This will be fine once greg makes prompt at top of file deterministic
|
|
||||||
test('can do many at once and get many prompts back, and interact with many', async ({
|
|
||||||
page,
|
|
||||||
homePage,
|
|
||||||
cmdBar,
|
|
||||||
}) => {
|
|
||||||
// Let this test run longer since we've seen it timeout.
|
|
||||||
test.setTimeout(180_000)
|
|
||||||
|
|
||||||
const u = await getUtils(page)
|
|
||||||
|
|
||||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
|
||||||
|
|
||||||
await homePage.goToModelingScene()
|
|
||||||
await u.waitForPageLoad()
|
|
||||||
|
|
||||||
await sendPromptFromCommandBarAndSetExistingProject(
|
|
||||||
page,
|
|
||||||
'a 2x4 lego',
|
|
||||||
cmdBar
|
|
||||||
)
|
|
||||||
|
|
||||||
await sendPromptFromCommandBarAndSetExistingProject(
|
|
||||||
page,
|
|
||||||
'a 2x8 lego',
|
|
||||||
cmdBar
|
|
||||||
)
|
|
||||||
|
|
||||||
await sendPromptFromCommandBarAndSetExistingProject(
|
|
||||||
page,
|
|
||||||
'a 2x10 lego',
|
|
||||||
cmdBar
|
|
||||||
)
|
|
||||||
|
|
||||||
// Find the toast.
|
|
||||||
// Look out for the toast message
|
|
||||||
const submittingToastMessage = page.getByText(
|
|
||||||
`Submitting to Text-to-CAD API...`
|
|
||||||
)
|
|
||||||
await expect(submittingToastMessage.first()).toBeVisible()
|
|
||||||
|
|
||||||
const generatingToastMessage = page.getByText(
|
|
||||||
`Generating parametric model...`
|
|
||||||
)
|
|
||||||
await expect(generatingToastMessage.first()).toBeVisible({
|
|
||||||
timeout: 10_000,
|
|
||||||
})
|
|
||||||
|
|
||||||
const successToastMessage = page.getByText(`Text-to-CAD successful`)
|
|
||||||
// We should have three success toasts.
|
|
||||||
await expect(successToastMessage).toHaveCount(3, { timeout: 25_000 })
|
|
||||||
|
|
||||||
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
|
|
||||||
await expect(page.getByText(`a 2x8 lego`)).toBeVisible()
|
|
||||||
await expect(page.getByText(`a 2x10 lego`)).toBeVisible()
|
|
||||||
|
|
||||||
// Ensure if you reject one, the others stay.
|
|
||||||
const rejectButton = page.getByRole('button', { name: 'Reject' })
|
|
||||||
await expect(rejectButton.first()).toBeVisible()
|
|
||||||
// Click the reject button on the first toast.
|
|
||||||
await rejectButton.first().click()
|
|
||||||
|
|
||||||
// The first toast should disappear, but not the others.
|
|
||||||
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
|
|
||||||
await expect(page.getByText(`a 2x8 lego`)).toBeVisible()
|
|
||||||
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
|
|
||||||
|
|
||||||
// Ensure you can copy the code for one of the models remaining.
|
|
||||||
const copyToClipboardButton = page.getByRole('button', {
|
|
||||||
name: 'Accept',
|
|
||||||
})
|
|
||||||
await expect(copyToClipboardButton.first()).toBeVisible()
|
|
||||||
// Click the button.
|
|
||||||
await copyToClipboardButton.first().click()
|
|
||||||
|
|
||||||
// Do NOT do AI tests like this: "Expect the code to be pasted."
|
|
||||||
// Reason: AI tests are NONDETERMINISTIC. Thus we need to be as most
|
|
||||||
// general as we can for the assertion.
|
|
||||||
// We can use Kolmogorov complexity as a measurement of the
|
|
||||||
// "probably most minimal version of this program" to have a lower
|
|
||||||
// bound to work with. It is completely by feel because there are
|
|
||||||
// no proofs that any program is its smallest self.
|
|
||||||
const code2x8 = await page.locator('.cm-content').innerText()
|
|
||||||
await expect(code2x8.length).toBeGreaterThan(249)
|
|
||||||
|
|
||||||
// Ensure the final toast remains.
|
|
||||||
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible()
|
|
||||||
await expect(page.getByText(`Prompt: "a 2x8 lego`)).not.toBeVisible()
|
|
||||||
await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
|
|
||||||
|
|
||||||
// Ensure you can copy the code for the final model.
|
|
||||||
await expect(copyToClipboardButton).toBeVisible()
|
|
||||||
// Click the button.
|
|
||||||
await copyToClipboardButton.click()
|
|
||||||
|
|
||||||
// Expect the code to be pasted.
|
|
||||||
const code2x4 = await page.locator('.cm-content').innerText()
|
|
||||||
await expect(code2x4.length).toBeGreaterThan(249)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('can do many at once with errors, clicking dismiss error does not dismiss all', async ({
|
test('can do many at once with errors, clicking dismiss error does not dismiss all', async ({
|
||||||
page,
|
page,
|
||||||
homePage,
|
homePage,
|
||||||
@ -675,82 +573,6 @@ async function sendPromptFromCommandBarAndSetExistingProject(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
test(
|
|
||||||
'Text-to-CAD functionality',
|
|
||||||
{ tag: '@electron' },
|
|
||||||
async ({ context, page, cmdBar }, testInfo) => {
|
|
||||||
const projectName = 'project-000'
|
|
||||||
const prompt = 'lego 2x4'
|
|
||||||
const textToCadFileName = 'lego-2x4.kcl'
|
|
||||||
|
|
||||||
const { dir } = await context.folderSetupFn(async () => {})
|
|
||||||
|
|
||||||
const fileExists = () =>
|
|
||||||
fs.existsSync(join(dir, projectName, textToCadFileName))
|
|
||||||
|
|
||||||
const { openFilePanel, openKclCodePanel, waitForPageLoad } = await getUtils(
|
|
||||||
page,
|
|
||||||
test
|
|
||||||
)
|
|
||||||
|
|
||||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
||||||
|
|
||||||
// Locators
|
|
||||||
const projectMenuButton = page
|
|
||||||
.getByTestId('project-sidebar-toggle')
|
|
||||||
.filter({ hasText: projectName })
|
|
||||||
const textToCadFileButton = page.getByRole('listitem').filter({
|
|
||||||
has: page.getByRole('button', { name: textToCadFileName }),
|
|
||||||
})
|
|
||||||
const textToCadComment = page.getByText(
|
|
||||||
`// Generated by Text-to-CAD: ${prompt}`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create and navigate to the project
|
|
||||||
await createProject({ name: 'project-000', page })
|
|
||||||
|
|
||||||
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command
|
|
||||||
await waitForPageLoad()
|
|
||||||
await openFilePanel()
|
|
||||||
await openKclCodePanel()
|
|
||||||
|
|
||||||
await test.step(`Test file creation`, async () => {
|
|
||||||
await sendPromptFromCommandBarAndSetExistingProject(
|
|
||||||
page,
|
|
||||||
prompt,
|
|
||||||
cmdBar,
|
|
||||||
projectName
|
|
||||||
)
|
|
||||||
// File is considered created if it shows up in the Project Files pane
|
|
||||||
await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 })
|
|
||||||
expect(fileExists()).toBeTruthy()
|
|
||||||
})
|
|
||||||
|
|
||||||
await test.step(`Test file navigation`, async () => {
|
|
||||||
await expect(projectMenuButton).toContainText('main.kcl')
|
|
||||||
await textToCadFileButton.click()
|
|
||||||
// File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane
|
|
||||||
await expect(textToCadComment).toBeVisible({ timeout: 20_000 })
|
|
||||||
await expect(projectMenuButton).toContainText(textToCadFileName)
|
|
||||||
})
|
|
||||||
|
|
||||||
await test.step(`Test file deletion on rejection`, async () => {
|
|
||||||
const rejectButton = page.getByRole('button', { name: 'Reject' })
|
|
||||||
// A file is created and can be navigated to while this prompt is still opened
|
|
||||||
// Click the "Reject" button within the prompt and it will delete the file.
|
|
||||||
await rejectButton.click()
|
|
||||||
|
|
||||||
const submittingToastMessage = page.getByText(
|
|
||||||
`Successfully deleted file "lego-2x4.kcl"`
|
|
||||||
)
|
|
||||||
await expect(submittingToastMessage).toBeVisible()
|
|
||||||
expect(fileExists()).toBeFalsy()
|
|
||||||
// Confirm we've navigated back to the main.kcl file after deletion
|
|
||||||
await expect(projectMenuButton).toContainText('main.kcl')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Below there are twelve (12) tests for testing the navigation and file creation
|
* Below there are twelve (12) tests for testing the navigation and file creation
|
||||||
* logic around text to cad. The Text to CAD command is now globally available
|
* logic around text to cad. The Text to CAD command is now globally available
|
||||||
@ -984,12 +806,12 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
)
|
)
|
||||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||||
'2x2x2-cube.kcl'
|
'main.kcl'
|
||||||
)
|
)
|
||||||
|
|
||||||
await u.openFilePanel()
|
await u.openFilePanel()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
|
page.getByTestId('file-tree-item').getByText('2x2x2-cube')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -1184,13 +1006,13 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
)
|
)
|
||||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||||
'2x2x2-cube.kcl'
|
'main.kcl'
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check file is created
|
// Check file is created
|
||||||
await u.openFilePanel()
|
await u.openFilePanel()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
|
page.getByTestId('file-tree-item').getByText('2x2x2-cube')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -1476,13 +1298,13 @@ test.describe('Mocked Text-to-CAD API tests', { tag: ['@skipWin'] }, () => {
|
|||||||
)
|
)
|
||||||
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
await expect(page.getByTestId('app-header-file-name')).toBeVisible()
|
||||||
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
await expect(page.getByTestId('app-header-file-name')).toContainText(
|
||||||
'2x2x2-cube.kcl'
|
'main.kcl'
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check file is created
|
// Check file is created
|
||||||
await u.openFilePanel()
|
await u.openFilePanel()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
|
page.getByTestId('file-tree-item').getByText('2x2x2-cube')
|
||||||
).toBeVisible()
|
).toBeVisible()
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId('file-tree-item').getByText('main.kcl')
|
page.getByTestId('file-tree-item').getByText('main.kcl')
|
||||||
|
@ -573,6 +573,7 @@ profile001 = startProfile(sketch002, at = [-12.34, 12.34])
|
|||||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||||
await page.waitForTimeout(100)
|
await page.waitForTimeout(100)
|
||||||
|
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
||||||
await cmdBar.progressCmdBar()
|
await cmdBar.progressCmdBar()
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
<link rel="apple-touch-icon" href="/logo192.png" />
|
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="stylesheet" href="./inter/inter.css" />
|
<link rel="stylesheet" href="./inter/inter.css" />
|
||||||
<link rel="stylesheet" href="https://use.typekit.net/zzv8rvm.css" />
|
|
||||||
<script
|
<script
|
||||||
defer
|
defer
|
||||||
data-domain="app.zoo.dev"
|
data-domain="app.zoo.dev"
|
||||||
|
1
package-lock.json
generated
@ -2492,6 +2492,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
|
"extraneous": true,
|
||||||
"inBundle": true,
|
"inBundle": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -27,7 +27,7 @@ if len(modified_release_body) > max_length:
|
|||||||
# Message to send to Discord
|
# Message to send to Discord
|
||||||
data = {
|
data = {
|
||||||
"content": textwrap.dedent(f'''
|
"content": textwrap.dedent(f'''
|
||||||
**{release_version}** is now available! Check out the latest features and improvements here: <https://zoo.dev/modeling-app/download>
|
**{release_version}** is now available! Check out the latest features and improvements here: <https://zoo.dev/design-studio>
|
||||||
|
|
||||||
{modified_release_body}
|
{modified_release_body}
|
||||||
'''),
|
'''),
|
||||||
|
@ -37,6 +37,8 @@ When you submit a PR to add or modify KCL samples, images will be generated and
|
|||||||
[](bottle/main.kcl)
|
[](bottle/main.kcl)
|
||||||
#### [bracket](bracket/main.kcl) ([screenshot](screenshots/bracket.png))
|
#### [bracket](bracket/main.kcl) ([screenshot](screenshots/bracket.png))
|
||||||
[](bracket/main.kcl)
|
[](bracket/main.kcl)
|
||||||
|
#### [brake-rotor](brake-rotor/main.kcl) ([screenshot](screenshots/brake-rotor.png))
|
||||||
|
[](brake-rotor/main.kcl)
|
||||||
#### [car-wheel-assembly](car-wheel-assembly/main.kcl) ([screenshot](screenshots/car-wheel-assembly.png))
|
#### [car-wheel-assembly](car-wheel-assembly/main.kcl) ([screenshot](screenshots/car-wheel-assembly.png))
|
||||||
[](car-wheel-assembly/main.kcl)
|
[](car-wheel-assembly/main.kcl)
|
||||||
#### [cold-plate](cold-plate/main.kcl) ([screenshot](screenshots/cold-plate.png))
|
#### [cold-plate](cold-plate/main.kcl) ([screenshot](screenshots/cold-plate.png))
|
||||||
@ -141,10 +143,14 @@ When you submit a PR to add or modify KCL samples, images will be generated and
|
|||||||
[](spur-reduction-gearset/main.kcl)
|
[](spur-reduction-gearset/main.kcl)
|
||||||
#### [surgical-drill-guide](surgical-drill-guide/main.kcl) ([screenshot](screenshots/surgical-drill-guide.png))
|
#### [surgical-drill-guide](surgical-drill-guide/main.kcl) ([screenshot](screenshots/surgical-drill-guide.png))
|
||||||
[](surgical-drill-guide/main.kcl)
|
[](surgical-drill-guide/main.kcl)
|
||||||
|
#### [telemetry-antenna](telemetry-antenna/main.kcl) ([screenshot](screenshots/telemetry-antenna.png))
|
||||||
|
[](telemetry-antenna/main.kcl)
|
||||||
#### [thermal-block-insert](thermal-block-insert/main.kcl) ([screenshot](screenshots/thermal-block-insert.png))
|
#### [thermal-block-insert](thermal-block-insert/main.kcl) ([screenshot](screenshots/thermal-block-insert.png))
|
||||||
[](thermal-block-insert/main.kcl)
|
[](thermal-block-insert/main.kcl)
|
||||||
#### [tooling-nest-block](tooling-nest-block/main.kcl) ([screenshot](screenshots/tooling-nest-block.png))
|
#### [tooling-nest-block](tooling-nest-block/main.kcl) ([screenshot](screenshots/tooling-nest-block.png))
|
||||||
[](tooling-nest-block/main.kcl)
|
[](tooling-nest-block/main.kcl)
|
||||||
|
#### [truss-structure](truss-structure/main.kcl) ([screenshot](screenshots/truss-structure.png))
|
||||||
|
[](truss-structure/main.kcl)
|
||||||
#### [utility-sink](utility-sink/main.kcl) ([screenshot](screenshots/utility-sink.png))
|
#### [utility-sink](utility-sink/main.kcl) ([screenshot](screenshots/utility-sink.png))
|
||||||
[](utility-sink/main.kcl)
|
[](utility-sink/main.kcl)
|
||||||
#### [walkie-talkie](walkie-talkie/main.kcl) ([screenshot](screenshots/walkie-talkie.png))
|
#### [walkie-talkie](walkie-talkie/main.kcl) ([screenshot](screenshots/walkie-talkie.png))
|
||||||
|
180
public/kcl-samples/brake-rotor/main.kcl
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
// Brake Rotor
|
||||||
|
// A 320mm vented brake disc (rotor), with straight vanes, 30mm thick. The disc bell should accommodate 5 M12 wheel studs on a 114.3mm pitch circle diameter.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@settings(defaultLengthUnit = mm)
|
||||||
|
|
||||||
|
// Define parameters.
|
||||||
|
dDisc = 320
|
||||||
|
dPitchCircle = 114.3
|
||||||
|
dBore = 64
|
||||||
|
nStuds = 5
|
||||||
|
dStudDrilling = 12.5 // M12
|
||||||
|
hFrictionSurface = 60
|
||||||
|
tDiscHalf = 10
|
||||||
|
|
||||||
|
// Vent parameters.
|
||||||
|
tVent = 10
|
||||||
|
wVent = 6
|
||||||
|
rVentFillet = 2
|
||||||
|
nVentBosses = 36
|
||||||
|
|
||||||
|
// Drilling parameters.
|
||||||
|
dDrillDia = 6
|
||||||
|
aBase = 90
|
||||||
|
aSweep = 30
|
||||||
|
nArcs = 12
|
||||||
|
|
||||||
|
// Bell parameters.
|
||||||
|
aDraftBell = 5
|
||||||
|
tBell = 5 // Wall thickness.
|
||||||
|
hBellAboveDiscFace = 40
|
||||||
|
hBellSubflush = 4
|
||||||
|
wUndercut = 8
|
||||||
|
|
||||||
|
fn drillHole(activeSketch, t) {
|
||||||
|
// Sketch a vent hole at line parameter value t on an arc drawn across the disc surface.
|
||||||
|
rInner = dDisc / 2 - hFrictionSurface
|
||||||
|
rOuter = dDisc / 2
|
||||||
|
|
||||||
|
aStart = aBase
|
||||||
|
aEnd = aBase - aSweep
|
||||||
|
|
||||||
|
// Linear interpolation of radius.
|
||||||
|
rCurrent = rInner + t * (rOuter - rInner)
|
||||||
|
|
||||||
|
// Linear interpolation of angle.
|
||||||
|
aCurrent = aStart + t * (aEnd - aStart)
|
||||||
|
|
||||||
|
// Calculate position.
|
||||||
|
xCenter = rCurrent * cos(aCurrent)
|
||||||
|
yCenter = rCurrent * sin(aCurrent)
|
||||||
|
|
||||||
|
// Draw.
|
||||||
|
drillCircle = circle(activeSketch, center = [xCenter, yCenter], radius = dDrillDia / 2)
|
||||||
|
return drillCircle
|
||||||
|
}
|
||||||
|
|
||||||
|
fn createDiscHalf(plane, dDiscParam, hFrictionSurfaceParam, tDiscHalfParam) {
|
||||||
|
// Create a disc half with a vent hole pattern.
|
||||||
|
sketchFace = startSketchOn(plane)
|
||||||
|
profileFace = circle(sketchFace, center = [0, 0], radius = dDiscParam / 2)
|
||||||
|
|> subtract2d(tool = circle(sketchFace, center = [0, 0], radius = dDiscParam / 2 - hFrictionSurfaceParam))
|
||||||
|
|
||||||
|
// Create three circles at t = 0, 0.5, and 1
|
||||||
|
hole1 = drillHole(activeSketch = sketchFace, t = 0.2)
|
||||||
|
hole2 = drillHole(activeSketch = sketchFace, t = 0.5)
|
||||||
|
hole3 = drillHole(activeSketch = sketchFace, t = 0.8)
|
||||||
|
|
||||||
|
// Pattern and cut.
|
||||||
|
holes = patternCircular2d(
|
||||||
|
[hole1, hole2, hole3],
|
||||||
|
instances = nArcs,
|
||||||
|
center = [0, 0],
|
||||||
|
arcDegrees = 360,
|
||||||
|
rotateDuplicates = true,
|
||||||
|
)
|
||||||
|
profileDrilled = subtract2d(profileFace, tool = holes)
|
||||||
|
|
||||||
|
// Extrude.
|
||||||
|
discHalf = extrude(profileFace, length = tDiscHalfParam)
|
||||||
|
return discHalf
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Create inboard half.
|
||||||
|
discInboard = createDiscHalf(
|
||||||
|
plane = XY,
|
||||||
|
dDiscParam = dDisc,
|
||||||
|
hFrictionSurfaceParam = hFrictionSurface,
|
||||||
|
tDiscHalfParam = tDiscHalf,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create vents.
|
||||||
|
planeVent = offsetPlane(XY, offset = tDiscHalf)
|
||||||
|
sketchVent = startSketchOn(planeVent)
|
||||||
|
profileVent = startProfile(sketchVent, at = [-wVent, dDisc / 2])
|
||||||
|
|> angledLine(angle = 0, length = wVent, tag = $rectangleSegmentA001)
|
||||||
|
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = hFrictionSurface, tag = $seg02)
|
||||||
|
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg03)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|
||||||
|
|> close()
|
||||||
|
|
||||||
|
ventPad = extrude(profileVent, length = tVent)
|
||||||
|
|> fillet(
|
||||||
|
radius = rVentFillet,
|
||||||
|
tags = [
|
||||||
|
getCommonEdge(faces = [seg01, rectangleSegmentA001]),
|
||||||
|
getCommonEdge(faces = [seg02, rectangleSegmentA001]),
|
||||||
|
getCommonEdge(faces = [seg01, seg03]),
|
||||||
|
getCommonEdge(faces = [seg03, seg02])
|
||||||
|
],
|
||||||
|
)
|
||||||
|
ventSet = patternCircular3d(
|
||||||
|
ventPad,
|
||||||
|
instances = nVentBosses,
|
||||||
|
axis = [0, 0, 1],
|
||||||
|
center = [0, 0, tDiscHalf],
|
||||||
|
arcDegrees = 360,
|
||||||
|
rotateDuplicates = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create outboard half.
|
||||||
|
planeOutboard = offsetPlane(XY, offset = tDiscHalf + tVent)
|
||||||
|
discOutboard = createDiscHalf(
|
||||||
|
plane = planeOutboard,
|
||||||
|
dDiscParam = dDisc,
|
||||||
|
hFrictionSurfaceParam = hFrictionSurface,
|
||||||
|
tDiscHalfParam = tDiscHalf,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Now create bell.
|
||||||
|
rCenter = dDisc / 2 - hFrictionSurface - wUndercut
|
||||||
|
rBore = dBore / 2
|
||||||
|
lDraftExterior = hBellAboveDiscFace / tan(90 - aDraftBell)
|
||||||
|
lDraftInterior = (hBellAboveDiscFace - tBell) / tan(90 - aDraftBell)
|
||||||
|
|
||||||
|
// Inner and outer radius of outboard face of disc bell.
|
||||||
|
rOuter = rCenter - lDraftExterior - rBore
|
||||||
|
rInner = rOuter + lDraftExterior - (tBell + lDraftInterior)
|
||||||
|
|
||||||
|
sketchDiscBell = startSketchOn(-YZ)
|
||||||
|
bodyDiscBell = startProfile(
|
||||||
|
sketchDiscBell,
|
||||||
|
at = [
|
||||||
|
-dDisc / 2 + hFrictionSurface,
|
||||||
|
tDiscHalf * 2 + tVent
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|> arc(
|
||||||
|
%,
|
||||||
|
angleStart = -180,
|
||||||
|
angleEnd = 0,
|
||||||
|
radius = wUndercut / 2,
|
||||||
|
)
|
||||||
|
|> line(end = [lDraftExterior, hBellAboveDiscFace])
|
||||||
|
|> xLine(length = rOuter, tag = $seg04)
|
||||||
|
|> yLine(length = -tBell)
|
||||||
|
|> xLine(length = -rInner)
|
||||||
|
|> line(end = [-lDraftInterior, -hBellAboveDiscFace])
|
||||||
|
|> line(end = [0, -2]) // Wall thickness.
|
||||||
|
|> xLine(length = -1 * (tBell + wUndercut))
|
||||||
|
|> close(%)
|
||||||
|
|> revolve(axis = Y)
|
||||||
|
|
||||||
|
// Drill lug holes.
|
||||||
|
sketchLugs = startSketchOn(bodyDiscBell, face = seg04)
|
||||||
|
profileStud = circle(sketchLugs, center = [0, dPitchCircle / 2], radius = dStudDrilling / 2)
|
||||||
|
|> patternCircular2d(
|
||||||
|
%,
|
||||||
|
instances = nStuds,
|
||||||
|
center = [0, 0],
|
||||||
|
arcDegrees = 360,
|
||||||
|
rotateDuplicates = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
clearance = 2 // Some margin on negative extrude.
|
||||||
|
lugs = extrude(profileStud, length = -1 * (tBell + clearance))
|
@ -74,6 +74,16 @@
|
|||||||
"main.kcl"
|
"main.kcl"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"file": "main.kcl",
|
||||||
|
"pathFromProjectDirectoryToFirstFile": "brake-rotor/main.kcl",
|
||||||
|
"multipleFiles": false,
|
||||||
|
"title": "Brake Rotor",
|
||||||
|
"description": "A 320mm vented brake disc (rotor), with straight vanes, 30mm thick. The disc bell should accommodate 5 M12 wheel studs on a 114.3mm pitch circle diameter.",
|
||||||
|
"files": [
|
||||||
|
"main.kcl"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"file": "main.kcl",
|
"file": "main.kcl",
|
||||||
"pathFromProjectDirectoryToFirstFile": "car-wheel-assembly/main.kcl",
|
"pathFromProjectDirectoryToFirstFile": "car-wheel-assembly/main.kcl",
|
||||||
@ -622,6 +632,16 @@
|
|||||||
"main.kcl"
|
"main.kcl"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"file": "main.kcl",
|
||||||
|
"pathFromProjectDirectoryToFirstFile": "telemetry-antenna/main.kcl",
|
||||||
|
"multipleFiles": false,
|
||||||
|
"title": "Aircraft telemetry antenna plate",
|
||||||
|
"description": "Consists of a circular base plate 3 inches in diameter and 0.08 inches thick, with a tapered monopole antenna mounted at the top with a base diameter of 0.65 inches and height of 1.36 inches. Also consists of a mounting base and connector at the bottom of the plate. The plate also has 6 countersunk holes at a defined pitch circle diameter.",
|
||||||
|
"files": [
|
||||||
|
"main.kcl"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"file": "main.kcl",
|
"file": "main.kcl",
|
||||||
"pathFromProjectDirectoryToFirstFile": "thermal-block-insert/main.kcl",
|
"pathFromProjectDirectoryToFirstFile": "thermal-block-insert/main.kcl",
|
||||||
@ -642,6 +662,16 @@
|
|||||||
"main.kcl"
|
"main.kcl"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"file": "main.kcl",
|
||||||
|
"pathFromProjectDirectoryToFirstFile": "truss-structure/main.kcl",
|
||||||
|
"multipleFiles": false,
|
||||||
|
"title": "Truss Structure",
|
||||||
|
"description": "A truss structure is a framework composed of triangular units made from straight members connected at joints, often called nodes. Trusses are widely used in architecture, civil engineering, and construction for their ability to support large loads with minimal material.",
|
||||||
|
"files": [
|
||||||
|
"main.kcl"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"file": "main.kcl",
|
"file": "main.kcl",
|
||||||
"pathFromProjectDirectoryToFirstFile": "utility-sink/main.kcl",
|
"pathFromProjectDirectoryToFirstFile": "utility-sink/main.kcl",
|
||||||
|
@ -72,25 +72,25 @@ leftSpacerShape = boxModuleFn(width = leftSpacerWidth)
|
|||||||
|
|
||||||
// Module for power switch including front plate and red rocker button
|
// Module for power switch including front plate and red rocker button
|
||||||
switchPosition = leftSpacerPosition + leftSpacerWidth / 2 + moduleWidth / 2
|
switchPosition = leftSpacerPosition + leftSpacerWidth / 2 + moduleWidth / 2
|
||||||
swtichWidth = moduleWidth
|
switchWidth = moduleWidth
|
||||||
|
|
||||||
// Switch Body
|
// Switch Body
|
||||||
switchBody = boxModuleFn(width = moduleWidth)
|
switchBody = boxModuleFn(width = moduleWidth)
|
||||||
|
|
||||||
// Switch Plate
|
// Switch Plate
|
||||||
swtichPlateWidth = 20
|
switchPlateWidth = 20
|
||||||
switchPlateHeight = 30
|
switchPlateHeight = 30
|
||||||
switchPlateThickness = 3
|
switchPlateThickness = 3
|
||||||
switchPlateShape = startSketchOn(switchBody, face = END)
|
switchPlateShape = startSketchOn(switchBody, face = END)
|
||||||
|> startProfile(
|
|> startProfile(
|
||||||
%,
|
%,
|
||||||
at = [
|
at = [
|
||||||
-swtichPlateWidth / 2,
|
-switchPlateWidth / 2,
|
||||||
-switchPlateHeight / 2
|
-switchPlateHeight / 2
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|> yLine(length = switchPlateHeight)
|
|> yLine(length = switchPlateHeight)
|
||||||
|> xLine(length = swtichPlateWidth)
|
|> xLine(length = switchPlateWidth)
|
||||||
|> yLine(length = -switchPlateHeight)
|
|> yLine(length = -switchPlateHeight)
|
||||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
@ -104,8 +104,8 @@ switchPlateBody = extrude(switchPlateShape, length = switchPlateThickness)
|
|||||||
|
|
||||||
// Switch Button
|
// Switch Button
|
||||||
switchButtonHeight = 26
|
switchButtonHeight = 26
|
||||||
swtichButtonWidth = 15
|
switchButtonWidth = 15
|
||||||
switchButtonShape = startSketchOn(offsetPlane(-YZ, offset = -swtichButtonWidth / 2))
|
switchButtonShape = startSketchOn(offsetPlane(-YZ, offset = -switchButtonWidth / 2))
|
||||||
|> startProfile(
|
|> startProfile(
|
||||||
%,
|
%,
|
||||||
at = [
|
at = [
|
||||||
@ -121,7 +121,7 @@ switchButtonShape = startSketchOn(offsetPlane(-YZ, offset = -swtichButtonWidth /
|
|||||||
])
|
])
|
||||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
switchButtonBody = extrude(switchButtonShape, length = swtichButtonWidth)
|
switchButtonBody = extrude(switchButtonShape, length = switchButtonWidth)
|
||||||
|> translate(
|
|> translate(
|
||||||
%,
|
%,
|
||||||
x = switchPosition,
|
x = switchPosition,
|
||||||
@ -132,7 +132,7 @@ switchButtonBody = extrude(switchButtonShape, length = swtichButtonWidth)
|
|||||||
|
|
||||||
// Spacer between switch and plug modules for layout alignment
|
// Spacer between switch and plug modules for layout alignment
|
||||||
secondSpacerWidth = moduleWidth / 2
|
secondSpacerWidth = moduleWidth / 2
|
||||||
secondSpacerPosition = switchPosition + swtichWidth / 2 + secondSpacerWidth / 2
|
secondSpacerPosition = switchPosition + switchWidth / 2 + secondSpacerWidth / 2
|
||||||
secondSpacerBody = boxModuleFn(width = secondSpacerWidth)
|
secondSpacerBody = boxModuleFn(width = secondSpacerWidth)
|
||||||
|> translate(
|
|> translate(
|
||||||
%,
|
%,
|
||||||
|
@ -33,14 +33,9 @@ stemLoftProfile2 = startSketchOn(offsetPlane(XY, offset = 75))
|
|||||||
// Draw the third profile for the lofted femur
|
// Draw the third profile for the lofted femur
|
||||||
p3Z = 110
|
p3Z = 110
|
||||||
p3A = 25
|
p3A = 25
|
||||||
plane003 = {
|
|
||||||
origin = [0, 0.0, p3Z],
|
|
||||||
xAxis = [cos(p3A), 0, sin(p3A)],
|
|
||||||
yAxis = [0.0, 1.0, 0.0]
|
|
||||||
}
|
|
||||||
l3 = 32
|
l3 = 32
|
||||||
r3 = 4
|
r3 = 4
|
||||||
stemLoftProfile3 = startSketchOn(plane003)
|
stemLoftProfile3 = startSketchOn(XY)
|
||||||
|> startProfile(at = [-15.5, -l3 / 2])
|
|> startProfile(at = [-15.5, -l3 / 2])
|
||||||
|> yLine(length = l3, tag = $seg03)
|
|> yLine(length = l3, tag = $seg03)
|
||||||
|> tangentialArc(angle = -120, radius = r3)
|
|> tangentialArc(angle = -120, radius = r3)
|
||||||
@ -49,18 +44,14 @@ stemLoftProfile3 = startSketchOn(plane003)
|
|||||||
|> angledLine(angle = 30, length = -segLen(seg03))
|
|> angledLine(angle = 30, length = -segLen(seg03))
|
||||||
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
|
|> translate(z = p3Z)
|
||||||
|
|> rotate(pitch = -p3A)
|
||||||
// Draw the fourth profile for the lofted femur
|
// Draw the fourth profile for the lofted femur
|
||||||
p4Z = 130
|
p4Z = 130
|
||||||
p4A = 36.5
|
p4A = 36.5
|
||||||
plane004 = {
|
|
||||||
origin = [0, 0.0, p4Z],
|
|
||||||
xAxis = [cos(p4A), 0, sin(p4A)],
|
|
||||||
yAxis = [0.0, 1.0, 0.0]
|
|
||||||
}
|
|
||||||
l4 = 16
|
l4 = 16
|
||||||
r4 = 5
|
r4 = 5
|
||||||
stemLoftProfile4 = startSketchOn(plane004)
|
stemLoftProfile4 = startSketchOn(XY)
|
||||||
|> startProfile(at = [-23, -l4 / 2])
|
|> startProfile(at = [-23, -l4 / 2])
|
||||||
|> yLine(length = l4, tag = $seg04)
|
|> yLine(length = l4, tag = $seg04)
|
||||||
|> tangentialArc(angle = -120, radius = r4)
|
|> tangentialArc(angle = -120, radius = r4)
|
||||||
@ -69,18 +60,14 @@ stemLoftProfile4 = startSketchOn(plane004)
|
|||||||
|> angledLine(angle = 30, length = -segLen(seg04))
|
|> angledLine(angle = 30, length = -segLen(seg04))
|
||||||
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
|
|> translate(z = p4Z)
|
||||||
|
|> rotate(pitch = -p4A)
|
||||||
// Draw the first profile for the femoral stem
|
// Draw the first profile for the femoral stem
|
||||||
p5Z = 140
|
p5Z = 140
|
||||||
p5A = 36.5
|
p5A = 36.5
|
||||||
plane005 = {
|
|
||||||
origin = [0, 0.0, p5Z],
|
|
||||||
xAxis = [cos(p5A), 0, sin(p5A)],
|
|
||||||
yAxis = [0.0, 1.0, 0.0]
|
|
||||||
}
|
|
||||||
l5 = 1.6
|
l5 = 1.6
|
||||||
r5 = 1.6
|
r5 = 1.6
|
||||||
stemLoftProfile5 = startSketchOn(plane005)
|
stemLoftProfile5 = startSketchOn(XY)
|
||||||
|> startProfile(at = [-19.5, -l5 / 2])
|
|> startProfile(at = [-19.5, -l5 / 2])
|
||||||
|> yLine(length = l5, tag = $seg05)
|
|> yLine(length = l5, tag = $seg05)
|
||||||
|> tangentialArc(angle = -120, radius = r5)
|
|> tangentialArc(angle = -120, radius = r5)
|
||||||
@ -89,18 +76,14 @@ stemLoftProfile5 = startSketchOn(plane005)
|
|||||||
|> angledLine(angle = 30, length = -segLen(seg05))
|
|> angledLine(angle = 30, length = -segLen(seg05))
|
||||||
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
|
|> translate(z = p5Z)
|
||||||
|
|> rotate(pitch = -p5A)
|
||||||
// Draw the second profile for the femoral stem
|
// Draw the second profile for the femoral stem
|
||||||
p6Z = 145
|
p6Z = 145
|
||||||
p6A = 36.5
|
p6A = 36.5
|
||||||
plane006 = {
|
|
||||||
origin = [0, 0.0, p6Z],
|
|
||||||
xAxis = [cos(p6A), 0, sin(p6A)],
|
|
||||||
yAxis = [0.0, 1.0, 0.0]
|
|
||||||
}
|
|
||||||
l6 = 1
|
l6 = 1
|
||||||
r6 = 3
|
r6 = 3
|
||||||
stemLoftProfile6 = startSketchOn(plane006)
|
stemLoftProfile6 = startSketchOn(XY)
|
||||||
|> startProfile(at = [-23.4, -l6 / 2])
|
|> startProfile(at = [-23.4, -l6 / 2])
|
||||||
|> yLine(length = l6, tag = $seg06)
|
|> yLine(length = l6, tag = $seg06)
|
||||||
|> tangentialArc(angle = -120, radius = r6)
|
|> tangentialArc(angle = -120, radius = r6)
|
||||||
@ -109,27 +92,24 @@ stemLoftProfile6 = startSketchOn(plane006)
|
|||||||
|> angledLine(angle = 30, length = -segLen(seg06))
|
|> angledLine(angle = 30, length = -segLen(seg06))
|
||||||
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|> close()
|
|> close()
|
||||||
|
|> translate(z = p6Z)
|
||||||
// Draw the third profile for the femoral stem
|
|> rotate(pitch = -p6A)
|
||||||
stemTab = clone(stemLoftProfile6)
|
|
||||||
|> extrude(%, length = 6)
|
|
||||||
|
|
||||||
// Loft the femur using all profiles in sequence
|
// Loft the femur using all profiles in sequence
|
||||||
|
|
||||||
femur = loft([
|
femur = loft([
|
||||||
stemLoftProfile1,
|
stemLoftProfile1,
|
||||||
stemLoftProfile2,
|
stemLoftProfile2,
|
||||||
stemLoftProfile3,
|
stemLoftProfile3,
|
||||||
stemLoftProfile4
|
stemLoftProfile4
|
||||||
])
|
])
|
||||||
|
|
||||||
// Loft the femoral stem
|
// Loft the femoral stem
|
||||||
femoralStem = loft([
|
femoralStem = loft([
|
||||||
clone(stemLoftProfile4),
|
clone(stemLoftProfile4),
|
||||||
stemLoftProfile5,
|
stemLoftProfile5,
|
||||||
stemLoftProfile6
|
clone(stemLoftProfile6)
|
||||||
])
|
])
|
||||||
|
// Draw the third profile for the femoral stem
|
||||||
|
stemTab = stemLoftProfile6
|
||||||
|
|> extrude(length = 6)
|
||||||
// Revolve a hollow socket to represent the femoral head
|
// Revolve a hollow socket to represent the femoral head
|
||||||
femoralHead = startSketchOn(XZ)
|
femoralHead = startSketchOn(XZ)
|
||||||
|> startProfile(at = [4, 0])
|
|> startProfile(at = [4, 0])
|
||||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
BIN
public/kcl-samples/screenshots/brake-rotor.png
Normal file
After Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 216 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
BIN
public/kcl-samples/screenshots/telemetry-antenna.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
public/kcl-samples/screenshots/truss-structure.png
Normal file
After Width: | Height: | Size: 68 KiB |
@ -53,8 +53,8 @@ baseSlab = boxFn(plane = XY, width = slabWidth, height = -baseThickness)
|
|||||||
|> appearance(%, color = "#dbd7d2")
|
|> appearance(%, color = "#dbd7d2")
|
||||||
|
|
||||||
// Create ground platform beneath the base
|
// Create ground platform beneath the base
|
||||||
goundSize = 50
|
groundSize = 50
|
||||||
groundBody = boxFn(plane = offsetPlane(XY, offset = -baseThickness), width = goundSize, height = -5)
|
groundBody = boxFn(plane = offsetPlane(XY, offset = -baseThickness), width = groundSize, height = -5)
|
||||||
|> appearance(%, color = "#3a3631")
|
|> appearance(%, color = "#3a3631")
|
||||||
|
|
||||||
// Create a single slab with handrail height to be reused with pattern
|
// Create a single slab with handrail height to be reused with pattern
|
||||||
|
63
public/kcl-samples/telemetry-antenna/main.kcl
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Aircraft telemetry antenna plate
|
||||||
|
// Consists of a circular base plate 3 inches in diameter and 0.08 inches thick, with a tapered monopole antenna mounted at the top with a base diameter of 0.65 inches and height of 1.36 inches. Also consists of a mounting base and connector at the bottom of the plate. The plate also has 6 countersunk holes at a defined pitch circle diameter.
|
||||||
|
|
||||||
|
// Set units
|
||||||
|
@settings(defaultLengthUnit = in)
|
||||||
|
|
||||||
|
// Define parameters
|
||||||
|
plateThickness = 0.08
|
||||||
|
plateDia = 3
|
||||||
|
antennaBaseDia = 0.65
|
||||||
|
antennaAngle = 95
|
||||||
|
antennaHeight = 1.36
|
||||||
|
seatingDia = 0.625
|
||||||
|
totalHeight = 2.14
|
||||||
|
|
||||||
|
boltDiameter = .196
|
||||||
|
boltPitchCircleDiameter = 2.5
|
||||||
|
|
||||||
|
// 2D cross-sectional profile of the part that will later be revolved
|
||||||
|
antennaCrossSectionSketch = startSketchOn(YZ)
|
||||||
|
antennaCrossSectionProfile = startProfile(antennaCrossSectionSketch, at = [plateDia / 2, 0])
|
||||||
|
|> yLine(length = plateThickness)
|
||||||
|
|> xLine(length = -(plateDia - antennaBaseDia) / 2, tag = $seg03)
|
||||||
|
|> angledLine(angle = antennaAngle, length = 1.1, tag = $seg01)
|
||||||
|
|> tangentialArc(endAbsolute = [0.025, antennaHeight])
|
||||||
|
|> xLine(endAbsolute = 0, tag = $seg02)
|
||||||
|
|> yLine(length = -totalHeight)
|
||||||
|
|> xLine(length = .25)
|
||||||
|
|> yLine(length = .05)
|
||||||
|
|> angledLine(angle = 45, length = 0.025)
|
||||||
|
|> yLine(length = .125)
|
||||||
|
|> angledLine(angle = 135, length = 0.025)
|
||||||
|
|> yLine(length = .125)
|
||||||
|
|> xLine(length = .025)
|
||||||
|
|> yLine(length = .025)
|
||||||
|
|> xLine(endAbsolute = seatingDia / 2)
|
||||||
|
|> yLine(endAbsolute = -0.25)
|
||||||
|
|> xLine(endAbsolute = 0.6)
|
||||||
|
|> yLine(endAbsolute = 0)
|
||||||
|
|> close()
|
||||||
|
|
||||||
|
// Revolution about y-axis of earlier profile
|
||||||
|
antennaCrossSectionRevolve = revolve(antennaCrossSectionProfile, angle = 360, axis = Y)
|
||||||
|
|
||||||
|
// Function to create a countersunk hole
|
||||||
|
fn countersink(@holePosition) {
|
||||||
|
startSketchOn(antennaCrossSectionRevolve, face = seg03)
|
||||||
|
|> circle(center = holePosition, radius = boltDiameter / 2, tag = $hole01)
|
||||||
|
|> extrude(length = -plateThickness)
|
||||||
|
|> chamfer(length = 0.04, tags = [hole01])
|
||||||
|
return { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// PCD converted to radius for positioning the holes
|
||||||
|
r = boltPitchCircleDiameter / 2
|
||||||
|
|
||||||
|
// 6 countersunk holes using the countersink function
|
||||||
|
countersink([r, 0]) // 0 °
|
||||||
|
countersink([r * 0.5, r * 0.8660254]) // 60 °
|
||||||
|
countersink([-r * 0.5, r * 0.8660254]) // 120 °
|
||||||
|
countersink([-r, 0]) // 180 °
|
||||||
|
countersink([-r * 0.5, -r * 0.8660254]) // 240 °
|
||||||
|
countersink([r * 0.5, -r * 0.8660254]) // 300 °
|
142
public/kcl-samples/truss-structure/main.kcl
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
// Truss Structure
|
||||||
|
// A truss structure is a framework composed of triangular units made from straight members connected at joints, often called nodes. Trusses are widely used in architecture, civil engineering, and construction for their ability to support large loads with minimal material.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@settings(defaultLengthUnit = in)
|
||||||
|
|
||||||
|
// Define parameters
|
||||||
|
thickness = 4
|
||||||
|
totalLength = 180
|
||||||
|
totalWidth = 120
|
||||||
|
totalHeight = 120
|
||||||
|
legHeight = 48
|
||||||
|
topTrussAngle = 25
|
||||||
|
beamWidth = 4
|
||||||
|
beamLength = 2
|
||||||
|
sparAngle = 30
|
||||||
|
nFrames = 3
|
||||||
|
crossBeamLength = 82
|
||||||
|
|
||||||
|
// Sketch the top frame
|
||||||
|
topFrameSketch = startSketchOn(YZ)
|
||||||
|
profile001 = startProfile(topFrameSketch, at = [totalWidth / 2, 0])
|
||||||
|
|> xLine(length = -totalWidth, tag = $bottomFace)
|
||||||
|
|> yLine(length = 12)
|
||||||
|
|> angledLine(angle = topTrussAngle, endAbsoluteX = 0, tag = $tag001)
|
||||||
|
|> angledLine(angle = -topTrussAngle, endAbsoluteX = totalWidth / 2, tag = $tag002)
|
||||||
|
|> close()
|
||||||
|
|
||||||
|
// Create two holes in the top frame sketch to create the center beam
|
||||||
|
profile002 = startProfile(topFrameSketch, at = [totalWidth / 2 - thickness, thickness])
|
||||||
|
|> xLine(endAbsolute = thickness / 2)
|
||||||
|
|> yLine(endAbsolute = segEndY(tag001) - thickness)
|
||||||
|
|> angledLine(endAbsoluteX = profileStartX(%), angle = -topTrussAngle)
|
||||||
|
|> close(%)
|
||||||
|
|
||||||
|
profile003 = startProfile(topFrameSketch, at = [-totalWidth / 2 + thickness, thickness])
|
||||||
|
|> xLine(endAbsolute = -thickness / 2)
|
||||||
|
|> yLine(endAbsolute = segEndY(tag001) - thickness)
|
||||||
|
|> angledLine(endAbsoluteX = profileStartX(%), angle = 180 + topTrussAngle)
|
||||||
|
|> close(%)
|
||||||
|
profile004 = subtract2d(profile001, tool = profile002)
|
||||||
|
subtract2d(profile001, tool = profile003)
|
||||||
|
|
||||||
|
// Extrude the sketch to make the top frame
|
||||||
|
topFrame = extrude(profile001, length = beamLength)
|
||||||
|
|
||||||
|
// Spar 1
|
||||||
|
sketch002 = startSketchOn(offsetPlane(YZ, offset = .1))
|
||||||
|
profile006 = startProfile(sketch002, at = [thickness / 2 - 1, 14])
|
||||||
|
|> angledLine(angle = sparAngle, length = 25)
|
||||||
|
|> angledLine(angle = -topTrussAngle, length = 5)
|
||||||
|
|> angledLine(angle = 180 + sparAngle, endAbsoluteX = profileStartX(%))
|
||||||
|
|> close(%)
|
||||||
|
|
||||||
|
spar001 = extrude(profile006, length = 1.8)
|
||||||
|
|
||||||
|
// Spar2
|
||||||
|
profile007 = startProfile(sketch002, at = [-thickness / 2 + 1, 14])
|
||||||
|
|> angledLine(angle = 180 - sparAngle, length = 25)
|
||||||
|
|> angledLine(angle = 180 + topTrussAngle, length = 5)
|
||||||
|
|> angledLine(angle = -sparAngle, endAbsoluteX = profileStartX(%))
|
||||||
|
|> close(%)
|
||||||
|
|
||||||
|
spar002 = extrude(profile007, length = 1.8)
|
||||||
|
|
||||||
|
// Combine the top frame with the intermediate support beams
|
||||||
|
newFrame = topFrame + spar001 + spar002
|
||||||
|
|
||||||
|
// Create the two legs on the frame
|
||||||
|
leg001Sketch = startSketchOn(offsetPlane(XY, offset = .1))
|
||||||
|
legProfile001 = startProfile(leg001Sketch, at = [0, -totalWidth / 2])
|
||||||
|
|> xLine(%, length = beamLength - .1)
|
||||||
|
|> yLine(%, length = beamWidth - 1)
|
||||||
|
|> xLine(%, endAbsolute = profileStartX(%))
|
||||||
|
|> close(%)
|
||||||
|
|
||||||
|
legProfile002 = startProfile(leg001Sketch, at = [0, totalWidth / 2])
|
||||||
|
|> xLine(%, length = beamLength - .1)
|
||||||
|
|> yLine(%, length = -(beamWidth - 1))
|
||||||
|
|> xLine(%, endAbsolute = profileStartX(%))
|
||||||
|
|> close(%)
|
||||||
|
leg001 = extrude(legProfile001, length = -legHeight - .1)
|
||||||
|
leg002 = extrude(legProfile002, length = -legHeight - .1)
|
||||||
|
|
||||||
|
// Combine the top frame with the legs and pattern
|
||||||
|
fullFrame = newFrame + leg001 + leg002
|
||||||
|
|> patternLinear3d(
|
||||||
|
%,
|
||||||
|
instances = nFrames,
|
||||||
|
distance = crossBeamLength + beamLength,
|
||||||
|
axis = [-1, 0, 0],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create the center cross beam
|
||||||
|
centerCrossBeamSketch = startSketchOn(YZ)
|
||||||
|
profile005 = startProfile(centerCrossBeamSketch, at = [0, segEndY(tag001) - 1])
|
||||||
|
|> angledLine(%, angle = -topTrussAngle, length = beamWidth * 3 / 8)
|
||||||
|
|> yLine(length = -beamWidth * 3 / 8)
|
||||||
|
|> angledLine(%, angle = 180 - topTrussAngle, length = beamWidth * 3 / 8)
|
||||||
|
|> angledLine(%, angle = 180 + topTrussAngle, length = beamWidth * 3 / 8)
|
||||||
|
|> yLine(length = beamWidth * 3 / 8)
|
||||||
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||||
|
|> close()
|
||||||
|
|
||||||
|
// Extrude the center cross beam and pattern to every frame
|
||||||
|
centerCrossBeam = extrude(profile005, length = -crossBeamLength)
|
||||||
|
|> patternLinear3d(
|
||||||
|
%,
|
||||||
|
instances = nFrames - 1,
|
||||||
|
distance = crossBeamLength + beamLength,
|
||||||
|
axis = [-1, 0, 0],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create the two side cross beams
|
||||||
|
sideCrossBeamSketch = startSketchOn(-YZ)
|
||||||
|
profile008 = startProfile(
|
||||||
|
sideCrossBeamSketch,
|
||||||
|
at = [
|
||||||
|
-totalWidth / 2 + 0.5,
|
||||||
|
segEndY(tag002) - .5
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|> yLine(length = -beamLength)
|
||||||
|
|> xLine(length = 3 / 4 * beamWidth)
|
||||||
|
|> yLine(length = beamLength)
|
||||||
|
|> close()
|
||||||
|
profile009 = startProfile(sideCrossBeamSketch, at = [totalWidth / 2, segEndY(tag002) - .5])
|
||||||
|
|> yLine(length = -beamLength)
|
||||||
|
|> xLine(%, length = -3 / 4 * beamWidth)
|
||||||
|
|> yLine(%, length = beamLength)
|
||||||
|
|> close(%)
|
||||||
|
|
||||||
|
// Extrude the side cross beams and pattern to every frame.
|
||||||
|
extrude([profile008, profile009], length = crossBeamLength)
|
||||||
|
|> patternLinear3d(
|
||||||
|
%,
|
||||||
|
instances = nFrames - 1,
|
||||||
|
distance = crossBeamLength + beamLength,
|
||||||
|
axis = [-1, 0, 0],
|
||||||
|
)
|
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB |
20
rust/Cargo.lock
generated
@ -1815,7 +1815,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-bumper"
|
name = "kcl-bumper"
|
||||||
version = "0.1.74"
|
version = "0.1.76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@ -1826,7 +1826,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-derive-docs"
|
name = "kcl-derive-docs"
|
||||||
version = "0.1.74"
|
version = "0.1.76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Inflector",
|
"Inflector",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -1845,7 +1845,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-directory-test-macro"
|
name = "kcl-directory-test-macro"
|
||||||
version = "0.1.74"
|
version = "0.1.76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@ -1855,7 +1855,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-language-server"
|
name = "kcl-language-server"
|
||||||
version = "0.2.74"
|
version = "0.2.76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@ -1876,7 +1876,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-language-server-release"
|
name = "kcl-language-server-release"
|
||||||
version = "0.1.74"
|
version = "0.1.76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@ -1896,7 +1896,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
version = "0.2.74"
|
version = "0.2.76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"approx 0.5.1",
|
"approx 0.5.1",
|
||||||
@ -1973,7 +1973,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-python-bindings"
|
name = "kcl-python-bindings"
|
||||||
version = "0.3.74"
|
version = "0.3.76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"kcl-lib",
|
"kcl-lib",
|
||||||
@ -1988,7 +1988,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-test-server"
|
name = "kcl-test-server"
|
||||||
version = "0.1.74"
|
version = "0.1.76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"hyper 0.14.32",
|
"hyper 0.14.32",
|
||||||
@ -2001,7 +2001,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-to-core"
|
name = "kcl-to-core"
|
||||||
version = "0.1.74"
|
version = "0.1.76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@ -2015,7 +2015,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-wasm-lib"
|
name = "kcl-wasm-lib"
|
||||||
version = "0.1.74"
|
version = "0.1.76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bson",
|
"bson",
|
||||||
|
@ -36,20 +36,20 @@ new-sim-test test_name render_to_png="true":
|
|||||||
|
|
||||||
# Run a KCL deterministic simulation test case and accept output.
|
# Run a KCL deterministic simulation test case and accept output.
|
||||||
overwrite-sim-test-sample test_name:
|
overwrite-sim-test-sample test_name:
|
||||||
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::kcl_samples::parse_{{test_name}}
|
ZOO_SIM_UPDATE=always EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::kcl_samples::parse_{{test_name}}
|
||||||
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::kcl_samples::unparse_{{test_name}}
|
ZOO_SIM_UPDATE=always EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::kcl_samples::unparse_{{test_name}}
|
||||||
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::kcl_samples::kcl_test_execute_{{test_name}}
|
ZOO_SIM_UPDATE=always EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::kcl_samples::kcl_test_execute_{{test_name}}
|
||||||
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::kcl_samples::test_after_engine
|
ZOO_SIM_UPDATE=always EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::kcl_samples::test_after_engine
|
||||||
|
|
||||||
overwrite-sim-test test_name:
|
overwrite-sim-test test_name:
|
||||||
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::{{test_name}}::parse
|
ZOO_SIM_UPDATE=always EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::{{test_name}}::parse
|
||||||
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::{{test_name}}::unparse
|
ZOO_SIM_UPDATE=always EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::{{test_name}}::unparse
|
||||||
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::{{test_name}}::kcl_test_execute
|
ZOO_SIM_UPDATE=always EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::{{test_name}}::kcl_test_execute
|
||||||
[ {{test_name}} != "kcl_samples" ] || EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::{{test_name}}::test_after_engine
|
[ {{test_name}} != "kcl_samples" ] || ZOO_SIM_UPDATE=always EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests::{{test_name}}::test_after_engine
|
||||||
|
|
||||||
# Regenerate all the simulation test output.
|
# Regenerate all the simulation test output.
|
||||||
redo-sim-tests:
|
redo-sim-tests:
|
||||||
EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests
|
ZOO_SIM_UPDATE=always EXPECTORATE=overwrite TWENTY_TWENTY=overwrite {{cita}} {{kcl_lib_flags}} --no-quiet -- simulation_tests
|
||||||
|
|
||||||
test:
|
test:
|
||||||
cargo install cargo-nextest
|
cargo install cargo-nextest
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "kcl-bumper"
|
name = "kcl-bumper"
|
||||||
version = "0.1.74"
|
version = "0.1.76"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
repository = "https://github.com/KittyCAD/modeling-api"
|
repository = "https://github.com/KittyCAD/modeling-api"
|
||||||
rust-version = "1.76"
|
rust-version = "1.76"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-derive-docs"
|
name = "kcl-derive-docs"
|
||||||
description = "A tool for generating documentation from Rust derive macros"
|
description = "A tool for generating documentation from Rust derive macros"
|
||||||
version = "0.1.74"
|
version = "0.1.76"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
139
rust/kcl-derive-docs/src/example_tests.rs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
use proc_macro2::Span;
|
||||||
|
use quote::{quote, ToTokens};
|
||||||
|
|
||||||
|
pub fn do_for_each_example_test(item: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
|
||||||
|
let item: syn::ItemFn = syn::parse2(item.clone()).unwrap();
|
||||||
|
let mut result = proc_macro2::TokenStream::new();
|
||||||
|
for name in TEST_NAMES {
|
||||||
|
let mut item = item.clone();
|
||||||
|
item.sig.ident = syn::Ident::new(
|
||||||
|
&format!("{}_{}", item.sig.ident, name.replace('-', "_")),
|
||||||
|
Span::call_site(),
|
||||||
|
);
|
||||||
|
let name = name.to_owned();
|
||||||
|
let stmts = &item.block.stmts;
|
||||||
|
let block = quote! {
|
||||||
|
{
|
||||||
|
const NAME: &str = #name;
|
||||||
|
#(#stmts)*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
item.block = Box::new(syn::parse2(block).unwrap());
|
||||||
|
result.extend(Some(item.into_token_stream()));
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn do_for_all_example_test(item: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
|
||||||
|
let mut item: syn::ItemFn = syn::parse2(item).unwrap();
|
||||||
|
let len = TEST_NAMES.len();
|
||||||
|
|
||||||
|
let stmts = &item.block.stmts;
|
||||||
|
let test_names = TEST_NAMES.iter().map(|n| n.to_owned());
|
||||||
|
let block = quote! {
|
||||||
|
{
|
||||||
|
const TEST_NAMES: [&str; #len] = [#(#test_names,)*];
|
||||||
|
#(#stmts)*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
item.block = Box::new(syn::parse2(block).unwrap());
|
||||||
|
|
||||||
|
item.into_token_stream()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TEST_NAMES: [&str; 93] = [
|
||||||
|
"std-array-map-0",
|
||||||
|
"std-array-map-1",
|
||||||
|
"std-array-pop-0",
|
||||||
|
"std-array-push-0",
|
||||||
|
"std-array-reduce-0",
|
||||||
|
"std-array-reduce-1",
|
||||||
|
"std-array-reduce-2",
|
||||||
|
"std-clone-0",
|
||||||
|
"std-clone-1",
|
||||||
|
"std-clone-2",
|
||||||
|
"std-clone-3",
|
||||||
|
"std-clone-4",
|
||||||
|
"std-clone-5",
|
||||||
|
"std-clone-6",
|
||||||
|
"std-clone-7",
|
||||||
|
"std-clone-8",
|
||||||
|
"std-clone-9",
|
||||||
|
"std-helix-0",
|
||||||
|
"std-helix-1",
|
||||||
|
"std-helix-2",
|
||||||
|
"std-helix-3",
|
||||||
|
"std-math-abs-0",
|
||||||
|
"std-math-acos-0",
|
||||||
|
"std-math-asin-0",
|
||||||
|
"std-math-atan-0",
|
||||||
|
"std-math-atan2-0",
|
||||||
|
"std-math-ceil-0",
|
||||||
|
"std-math-cos-0",
|
||||||
|
"std-math-floor-0",
|
||||||
|
"std-math-ln-0",
|
||||||
|
"std-math-legLen-0",
|
||||||
|
"std-math-legAngX-0",
|
||||||
|
"std-math-legAngY-0",
|
||||||
|
"std-math-log-0",
|
||||||
|
"std-math-log10-0",
|
||||||
|
"std-math-log2-0",
|
||||||
|
"std-math-max-0",
|
||||||
|
"std-math-min-0",
|
||||||
|
"std-math-polar-0",
|
||||||
|
"std-math-pow-0",
|
||||||
|
"std-math-rem-0",
|
||||||
|
"std-math-round-0",
|
||||||
|
"std-math-sin-0",
|
||||||
|
"std-math-sqrt-0",
|
||||||
|
"std-math-tan-0",
|
||||||
|
"std-offsetPlane-0",
|
||||||
|
"std-offsetPlane-1",
|
||||||
|
"std-offsetPlane-2",
|
||||||
|
"std-offsetPlane-3",
|
||||||
|
"std-offsetPlane-4",
|
||||||
|
"std-sketch-circle-0",
|
||||||
|
"std-sketch-circle-1",
|
||||||
|
"std-sketch-patternTransform2d-0",
|
||||||
|
"std-sketch-revolve-0",
|
||||||
|
"std-sketch-revolve-1",
|
||||||
|
"std-sketch-revolve-10",
|
||||||
|
"std-sketch-revolve-11",
|
||||||
|
"std-sketch-revolve-12",
|
||||||
|
"std-sketch-revolve-2",
|
||||||
|
"std-sketch-revolve-3",
|
||||||
|
"std-sketch-revolve-4",
|
||||||
|
"std-sketch-revolve-5",
|
||||||
|
"std-sketch-revolve-6",
|
||||||
|
"std-sketch-revolve-7",
|
||||||
|
"std-sketch-revolve-8",
|
||||||
|
"std-sketch-revolve-9",
|
||||||
|
"std-solid-chamfer-0",
|
||||||
|
"std-solid-chamfer-1",
|
||||||
|
"std-solid-fillet-0",
|
||||||
|
"std-solid-fillet-1",
|
||||||
|
"std-solid-hollow-0",
|
||||||
|
"std-solid-hollow-1",
|
||||||
|
"std-solid-hollow-2",
|
||||||
|
"std-solid-patternTransform-0",
|
||||||
|
"std-solid-patternTransform-1",
|
||||||
|
"std-solid-patternTransform-2",
|
||||||
|
"std-solid-patternTransform-3",
|
||||||
|
"std-solid-patternTransform-4",
|
||||||
|
"std-solid-patternTransform-5",
|
||||||
|
"std-solid-shell-0",
|
||||||
|
"std-solid-shell-1",
|
||||||
|
"std-solid-shell-2",
|
||||||
|
"std-solid-shell-3",
|
||||||
|
"std-solid-shell-4",
|
||||||
|
"std-solid-shell-5",
|
||||||
|
"std-solid-shell-6",
|
||||||
|
"std-transform-mirror2d-0",
|
||||||
|
"std-transform-mirror2d-1",
|
||||||
|
"std-transform-mirror2d-2",
|
||||||
|
"std-transform-mirror2d-3",
|
||||||
|
"std-transform-mirror2d-4",
|
||||||
|
"std-units-toDegrees-0",
|
||||||
|
"std-units-toRadians-0",
|
||||||
|
];
|
@ -2,16 +2,16 @@
|
|||||||
// automated enforcement.
|
// automated enforcement.
|
||||||
#![allow(clippy::style)]
|
#![allow(clippy::style)]
|
||||||
|
|
||||||
|
mod example_tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
mod unbox;
|
mod unbox;
|
||||||
|
|
||||||
use std::{collections::HashMap, fs};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use convert_case::Casing;
|
use convert_case::Casing;
|
||||||
use inflector::{cases::camelcase::to_camel_case, Inflector};
|
use inflector::{cases::camelcase::to_camel_case, Inflector};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use proc_macro2::Span;
|
|
||||||
use quote::{format_ident, quote, quote_spanned, ToTokens};
|
use quote::{format_ident, quote, quote_spanned, ToTokens};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@ -28,8 +28,13 @@ pub fn stdlib(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> p
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn for_each_std_mod(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
pub fn for_each_example_test(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
do_for_each_std_mod(item.into()).into()
|
example_tests::do_for_each_example_test(item.into()).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn for_all_example_test(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
|
example_tests::do_for_all_example_test(item.into()).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Describes an argument of a stdlib function.
|
/// Describes an argument of a stdlib function.
|
||||||
@ -42,6 +47,14 @@ struct ArgMetadata {
|
|||||||
/// Does not do anything if the argument is already required.
|
/// Does not do anything if the argument is already required.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
include_in_snippet: bool,
|
include_in_snippet: bool,
|
||||||
|
|
||||||
|
/// The snippet should suggest this value for the arg.
|
||||||
|
#[serde(default)]
|
||||||
|
snippet_value: Option<String>,
|
||||||
|
|
||||||
|
/// The snippet should suggest this value for the arg.
|
||||||
|
#[serde(default)]
|
||||||
|
snippet_value_array: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@ -69,11 +82,6 @@ struct StdlibMetadata {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
feature_tree_operation: bool,
|
feature_tree_operation: bool,
|
||||||
|
|
||||||
/// If true, expects keyword arguments.
|
|
||||||
/// If false, expects positional arguments.
|
|
||||||
#[serde(default)]
|
|
||||||
keywords: bool,
|
|
||||||
|
|
||||||
/// If true, the first argument is unlabeled.
|
/// If true, the first argument is unlabeled.
|
||||||
/// If false, all arguments require labels.
|
/// If false, all arguments require labels.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@ -92,34 +100,6 @@ fn do_stdlib(
|
|||||||
do_stdlib_inner(metadata, attr, item)
|
do_stdlib_inner(metadata, attr, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn do_for_each_std_mod(item: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
|
|
||||||
let item: syn::ItemFn = syn::parse2(item.clone()).unwrap();
|
|
||||||
let mut result = proc_macro2::TokenStream::new();
|
|
||||||
for name in fs::read_dir("kcl-lib/std").unwrap().filter_map(|e| {
|
|
||||||
let e = e.unwrap();
|
|
||||||
let filename = e.file_name();
|
|
||||||
filename.to_str().unwrap().strip_suffix(".kcl").map(str::to_owned)
|
|
||||||
}) {
|
|
||||||
for i in 0..10_usize {
|
|
||||||
let mut item = item.clone();
|
|
||||||
item.sig.ident = syn::Ident::new(&format!("{}_{}_shard_{i}", item.sig.ident, name), Span::call_site());
|
|
||||||
let stmts = &item.block.stmts;
|
|
||||||
let block = quote! {
|
|
||||||
{
|
|
||||||
const STD_MOD_NAME: &str = #name;
|
|
||||||
const SHARD: usize = #i;
|
|
||||||
const SHARD_COUNT: usize = 10;
|
|
||||||
#(#stmts)*
|
|
||||||
}
|
|
||||||
};
|
|
||||||
item.block = Box::new(syn::parse2(block).unwrap());
|
|
||||||
result.extend(Some(item.into_token_stream()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_output(res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>) -> proc_macro::TokenStream {
|
fn do_output(res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>) -> proc_macro::TokenStream {
|
||||||
match res {
|
match res {
|
||||||
Err(err) => err.to_compile_error().into(),
|
Err(err) => err.to_compile_error().into(),
|
||||||
@ -307,12 +287,6 @@ fn do_stdlib_inner(
|
|||||||
quote! { false }
|
quote! { false }
|
||||||
};
|
};
|
||||||
|
|
||||||
let uses_keyword_arguments = if metadata.keywords {
|
|
||||||
quote! { true }
|
|
||||||
} else {
|
|
||||||
quote! { false }
|
|
||||||
};
|
|
||||||
|
|
||||||
let docs_crate = get_crate(None);
|
let docs_crate = get_crate(None);
|
||||||
|
|
||||||
// When the user attaches this proc macro to a function with the wrong type
|
// When the user attaches this proc macro to a function with the wrong type
|
||||||
@ -341,6 +315,10 @@ fn do_stdlib_inner(
|
|||||||
}
|
}
|
||||||
.trim_start_matches('_')
|
.trim_start_matches('_')
|
||||||
.to_string();
|
.to_string();
|
||||||
|
// These aren't really KCL args, they're just state that each stdlib function's impl needs.
|
||||||
|
if arg_name == "exec_state" || arg_name == "args" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let ty = match arg {
|
let ty = match arg {
|
||||||
syn::FnArg::Receiver(pat) => pat.ty.as_ref().into_token_stream(),
|
syn::FnArg::Receiver(pat) => pat.ty.as_ref().into_token_stream(),
|
||||||
@ -351,27 +329,24 @@ fn do_stdlib_inner(
|
|||||||
|
|
||||||
let ty_string = rust_type_to_openapi_type(&ty_string);
|
let ty_string = rust_type_to_openapi_type(&ty_string);
|
||||||
let required = !ty_ident.to_string().starts_with("Option <");
|
let required = !ty_ident.to_string().starts_with("Option <");
|
||||||
let arg_meta = metadata.args.get(&arg_name);
|
let Some(arg_meta) = metadata.args.get(&arg_name) else {
|
||||||
let description = if let Some(s) = arg_meta.map(|arg| &arg.docs) {
|
errors.push(Error::new_spanned(arg, format!("arg {arg_name} not found")));
|
||||||
quote! { #s }
|
|
||||||
} else if metadata.keywords && ty_string != "Args" && ty_string != "ExecState" {
|
|
||||||
errors.push(Error::new_spanned(
|
|
||||||
&arg,
|
|
||||||
"Argument was not documented in the args block",
|
|
||||||
));
|
|
||||||
continue;
|
continue;
|
||||||
} else {
|
|
||||||
quote! { String::new() }
|
|
||||||
};
|
};
|
||||||
let include_in_snippet = required || arg_meta.map(|arg| arg.include_in_snippet).unwrap_or_default();
|
let description = arg_meta.docs.clone();
|
||||||
|
let include_in_snippet = required || arg_meta.include_in_snippet;
|
||||||
|
let snippet_value = arg_meta.snippet_value.clone();
|
||||||
|
let snippet_value_array = arg_meta.snippet_value_array.clone();
|
||||||
|
if snippet_value.is_some() && snippet_value_array.is_some() {
|
||||||
|
errors.push(Error::new_spanned(arg, format!("arg {arg_name} has set both snippet_value and snippet_value array, but at most one of these may be set. Please delete one of them.")));
|
||||||
|
}
|
||||||
let label_required = !(i == 0 && metadata.unlabeled_first);
|
let label_required = !(i == 0 && metadata.unlabeled_first);
|
||||||
let camel_case_arg_name = to_camel_case(&arg_name);
|
let camel_case_arg_name = to_camel_case(&arg_name);
|
||||||
if ty_string != "ExecState" && ty_string != "Args" {
|
if ty_string != "ExecState" && ty_string != "Args" {
|
||||||
let schema = quote! {
|
let schema = quote! {
|
||||||
generator.root_schema_for::<#ty_ident>()
|
generator.root_schema_for::<#ty_ident>()
|
||||||
};
|
};
|
||||||
arg_types.push(quote! {
|
let q0 = quote! {
|
||||||
#docs_crate::StdLibFnArg {
|
|
||||||
name: #camel_case_arg_name.to_string(),
|
name: #camel_case_arg_name.to_string(),
|
||||||
type_: #ty_string.to_string(),
|
type_: #ty_string.to_string(),
|
||||||
schema: #schema,
|
schema: #schema,
|
||||||
@ -379,6 +354,32 @@ fn do_stdlib_inner(
|
|||||||
label_required: #label_required,
|
label_required: #label_required,
|
||||||
description: #description.to_string(),
|
description: #description.to_string(),
|
||||||
include_in_snippet: #include_in_snippet,
|
include_in_snippet: #include_in_snippet,
|
||||||
|
};
|
||||||
|
let q1 = if let Some(snippet_value) = snippet_value {
|
||||||
|
quote! {
|
||||||
|
snippet_value: Some(#snippet_value.to_owned()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
snippet_value: None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let q2 = if let Some(snippet_value_array) = snippet_value_array {
|
||||||
|
quote! {
|
||||||
|
snippet_value_array: Some(vec![
|
||||||
|
#(#snippet_value_array.to_owned()),*
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
snippet_value_array: None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
arg_types.push(quote! {
|
||||||
|
#docs_crate::StdLibFnArg {
|
||||||
|
#q0
|
||||||
|
#q1
|
||||||
|
#q2
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -442,6 +443,8 @@ fn do_stdlib_inner(
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -508,10 +511,6 @@ fn do_stdlib_inner(
|
|||||||
vec![#(#tags),*]
|
vec![#(#tags),*]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
#uses_keyword_arguments
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<#docs_crate::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<#docs_crate::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
// We set this to false so we can recurse them later.
|
// We set this to false so we can recurse them later.
|
||||||
|
@ -40,6 +40,9 @@ fn test_args_with_refs() {
|
|||||||
let (item, mut errors) = do_stdlib(
|
let (item, mut errors) = do_stdlib(
|
||||||
quote! {
|
quote! {
|
||||||
name = "someFn",
|
name = "someFn",
|
||||||
|
args = {
|
||||||
|
data = { docs = "The data for this function"},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
quote! {
|
quote! {
|
||||||
/// Docs
|
/// Docs
|
||||||
@ -65,6 +68,9 @@ fn test_args_with_lifetime() {
|
|||||||
let (item, mut errors) = do_stdlib(
|
let (item, mut errors) = do_stdlib(
|
||||||
quote! {
|
quote! {
|
||||||
name = "someFn",
|
name = "someFn",
|
||||||
|
args = {
|
||||||
|
data = { docs = "Arg for the function" },
|
||||||
|
}
|
||||||
},
|
},
|
||||||
quote! {
|
quote! {
|
||||||
/// Docs
|
/// Docs
|
||||||
@ -117,7 +123,8 @@ fn test_stdlib_line_to() {
|
|||||||
quote! {
|
quote! {
|
||||||
name = "lineTo",
|
name = "lineTo",
|
||||||
args = {
|
args = {
|
||||||
sketch = { docs = "the sketch you're adding the line to" }
|
data = { docs = "the sketch you're adding the line to" },
|
||||||
|
sketch = { docs = "the sketch you're adding the line to" },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
quote! {
|
quote! {
|
||||||
|
@ -91,10 +91,6 @@ impl crate::docs::StdLibFn for SomeFn {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
settings.inline_subschemas = inline_subschemas;
|
settings.inline_subschemas = inline_subschemas;
|
||||||
@ -105,8 +101,10 @@ impl crate::docs::StdLibFn for SomeFn {
|
|||||||
schema: generator.root_schema_for::<Foo>(),
|
schema: generator.root_schema_for::<Foo>(),
|
||||||
required: true,
|
required: true,
|
||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new().to_string(),
|
description: "Arg for the function".to_string(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +121,8 @@ impl crate::docs::StdLibFn for SomeFn {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,10 +91,6 @@ impl crate::docs::StdLibFn for SomeFn {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
settings.inline_subschemas = inline_subschemas;
|
settings.inline_subschemas = inline_subschemas;
|
||||||
@ -105,8 +101,10 @@ impl crate::docs::StdLibFn for SomeFn {
|
|||||||
schema: generator.root_schema_for::<str>(),
|
schema: generator.root_schema_for::<str>(),
|
||||||
required: true,
|
required: true,
|
||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new().to_string(),
|
description: "The data for this function".to_string(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +121,8 @@ impl crate::docs::StdLibFn for SomeFn {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,23 +92,11 @@ impl crate::docs::StdLibFn for Show {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
settings.inline_subschemas = inline_subschemas;
|
settings.inline_subschemas = inline_subschemas;
|
||||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
vec![crate::docs::StdLibFnArg {
|
vec![]
|
||||||
name: "args".to_string(),
|
|
||||||
type_: "[number]".to_string(),
|
|
||||||
schema: generator.root_schema_for::<[f64; 2usize]>(),
|
|
||||||
required: true,
|
|
||||||
label_required: true,
|
|
||||||
description: String::new().to_string(),
|
|
||||||
include_in_snippet: true,
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
||||||
@ -124,6 +112,8 @@ impl crate::docs::StdLibFn for Show {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,23 +92,11 @@ impl crate::docs::StdLibFn for Show {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
settings.inline_subschemas = inline_subschemas;
|
settings.inline_subschemas = inline_subschemas;
|
||||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
vec![crate::docs::StdLibFnArg {
|
vec![]
|
||||||
name: "args".to_string(),
|
|
||||||
type_: "number".to_string(),
|
|
||||||
schema: generator.root_schema_for::<f64>(),
|
|
||||||
required: true,
|
|
||||||
label_required: true,
|
|
||||||
description: String::new().to_string(),
|
|
||||||
include_in_snippet: true,
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
||||||
@ -124,6 +112,8 @@ impl crate::docs::StdLibFn for Show {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,23 +93,11 @@ impl crate::docs::StdLibFn for MyFunc {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
settings.inline_subschemas = inline_subschemas;
|
settings.inline_subschemas = inline_subschemas;
|
||||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
vec![crate::docs::StdLibFnArg {
|
vec![]
|
||||||
name: "args".to_string(),
|
|
||||||
type_: "kittycad::types::InputFormat".to_string(),
|
|
||||||
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
|
|
||||||
required: false,
|
|
||||||
label_required: true,
|
|
||||||
description: String::new().to_string(),
|
|
||||||
include_in_snippet: false,
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
||||||
@ -125,6 +113,8 @@ impl crate::docs::StdLibFn for MyFunc {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,10 +93,6 @@ impl crate::docs::StdLibFn for LineTo {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
settings.inline_subschemas = inline_subschemas;
|
settings.inline_subschemas = inline_subschemas;
|
||||||
@ -108,8 +104,10 @@ impl crate::docs::StdLibFn for LineTo {
|
|||||||
schema: generator.root_schema_for::<LineToData>(),
|
schema: generator.root_schema_for::<LineToData>(),
|
||||||
required: true,
|
required: true,
|
||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new().to_string(),
|
description: "the sketch you're adding the line to".to_string(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
},
|
},
|
||||||
crate::docs::StdLibFnArg {
|
crate::docs::StdLibFnArg {
|
||||||
name: "sketch".to_string(),
|
name: "sketch".to_string(),
|
||||||
@ -119,6 +117,8 @@ impl crate::docs::StdLibFn for LineTo {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: "the sketch you're adding the line to".to_string(),
|
description: "the sketch you're adding the line to".to_string(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -136,6 +136,8 @@ impl crate::docs::StdLibFn for LineTo {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,23 +92,11 @@ impl crate::docs::StdLibFn for Min {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
settings.inline_subschemas = inline_subschemas;
|
settings.inline_subschemas = inline_subschemas;
|
||||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
vec![crate::docs::StdLibFnArg {
|
vec![]
|
||||||
name: "args".to_string(),
|
|
||||||
type_: "[number]".to_string(),
|
|
||||||
schema: generator.root_schema_for::<Vec<f64>>(),
|
|
||||||
required: true,
|
|
||||||
label_required: true,
|
|
||||||
description: String::new().to_string(),
|
|
||||||
include_in_snippet: true,
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
||||||
@ -124,6 +112,8 @@ impl crate::docs::StdLibFn for Min {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,23 +92,11 @@ impl crate::docs::StdLibFn for Show {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
settings.inline_subschemas = inline_subschemas;
|
settings.inline_subschemas = inline_subschemas;
|
||||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
vec![crate::docs::StdLibFnArg {
|
vec![]
|
||||||
name: "args".to_string(),
|
|
||||||
type_: "number".to_string(),
|
|
||||||
schema: generator.root_schema_for::<Option<f64>>(),
|
|
||||||
required: false,
|
|
||||||
label_required: true,
|
|
||||||
description: String::new().to_string(),
|
|
||||||
include_in_snippet: false,
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
||||||
@ -124,6 +112,8 @@ impl crate::docs::StdLibFn for Show {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,23 +92,11 @@ impl crate::docs::StdLibFn for Import {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
settings.inline_subschemas = inline_subschemas;
|
settings.inline_subschemas = inline_subschemas;
|
||||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
vec![crate::docs::StdLibFnArg {
|
vec![]
|
||||||
name: "args".to_string(),
|
|
||||||
type_: "kittycad::types::InputFormat".to_string(),
|
|
||||||
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
|
|
||||||
required: false,
|
|
||||||
label_required: true,
|
|
||||||
description: String::new().to_string(),
|
|
||||||
include_in_snippet: false,
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
||||||
@ -124,6 +112,8 @@ impl crate::docs::StdLibFn for Import {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,23 +92,11 @@ impl crate::docs::StdLibFn for Import {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
settings.inline_subschemas = inline_subschemas;
|
settings.inline_subschemas = inline_subschemas;
|
||||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
vec![crate::docs::StdLibFnArg {
|
vec![]
|
||||||
name: "args".to_string(),
|
|
||||||
type_: "kittycad::types::InputFormat".to_string(),
|
|
||||||
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
|
|
||||||
required: false,
|
|
||||||
label_required: true,
|
|
||||||
description: String::new().to_string(),
|
|
||||||
include_in_snippet: false,
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
||||||
@ -124,6 +112,8 @@ impl crate::docs::StdLibFn for Import {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,23 +92,11 @@ impl crate::docs::StdLibFn for Import {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
settings.inline_subschemas = inline_subschemas;
|
settings.inline_subschemas = inline_subschemas;
|
||||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
vec![crate::docs::StdLibFnArg {
|
vec![]
|
||||||
name: "args".to_string(),
|
|
||||||
type_: "kittycad::types::InputFormat".to_string(),
|
|
||||||
schema: generator.root_schema_for::<Option<kittycad::types::InputFormat>>(),
|
|
||||||
required: false,
|
|
||||||
label_required: true,
|
|
||||||
description: String::new().to_string(),
|
|
||||||
include_in_snippet: false,
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
||||||
@ -124,6 +112,8 @@ impl crate::docs::StdLibFn for Import {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,23 +92,11 @@ impl crate::docs::StdLibFn for Show {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
settings.inline_subschemas = inline_subschemas;
|
settings.inline_subschemas = inline_subschemas;
|
||||||
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
let mut generator = schemars::gen::SchemaGenerator::new(settings);
|
||||||
vec![crate::docs::StdLibFnArg {
|
vec![]
|
||||||
name: "args".to_string(),
|
|
||||||
type_: "[number]".to_string(),
|
|
||||||
schema: generator.root_schema_for::<Vec<f64>>(),
|
|
||||||
required: true,
|
|
||||||
label_required: true,
|
|
||||||
description: String::new().to_string(),
|
|
||||||
include_in_snippet: true,
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
fn return_value(&self, inline_subschemas: bool) -> Option<crate::docs::StdLibFnArg> {
|
||||||
@ -124,6 +112,8 @@ impl crate::docs::StdLibFn for Show {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,10 +91,6 @@ impl crate::docs::StdLibFn for SomeFunction {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keyword_arguments(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
fn args(&self, inline_subschemas: bool) -> Vec<crate::docs::StdLibFnArg> {
|
||||||
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
let mut settings = schemars::gen::SchemaSettings::openapi3();
|
||||||
settings.inline_subschemas = inline_subschemas;
|
settings.inline_subschemas = inline_subschemas;
|
||||||
@ -115,6 +111,8 @@ impl crate::docs::StdLibFn for SomeFunction {
|
|||||||
label_required: true,
|
label_required: true,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
include_in_snippet: true,
|
include_in_snippet: true,
|
||||||
|
snippet_value: None,
|
||||||
|
snippet_value_array: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-directory-test-macro"
|
name = "kcl-directory-test-macro"
|
||||||
description = "A tool for generating tests from a directory of kcl files"
|
description = "A tool for generating tests from a directory of kcl files"
|
||||||
version = "0.1.74"
|
version = "0.1.76"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kcl-language-server-release"
|
name = "kcl-language-server-release"
|
||||||
version = "0.1.74"
|
version = "0.1.76"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
||||||
publish = false
|
publish = false
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
name = "kcl-language-server"
|
name = "kcl-language-server"
|
||||||
description = "A language server for KCL."
|
description = "A language server for KCL."
|
||||||
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
||||||
version = "0.2.74"
|
version = "0.2.76"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
@ -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.74"
|
version = "0.2.76"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -2,7 +2,7 @@ mod cache;
|
|||||||
|
|
||||||
use kcl_lib::{
|
use kcl_lib::{
|
||||||
test_server::{execute_and_export_step, execute_and_snapshot, execute_and_snapshot_no_auth},
|
test_server::{execute_and_export_step, execute_and_snapshot, execute_and_snapshot_no_auth},
|
||||||
ExecError,
|
BacktraceItem, ExecError, ModuleId, SourceRange,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The minimum permissible difference between asserted twenty-twenty images.
|
/// The minimum permissible difference between asserted twenty-twenty images.
|
||||||
@ -441,10 +441,15 @@ async fn kcl_test_import_file_doesnt_exist() {
|
|||||||
model = cube"#;
|
model = cube"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "File `thing.obj` does not exist.");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([0, 18, 0])], message: "File `thing.obj` does not exist." }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(0, 18, ModuleId::default()),
|
||||||
|
fn_name: None,
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,10 +524,18 @@ import 'e2e/executor/inputs/cube.gltf'
|
|||||||
model = cube"#;
|
model = cube"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.message(),
|
||||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([32, 70, 0])], message: "The given format does not match the file extension. Expected: `gltf`, Given: `obj`" }"#
|
"The given format does not match the file extension. Expected: `gltf`, Given: `obj`"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
err.backtrace(),
|
||||||
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(32, 70, ModuleId::default()),
|
||||||
|
fn_name: None,
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1666,10 +1679,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have an x constrained angle of 90 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 111, 0])], message: "Cannot have an x constrained angle of 90 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(70, 111, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1686,10 +1704,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have an x constrained angle of 270 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 112, 0])], message: "Cannot have an x constrained angle of 270 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(70, 112, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1706,10 +1729,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have a y constrained angle of 0 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 110, 0])], message: "Cannot have a y constrained angle of 0 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(70, 110, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1726,10 +1754,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 112, 0])], message: "Cannot have a y constrained angle of 180 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(70, 112, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1746,10 +1779,15 @@ extrusion = extrude(sketch001, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have an x constrained angle of 90 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([66, 116, 0])], message: "Cannot have an x constrained angle of 90 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(66, 116, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1757,7 +1795,7 @@ extrusion = extrude(sketch001, length = 10)
|
|||||||
async fn kcl_test_angled_line_of_x_length_270() {
|
async fn kcl_test_angled_line_of_x_length_270() {
|
||||||
let code = r#"sketch001 = startSketchOn(XZ)
|
let code = r#"sketch001 = startSketchOn(XZ)
|
||||||
|> startProfile(at = [0, 0])
|
|> startProfile(at = [0, 0])
|
||||||
|> angledLine(angle = 90, lengthX = 90, tag = $edge1)
|
|> angledLine(angle = 270, lengthX = 90, tag = $edge1)
|
||||||
|> angledLine(angle = -15, lengthX = -15, tag = $edge2)
|
|> angledLine(angle = -15, lengthX = -15, tag = $edge2)
|
||||||
|> line(end = [0, -5])
|
|> line(end = [0, -5])
|
||||||
|> close(tag = $edge3)
|
|> close(tag = $edge3)
|
||||||
@ -1766,10 +1804,15 @@ extrusion = extrude(sketch001, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have an x constrained angle of 270 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([66, 116, 0])], message: "Cannot have an x constrained angle of 90 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(66, 117, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1788,10 +1831,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have a y constrained angle of 0 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([95, 130, 0])], message: "Cannot have a y constrained angle of 0 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(95, 130, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1810,10 +1858,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([95, 132, 0])], message: "Cannot have a y constrained angle of 180 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(95, 132, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1832,10 +1885,15 @@ example = extrude(exampleSketch, length = 10)
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
|
assert_eq!(err.message(), "Cannot have a y constrained angle of 180 degrees");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.backtrace(),
|
||||||
r#"type: KclErrorDetails { source_ranges: [SourceRange([95, 133, 0])], message: "Cannot have a y constrained angle of 180 degrees" }"#
|
vec![BacktraceItem {
|
||||||
|
source_range: SourceRange::new(95, 133, ModuleId::default()),
|
||||||
|
fn_name: Some("angledLine".to_owned())
|
||||||
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1849,10 +1907,31 @@ someFunction('INVALID')
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot(code, None).await;
|
let result = execute_and_snapshot(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
|
let err = err.as_kcl_error().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.err().unwrap().to_string(),
|
err.message(),
|
||||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([46, 55, 0]), SourceRange([60, 83, 0])], message: "This function expected the input argument to be Solid or Plane but it's actually of type string" }"#
|
"This function expected the input argument to be Solid or Plane but it's actually of type string"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
err.source_ranges(),
|
||||||
|
vec![
|
||||||
|
SourceRange::new(46, 55, ModuleId::default()),
|
||||||
|
SourceRange::new(60, 83, ModuleId::default()),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
err.backtrace(),
|
||||||
|
vec![
|
||||||
|
BacktraceItem {
|
||||||
|
source_range: SourceRange::new(46, 55, ModuleId::default()),
|
||||||
|
fn_name: Some("someFunction".to_owned()),
|
||||||
|
},
|
||||||
|
BacktraceItem {
|
||||||
|
source_range: SourceRange::new(60, 83, ModuleId::default()),
|
||||||
|
fn_name: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1873,12 +1952,14 @@ async fn kcl_test_error_no_auth_websocket() {
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = execute_and_snapshot_no_auth(code, None).await;
|
let result = execute_and_snapshot_no_auth(code, None).await;
|
||||||
assert!(result.is_err());
|
let err = result.unwrap_err();
|
||||||
assert!(result
|
let err = err.as_kcl_error().unwrap();
|
||||||
.err()
|
assert!(
|
||||||
.unwrap()
|
err.message()
|
||||||
.to_string()
|
.contains("Please send the following object over this websocket"),
|
||||||
.contains("Please send the following object over this websocket"));
|
"actual: {}",
|
||||||
|
err.message()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@ -10,7 +10,9 @@ use tower_lsp::lsp_types::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
execution::annotations,
|
execution::annotations,
|
||||||
parsing::{
|
parsing::{
|
||||||
ast::types::{Annotation, ImportSelector, ItemVisibility, Node, NonCodeValue, VariableKind},
|
ast::types::{
|
||||||
|
Annotation, Expr, ImportSelector, ItemVisibility, LiteralValue, Node, NonCodeValue, VariableKind,
|
||||||
|
},
|
||||||
token::NumericSuffix,
|
token::NumericSuffix,
|
||||||
},
|
},
|
||||||
ModuleId,
|
ModuleId,
|
||||||
@ -293,17 +295,6 @@ impl DocData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn examples(&self) -> impl Iterator<Item = &String> {
|
|
||||||
match self {
|
|
||||||
DocData::Fn(f) => f.examples.iter(),
|
|
||||||
DocData::Const(c) => c.examples.iter(),
|
|
||||||
DocData::Ty(t) => t.examples.iter(),
|
|
||||||
DocData::Mod(_) => unimplemented!(),
|
|
||||||
}
|
|
||||||
.filter_map(|(s, p)| (!p.norun).then_some(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expect_mod(&self) -> &ModData {
|
fn expect_mod(&self) -> &ModData {
|
||||||
match self {
|
match self {
|
||||||
DocData::Mod(m) => m,
|
DocData::Mod(m) => m,
|
||||||
@ -694,6 +685,8 @@ pub struct ArgData {
|
|||||||
/// This is helpful if the type is really basic, like "number" -- that won't tell the user much about
|
/// This is helpful if the type is really basic, like "number" -- that won't tell the user much about
|
||||||
/// how this argument is meant to be used.
|
/// how this argument is meant to be used.
|
||||||
pub docs: Option<String>,
|
pub docs: Option<String>,
|
||||||
|
/// If given, LSP should use these as completion items.
|
||||||
|
pub snippet_array: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ArgData {
|
impl fmt::Display for ArgData {
|
||||||
@ -721,6 +714,7 @@ pub enum ArgKind {
|
|||||||
impl ArgData {
|
impl ArgData {
|
||||||
fn from_ast(arg: &crate::parsing::ast::types::Parameter) -> Self {
|
fn from_ast(arg: &crate::parsing::ast::types::Parameter) -> Self {
|
||||||
let mut result = ArgData {
|
let mut result = ArgData {
|
||||||
|
snippet_array: Default::default(),
|
||||||
name: arg.identifier.name.clone(),
|
name: arg.identifier.name.clone(),
|
||||||
ty: arg.type_.as_ref().map(|t| t.to_string()),
|
ty: arg.type_.as_ref().map(|t| t.to_string()),
|
||||||
docs: None,
|
docs: None,
|
||||||
@ -749,6 +743,30 @@ impl ArgData {
|
|||||||
p.value
|
p.value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if p.key.name == "snippetArray" {
|
||||||
|
let Expr::ArrayExpression(arr) = &p.value else {
|
||||||
|
panic!(
|
||||||
|
"Invalid value for `snippetArray`, expected array literal, found {:?}",
|
||||||
|
p.value
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let mut items = Vec::new();
|
||||||
|
for s in &arr.elements {
|
||||||
|
let Expr::Literal(lit) = s else {
|
||||||
|
panic!(
|
||||||
|
"Invalid value in `snippetArray`, all items must be string literals but found {:?}",
|
||||||
|
s
|
||||||
|
);
|
||||||
|
};
|
||||||
|
let LiteralValue::String(litstr) = &lit.inner.value else {
|
||||||
|
panic!(
|
||||||
|
"Invalid value in `snippetArray`, all items must be string literals but found {:?}",
|
||||||
|
s
|
||||||
|
);
|
||||||
|
};
|
||||||
|
items.push(litstr.to_owned());
|
||||||
|
}
|
||||||
|
result.snippet_array = Some(items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -770,6 +788,19 @@ impl ArgData {
|
|||||||
} else {
|
} else {
|
||||||
format!("{} = ", self.name)
|
format!("{} = ", self.name)
|
||||||
};
|
};
|
||||||
|
if let Some(vals) = &self.snippet_array {
|
||||||
|
let mut snippet = label.to_owned();
|
||||||
|
snippet.push('[');
|
||||||
|
let n = vals.len();
|
||||||
|
for (i, val) in vals.iter().enumerate() {
|
||||||
|
snippet.push_str(&format!("${{{}:{}}}", index + i, val));
|
||||||
|
if i != n - 1 {
|
||||||
|
snippet.push_str(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snippet.push(']');
|
||||||
|
return Some((index + n - 1, snippet));
|
||||||
|
}
|
||||||
match self.ty.as_deref() {
|
match self.ty.as_deref() {
|
||||||
Some(s) if s.starts_with("number") => Some((index, format!(r#"{label}${{{}:3.14}}"#, index))),
|
Some(s) if s.starts_with("number") => Some((index, format!(r#"{label}${{{}:3.14}}"#, index))),
|
||||||
Some("Point2d") => Some((
|
Some("Point2d") => Some((
|
||||||
@ -1185,7 +1216,7 @@ impl ApplyMeta for ArgData {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use kcl_derive_docs::for_each_std_mod;
|
use kcl_derive_docs::{for_all_example_test, for_each_example_test};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@ -1223,51 +1254,81 @@ mod test {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[for_each_std_mod]
|
#[for_all_example_test]
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn missing_test_examples() {
|
||||||
|
fn check_mod(m: &ModData) {
|
||||||
|
for d in m.children.values() {
|
||||||
|
let DocData::Fn(f) = d else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in 0..f.examples.len() {
|
||||||
|
let name = format!("{}-{i}", f.qual_name.replace("::", "-"));
|
||||||
|
assert!(TEST_NAMES.contains(&&*name), "Missing test for example \"{name}\", maybe need to update kcl-derive-docs/src/example_tests.rs?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = walk_prelude();
|
||||||
|
|
||||||
|
check_mod(&data);
|
||||||
|
for m in data.children.values() {
|
||||||
|
if let DocData::Mod(m) = m {
|
||||||
|
check_mod(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[for_each_example_test]
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn kcl_test_examples() {
|
async fn kcl_test_examples() {
|
||||||
let std = walk_prelude();
|
let std = walk_prelude();
|
||||||
let mut errs = Vec::new();
|
|
||||||
|
|
||||||
let data = if STD_MOD_NAME == "prelude" {
|
let names = NAME.split('-');
|
||||||
|
let mut mods: Vec<_> = names.collect();
|
||||||
|
let number = mods.pop().unwrap();
|
||||||
|
let number: usize = number.parse().unwrap();
|
||||||
|
let name = mods.pop().unwrap();
|
||||||
|
let mut qualname = mods.join("::");
|
||||||
|
qualname.push_str("::");
|
||||||
|
qualname.push_str(name);
|
||||||
|
|
||||||
|
let data = if mods.len() == 1 {
|
||||||
&std
|
&std
|
||||||
} else {
|
} else {
|
||||||
std.children
|
std.children.get(&format!("M:std::{}", mods[1])).unwrap().expect_mod()
|
||||||
.get(&format!("M:std::{STD_MOD_NAME}"))
|
|
||||||
.unwrap()
|
|
||||||
.expect_mod()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut count = 0;
|
let Some(DocData::Fn(d)) = data.children.get(&format!("I:{qualname}")) else {
|
||||||
for d in data.children.values() {
|
panic!("Could not find data for {NAME} (missing a child entry for {qualname}), maybe need to update kcl-derive-docs/src/example_tests.rs?");
|
||||||
if let DocData::Mod(_) = d {
|
};
|
||||||
|
|
||||||
|
for (i, eg) in d.examples.iter().enumerate() {
|
||||||
|
if i != number {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let result = match crate::test_server::execute_and_snapshot(&eg.0, None).await {
|
||||||
for (i, eg) in d.examples().enumerate() {
|
Err(crate::errors::ExecError::Kcl(e)) => {
|
||||||
count += 1;
|
panic!("Error testing example {}{i}: {}", d.name, e.error.message());
|
||||||
if count % SHARD_COUNT != SHARD {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
Err(other_err) => panic!("{}", other_err),
|
||||||
let result = match crate::test_server::execute_and_snapshot(eg, None).await {
|
Ok(img) => img,
|
||||||
Err(crate::errors::ExecError::Kcl(e)) => {
|
};
|
||||||
errs.push(format!("Error testing example {}{i}: {}", d.name(), e.error.message()));
|
if eg.1.norun {
|
||||||
continue;
|
return;
|
||||||
}
|
|
||||||
Err(other_err) => panic!("{}", other_err),
|
|
||||||
Ok(img) => img,
|
|
||||||
};
|
|
||||||
twenty_twenty::assert_image(
|
|
||||||
format!("tests/outputs/serial_test_example_{}{i}.png", d.example_name()),
|
|
||||||
&result,
|
|
||||||
0.99,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
twenty_twenty::assert_image(
|
||||||
|
format!(
|
||||||
|
"tests/outputs/serial_test_example_fn_{}{i}.png",
|
||||||
|
qualname.replace("::", "-")
|
||||||
|
),
|
||||||
|
&result,
|
||||||
|
0.99,
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errs.is_empty() {
|
panic!("Could not find data for {NAME} (no example {number}), maybe need to update kcl-derive-docs/src/example_tests.rs?");
|
||||||
panic!("{}", errs.join("\n\n"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,8 +78,6 @@ pub struct StdLibFnData {
|
|||||||
pub description: String,
|
pub description: String,
|
||||||
/// The tags of the function.
|
/// The tags of the function.
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
/// If this function uses keyword arguments, or positional arguments.
|
|
||||||
pub keyword_arguments: bool,
|
|
||||||
/// The args of the function.
|
/// The args of the function.
|
||||||
pub args: Vec<StdLibFnArg>,
|
pub args: Vec<StdLibFnArg>,
|
||||||
/// The return value of the function.
|
/// The return value of the function.
|
||||||
@ -111,6 +109,13 @@ pub struct StdLibFnArg {
|
|||||||
/// Include this in completion snippets?
|
/// Include this in completion snippets?
|
||||||
#[serde(default, skip_serializing_if = "is_false")]
|
#[serde(default, skip_serializing_if = "is_false")]
|
||||||
pub include_in_snippet: bool,
|
pub include_in_snippet: bool,
|
||||||
|
/// Snippet should suggest this value for the argument.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub snippet_value: Option<String>,
|
||||||
|
/// Snippet should suggest this value for the argument.
|
||||||
|
/// The suggested value should be an array, with these elements.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub snippet_value_array: Option<Vec<String>>,
|
||||||
/// Additional information that could be used instead of the type's description.
|
/// Additional information that could be used instead of the type's description.
|
||||||
/// This is helpful if the type is really basic, like "u32" -- that won't tell the user much about
|
/// This is helpful if the type is really basic, like "u32" -- that won't tell the user much about
|
||||||
/// how this argument is meant to be used.
|
/// how this argument is meant to be used.
|
||||||
@ -165,6 +170,21 @@ impl StdLibFnArg {
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
|
if let Some(vals) = &self.snippet_value_array {
|
||||||
|
let mut snippet = label.to_owned();
|
||||||
|
snippet.push('[');
|
||||||
|
for (i, val) in vals.iter().enumerate() {
|
||||||
|
snippet.push_str(&format!("${{{}:{}}}", index + i, val));
|
||||||
|
if i != vals.len() - 1 {
|
||||||
|
snippet.push_str(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snippet.push(']');
|
||||||
|
return Ok(Some((index + vals.len() - 1, snippet)));
|
||||||
|
}
|
||||||
|
if let Some(val) = &self.snippet_value {
|
||||||
|
return Ok(Some((index, format!("{label}${{{}:{}}}", index, val))));
|
||||||
|
}
|
||||||
if (self.type_ == "Sketch"
|
if (self.type_ == "Sketch"
|
||||||
|| self.type_ == "[Sketch]"
|
|| self.type_ == "[Sketch]"
|
||||||
|| self.type_ == "Geometry"
|
|| self.type_ == "Geometry"
|
||||||
@ -450,9 +470,6 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
|
|||||||
/// The description of the function.
|
/// The description of the function.
|
||||||
fn description(&self) -> String;
|
fn description(&self) -> String;
|
||||||
|
|
||||||
/// Does this use keyword arguments, or positional?
|
|
||||||
fn keyword_arguments(&self) -> bool;
|
|
||||||
|
|
||||||
/// The tags of the function.
|
/// The tags of the function.
|
||||||
fn tags(&self) -> Vec<String>;
|
fn tags(&self) -> Vec<String>;
|
||||||
|
|
||||||
@ -487,7 +504,6 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
|
|||||||
summary: self.summary(),
|
summary: self.summary(),
|
||||||
description: self.description(),
|
description: self.description(),
|
||||||
tags: self.tags(),
|
tags: self.tags(),
|
||||||
keyword_arguments: self.keyword_arguments(),
|
|
||||||
args: self.args(false),
|
args: self.args(false),
|
||||||
return_value: self.return_value(false),
|
return_value: self.return_value(false),
|
||||||
unpublished: self.unpublished(),
|
unpublished: self.unpublished(),
|
||||||
@ -571,7 +587,7 @@ pub trait StdLibFn: std::fmt::Debug + Send + Sync {
|
|||||||
} else if self.name() == "subtract2D" {
|
} else if self.name() == "subtract2D" {
|
||||||
return Ok("subtract2d(${0:%}, tool = ${1:%})".to_string());
|
return Ok("subtract2d(${0:%}, tool = ${1:%})".to_string());
|
||||||
}
|
}
|
||||||
let in_keyword_fn = self.keyword_arguments();
|
let in_keyword_fn = true;
|
||||||
let mut args = Vec::new();
|
let mut args = Vec::new();
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
for arg in self.args(true).iter() {
|
for arg in self.args(true).iter() {
|
||||||
@ -988,6 +1004,13 @@ mod tests {
|
|||||||
assert_eq!(snippet, r#"startSketchOn(${0:XY})"#);
|
assert_eq!(snippet, r#"startSketchOn(${0:XY})"#);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_autocomplete_snippet_start_profile() {
|
||||||
|
let start_sketch_on_fn: Box<dyn StdLibFn> = Box::new(crate::std::sketch::StartProfile);
|
||||||
|
let snippet = start_sketch_on_fn.to_autocomplete_snippet().unwrap();
|
||||||
|
assert_eq!(snippet, r#"startProfile(${0:%}, at = [${1:0}, ${2:0}])"#);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn get_autocomplete_snippet_pattern_circular_3d() {
|
fn get_autocomplete_snippet_pattern_circular_3d() {
|
||||||
// We test this one specifically because it has ints and floats and strings.
|
// We test this one specifically because it has ints and floats and strings.
|
||||||
@ -995,7 +1018,7 @@ mod tests {
|
|||||||
let snippet = pattern_fn.to_autocomplete_snippet().unwrap();
|
let snippet = pattern_fn.to_autocomplete_snippet().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snippet,
|
snippet,
|
||||||
r#"patternCircular3d(${0:%}, instances = ${1:10}, axis = [${2:3.14}, ${3:3.14}, ${4:3.14}], center = [${5:3.14}, ${6:3.14}, ${7:3.14}])"#
|
r#"patternCircular3d(${0:%}, instances = ${1:10}, axis = [${2:1}, ${3:0}, ${4:0}], center = [${5:0}, ${6:0}, ${7:0}])"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1017,8 +1040,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let snippet = circle_fn.to_autocomplete_snippet();
|
let snippet = circle_fn.to_autocomplete_snippet();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snippet,
|
snippet, r#"circle(center = [${0:0}, ${1:0}], diameter = ${2:3.14})"#,
|
||||||
r#"circle(center = [${0:3.14}, ${1:3.14}], radius = ${2:3.14})"#
|
"actual = left, expected = right"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1048,7 +1071,7 @@ mod tests {
|
|||||||
let snippet = pattern_fn.to_autocomplete_snippet().unwrap();
|
let snippet = pattern_fn.to_autocomplete_snippet().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
snippet,
|
snippet,
|
||||||
r#"patternLinear2d(${0:%}, instances = ${1:10}, distance = ${2:3.14}, axis = [${3:3.14}, ${4:3.14}])"#
|
r#"patternLinear2d(${0:%}, instances = ${1:10}, distance = ${2:3.14}, axis = [${3:1}, ${4:0}])"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -439,12 +439,7 @@ impl EngineManager for EngineConnection {
|
|||||||
request_sent: tx,
|
request_sent: tx,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| KclError::Engine(KclErrorDetails::new(format!("Failed to send debug: {}", e), vec![])))?;
|
||||||
KclError::Engine(KclErrorDetails {
|
|
||||||
message: format!("Failed to send debug: {}", e),
|
|
||||||
source_ranges: vec![],
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let _ = rx.await;
|
let _ = rx.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -479,25 +474,25 @@ impl EngineManager for EngineConnection {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to send modeling command: {}", e),
|
format!("Failed to send modeling command: {}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Wait for the request to be sent.
|
// Wait for the request to be sent.
|
||||||
rx.await
|
rx.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("could not send request to the engine actor: {e}"),
|
format!("could not send request to the engine actor: {e}"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?
|
})?
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("could not send request to the engine: {e}"),
|
format!("could not send request to the engine: {e}"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -521,15 +516,15 @@ impl EngineManager for EngineConnection {
|
|||||||
// Check if we have any pending errors.
|
// Check if we have any pending errors.
|
||||||
let pe = self.pending_errors.read().await;
|
let pe = self.pending_errors.read().await;
|
||||||
if !pe.is_empty() {
|
if !pe.is_empty() {
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: pe.join(", ").to_string(),
|
pe.join(", ").to_string(),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
} else {
|
} else {
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: "Modeling command failed: websocket closed early".to_string(),
|
"Modeling command failed: websocket closed early".to_string(),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -548,10 +543,10 @@ impl EngineManager for EngineConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(KclError::Engine(KclErrorDetails {
|
Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Modeling command timed out `{}`", id),
|
format!("Modeling command timed out `{}`", id),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_session_data(&self) -> Option<ModelingSessionData> {
|
async fn get_session_data(&self) -> Option<ModelingSessionData> {
|
||||||
|
@ -147,32 +147,27 @@ impl EngineConnection {
|
|||||||
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
|
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
|
||||||
) -> Result<(), KclError> {
|
) -> Result<(), KclError> {
|
||||||
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
|
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to serialize source range: {:?}", e),
|
format!("Failed to serialize source range: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
|
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to serialize modeling command: {:?}", e),
|
format!("Failed to serialize modeling command: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
|
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to serialize id to source range: {:?}", e),
|
format!("Failed to serialize id to source range: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
self.manager
|
self.manager
|
||||||
.fire_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
|
.fire_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
|
||||||
.map_err(|e| {
|
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
|
||||||
KclError::Engine(KclErrorDetails {
|
|
||||||
message: e.to_string().into(),
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -185,33 +180,28 @@ impl EngineConnection {
|
|||||||
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
|
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
|
||||||
) -> Result<WebSocketResponse, KclError> {
|
) -> Result<WebSocketResponse, KclError> {
|
||||||
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
|
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to serialize source range: {:?}", e),
|
format!("Failed to serialize source range: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
|
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to serialize modeling command: {:?}", e),
|
format!("Failed to serialize modeling command: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
|
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to serialize id to source range: {:?}", e),
|
format!("Failed to serialize id to source range: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let promise = self
|
let promise = self
|
||||||
.manager
|
.manager
|
||||||
.send_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
|
.send_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
|
||||||
.map_err(|e| {
|
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
|
||||||
KclError::Engine(KclErrorDetails {
|
|
||||||
message: e.to_string().into(),
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| {
|
let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| {
|
||||||
// Try to parse the error as an engine error.
|
// Try to parse the error as an engine error.
|
||||||
@ -219,53 +209,52 @@ impl EngineConnection {
|
|||||||
if let Ok(kittycad_modeling_cmds::websocket::FailureWebSocketResponse { errors, .. }) =
|
if let Ok(kittycad_modeling_cmds::websocket::FailureWebSocketResponse { errors, .. }) =
|
||||||
serde_json::from_str(&err_str)
|
serde_json::from_str(&err_str)
|
||||||
{
|
{
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
|
errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
} else if let Ok(data) =
|
} else if let Ok(data) =
|
||||||
serde_json::from_str::<Vec<kittycad_modeling_cmds::websocket::FailureWebSocketResponse>>(&err_str)
|
serde_json::from_str::<Vec<kittycad_modeling_cmds::websocket::FailureWebSocketResponse>>(&err_str)
|
||||||
{
|
{
|
||||||
if let Some(data) = data.first() {
|
if let Some(data) = data.first() {
|
||||||
// It could also be an array of responses.
|
// It could also be an array of responses.
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: data
|
data.errors
|
||||||
.errors
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| e.message.clone())
|
.map(|e| e.message.clone())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
} else {
|
} else {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: "Received empty response from engine".into(),
|
"Received empty response from engine".into(),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to wait for promise from send modeling command: {:?}", e),
|
format!("Failed to wait for promise from send modeling command: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if value.is_null() || value.is_undefined() {
|
if value.is_null() || value.is_undefined() {
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: "Received null or undefined response from engine".into(),
|
"Received null or undefined response from engine".into(),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert JsValue to a Uint8Array
|
// Convert JsValue to a Uint8Array
|
||||||
let data = js_sys::Uint8Array::from(value);
|
let data = js_sys::Uint8Array::from(value);
|
||||||
|
|
||||||
let ws_result: WebSocketResponse = bson::from_slice(&data.to_vec()).map_err(|e| {
|
let ws_result: WebSocketResponse = bson::from_slice(&data.to_vec()).map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to deserialize bson response from engine: {:?}", e),
|
format!("Failed to deserialize bson response from engine: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(ws_result)
|
Ok(ws_result)
|
||||||
@ -316,18 +305,16 @@ impl crate::engine::EngineManager for EngineConnection {
|
|||||||
*self.default_planes.write().await = Some(new_planes);
|
*self.default_planes.write().await = Some(new_planes);
|
||||||
|
|
||||||
// Start a new session.
|
// Start a new session.
|
||||||
let promise = self.manager.start_new_session().map_err(|e| {
|
let promise = self
|
||||||
KclError::Engine(KclErrorDetails {
|
.manager
|
||||||
message: e.to_string().into(),
|
.start_new_session()
|
||||||
source_ranges: vec![source_range],
|
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
crate::wasm::JsFuture::from(promise).await.map_err(|e| {
|
crate::wasm::JsFuture::from(promise).await.map_err(|e| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to wait for promise from start new session: {:?}", e),
|
format!("Failed to wait for promise from start new session: {:?}", e),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -276,10 +276,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
{
|
{
|
||||||
let duration = instant::Duration::from_millis(1);
|
let duration = instant::Duration::from_millis(1);
|
||||||
wasm_timer::Delay::new(duration).await.map_err(|err| {
|
wasm_timer::Delay::new(duration).await.map_err(|err| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("Failed to sleep: {:?}", err),
|
format!("Failed to sleep: {:?}", err),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@ -293,10 +293,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
return Ok(response);
|
return Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(KclError::Engine(KclErrorDetails {
|
Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: "async command timed out".to_string(),
|
"async command timed out".to_string(),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure ALL async commands have been completed.
|
/// Ensure ALL async commands have been completed.
|
||||||
@ -547,10 +547,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
id_to_source_range.insert(Uuid::from(*cmd_id), *range);
|
id_to_source_range.insert(Uuid::from(*cmd_id), *range);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("The request is not a modeling command: {:?}", req),
|
format!("The request is not a modeling command: {:?}", req),
|
||||||
source_ranges: vec![*range],
|
vec![*range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -595,10 +595,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
|
self.parse_batch_responses(last_id.into(), id_to_source_range, responses)
|
||||||
} else {
|
} else {
|
||||||
// We should never get here.
|
// We should never get here.
|
||||||
Err(KclError::Engine(KclErrorDetails {
|
Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to get batch response: {:?}", response),
|
format!("Failed to get batch response: {:?}", response),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
|
WebSocketRequest::ModelingCmdReq(ModelingCmdReq { cmd: _, cmd_id }) => {
|
||||||
@ -610,20 +610,20 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
// request so we need the original request source range in case the engine returns
|
// request so we need the original request source range in case the engine returns
|
||||||
// an error.
|
// an error.
|
||||||
let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
|
let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to get source range for command ID: {:?}", cmd_id),
|
format!("Failed to get source range for command ID: {:?}", cmd_id),
|
||||||
source_ranges: vec![],
|
vec![],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let ws_resp = self
|
let ws_resp = self
|
||||||
.inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
|
.inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
|
||||||
.await?;
|
.await?;
|
||||||
self.parse_websocket_response(ws_resp, source_range)
|
self.parse_websocket_response(ws_resp, source_range)
|
||||||
}
|
}
|
||||||
_ => Err(KclError::Engine(KclErrorDetails {
|
_ => Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("The final request is not a modeling command: {:?}", final_req),
|
format!("The final request is not a modeling command: {:?}", final_req),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -729,10 +729,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
for (name, plane_id, color) in plane_settings {
|
for (name, plane_id, color) in plane_settings {
|
||||||
let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
|
let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
|
||||||
// We should never get here.
|
// We should never get here.
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to get default plane info for: {:?}", name),
|
format!("Failed to get default plane info for: {:?}", name),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
planes.insert(
|
planes.insert(
|
||||||
name,
|
name,
|
||||||
@ -763,15 +763,14 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
WebSocketResponse::Success(success) => Ok(success.resp),
|
WebSocketResponse::Success(success) => Ok(success.resp),
|
||||||
WebSocketResponse::Failure(fail) => {
|
WebSocketResponse::Failure(fail) => {
|
||||||
let _request_id = fail.request_id;
|
let _request_id = fail.request_id;
|
||||||
Err(KclError::Engine(KclErrorDetails {
|
Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: fail
|
fail.errors
|
||||||
.errors
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| e.message.clone())
|
.map(|e| e.message.clone())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -806,25 +805,25 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
|
|||||||
BatchResponse::Failure { errors } => {
|
BatchResponse::Failure { errors } => {
|
||||||
// Get the source range for the command.
|
// Get the source range for the command.
|
||||||
let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
|
let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
|
||||||
KclError::Engine(KclErrorDetails {
|
KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to get source range for command ID: {:?}", cmd_id),
|
format!("Failed to get source range for command ID: {:?}", cmd_id),
|
||||||
source_ranges: vec![],
|
vec![],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
return Err(KclError::Engine(KclErrorDetails {
|
return Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
|
errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return an error that we did not get an error or the response we wanted.
|
// Return an error that we did not get an error or the response we wanted.
|
||||||
// This should never happen but who knows.
|
// This should never happen but who knows.
|
||||||
Err(KclError::Engine(KclErrorDetails {
|
Err(KclError::Engine(KclErrorDetails::new(
|
||||||
message: format!("Failed to find response for command ID: {:?}", id),
|
format!("Failed to find response for command ID: {:?}", id),
|
||||||
source_ranges: vec![],
|
vec![],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn modify_grid(
|
async fn modify_grid(
|
||||||
|
@ -380,20 +380,39 @@ impl miette::Diagnostic for Report {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
|
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
#[error("{message}")]
|
#[error("{message}")]
|
||||||
#[ts(export)]
|
#[ts(export)]
|
||||||
pub struct KclErrorDetails {
|
pub struct KclErrorDetails {
|
||||||
#[serde(rename = "sourceRanges")]
|
|
||||||
#[label(collection, "Errors")]
|
#[label(collection, "Errors")]
|
||||||
pub source_ranges: Vec<SourceRange>,
|
pub source_ranges: Vec<SourceRange>,
|
||||||
|
pub backtrace: Vec<BacktraceItem>,
|
||||||
#[serde(rename = "msg")]
|
#[serde(rename = "msg")]
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl KclErrorDetails {
|
||||||
|
pub fn new(message: String, source_ranges: Vec<SourceRange>) -> KclErrorDetails {
|
||||||
|
let backtrace = source_ranges
|
||||||
|
.iter()
|
||||||
|
.map(|s| BacktraceItem {
|
||||||
|
source_range: *s,
|
||||||
|
fn_name: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
KclErrorDetails {
|
||||||
|
source_ranges,
|
||||||
|
backtrace,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl KclError {
|
impl KclError {
|
||||||
pub fn internal(message: String) -> KclError {
|
pub fn internal(message: String) -> KclError {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails {
|
||||||
source_ranges: Default::default(),
|
source_ranges: Default::default(),
|
||||||
|
backtrace: Default::default(),
|
||||||
message,
|
message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -455,45 +474,122 @@ impl KclError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn backtrace(&self) -> Vec<BacktraceItem> {
|
||||||
|
match self {
|
||||||
|
KclError::Lexical(e)
|
||||||
|
| KclError::Syntax(e)
|
||||||
|
| KclError::Semantic(e)
|
||||||
|
| KclError::ImportCycle(e)
|
||||||
|
| KclError::Type(e)
|
||||||
|
| KclError::Io(e)
|
||||||
|
| KclError::Unexpected(e)
|
||||||
|
| KclError::ValueAlreadyDefined(e)
|
||||||
|
| KclError::UndefinedValue(e)
|
||||||
|
| KclError::InvalidExpression(e)
|
||||||
|
| KclError::Engine(e)
|
||||||
|
| KclError::Internal(e) => e.backtrace.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
|
pub(crate) fn override_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
|
||||||
let mut new = self.clone();
|
let mut new = self.clone();
|
||||||
match &mut new {
|
match &mut new {
|
||||||
KclError::Lexical(e) => e.source_ranges = source_ranges,
|
KclError::Lexical(e)
|
||||||
KclError::Syntax(e) => e.source_ranges = source_ranges,
|
| KclError::Syntax(e)
|
||||||
KclError::Semantic(e) => e.source_ranges = source_ranges,
|
| KclError::Semantic(e)
|
||||||
KclError::ImportCycle(e) => e.source_ranges = source_ranges,
|
| KclError::ImportCycle(e)
|
||||||
KclError::Type(e) => e.source_ranges = source_ranges,
|
| KclError::Type(e)
|
||||||
KclError::Io(e) => e.source_ranges = source_ranges,
|
| KclError::Io(e)
|
||||||
KclError::Unexpected(e) => e.source_ranges = source_ranges,
|
| KclError::Unexpected(e)
|
||||||
KclError::ValueAlreadyDefined(e) => e.source_ranges = source_ranges,
|
| KclError::ValueAlreadyDefined(e)
|
||||||
KclError::UndefinedValue(e) => e.source_ranges = source_ranges,
|
| KclError::UndefinedValue(e)
|
||||||
KclError::InvalidExpression(e) => e.source_ranges = source_ranges,
|
| KclError::InvalidExpression(e)
|
||||||
KclError::Engine(e) => e.source_ranges = source_ranges,
|
| KclError::Engine(e)
|
||||||
KclError::Internal(e) => e.source_ranges = source_ranges,
|
| KclError::Internal(e) => {
|
||||||
|
e.backtrace = source_ranges
|
||||||
|
.iter()
|
||||||
|
.map(|s| BacktraceItem {
|
||||||
|
source_range: *s,
|
||||||
|
fn_name: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
e.source_ranges = source_ranges;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new
|
new
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_source_ranges(&self, source_ranges: Vec<SourceRange>) -> Self {
|
pub(crate) fn set_last_backtrace_fn_name(&self, last_fn_name: Option<String>) -> Self {
|
||||||
let mut new = self.clone();
|
let mut new = self.clone();
|
||||||
match &mut new {
|
match &mut new {
|
||||||
KclError::Lexical(e) => e.source_ranges.extend(source_ranges),
|
KclError::Lexical(e)
|
||||||
KclError::Syntax(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Syntax(e)
|
||||||
KclError::Semantic(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Semantic(e)
|
||||||
KclError::ImportCycle(e) => e.source_ranges.extend(source_ranges),
|
| KclError::ImportCycle(e)
|
||||||
KclError::Type(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Type(e)
|
||||||
KclError::Io(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Io(e)
|
||||||
KclError::Unexpected(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Unexpected(e)
|
||||||
KclError::ValueAlreadyDefined(e) => e.source_ranges.extend(source_ranges),
|
| KclError::ValueAlreadyDefined(e)
|
||||||
KclError::UndefinedValue(e) => e.source_ranges.extend(source_ranges),
|
| KclError::UndefinedValue(e)
|
||||||
KclError::InvalidExpression(e) => e.source_ranges.extend(source_ranges),
|
| KclError::InvalidExpression(e)
|
||||||
KclError::Engine(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Engine(e)
|
||||||
KclError::Internal(e) => e.source_ranges.extend(source_ranges),
|
| KclError::Internal(e) => {
|
||||||
|
if let Some(item) = e.backtrace.last_mut() {
|
||||||
|
item.fn_name = last_fn_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
new
|
new
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_unwind_location(&self, last_fn_name: Option<String>, source_range: SourceRange) -> Self {
|
||||||
|
let mut new = self.clone();
|
||||||
|
match &mut new {
|
||||||
|
KclError::Lexical(e)
|
||||||
|
| KclError::Syntax(e)
|
||||||
|
| KclError::Semantic(e)
|
||||||
|
| KclError::ImportCycle(e)
|
||||||
|
| KclError::Type(e)
|
||||||
|
| KclError::Io(e)
|
||||||
|
| KclError::Unexpected(e)
|
||||||
|
| KclError::ValueAlreadyDefined(e)
|
||||||
|
| KclError::UndefinedValue(e)
|
||||||
|
| KclError::InvalidExpression(e)
|
||||||
|
| KclError::Engine(e)
|
||||||
|
| KclError::Internal(e) => {
|
||||||
|
if let Some(item) = e.backtrace.last_mut() {
|
||||||
|
item.fn_name = last_fn_name;
|
||||||
|
}
|
||||||
|
e.backtrace.push(BacktraceItem {
|
||||||
|
source_range,
|
||||||
|
fn_name: None,
|
||||||
|
});
|
||||||
|
e.source_ranges.push(source_range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS, thiserror::Error, miette::Diagnostic)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export)]
|
||||||
|
pub struct BacktraceItem {
|
||||||
|
pub source_range: SourceRange,
|
||||||
|
pub fn_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for BacktraceItem {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(fn_name) = &self.fn_name {
|
||||||
|
write!(f, "{fn_name}: {:?}", self.source_range)
|
||||||
|
} else {
|
||||||
|
write!(f, "(fn): {:?}", self.source_range)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoDiagnostic for KclError {
|
impl IntoDiagnostic for KclError {
|
||||||
@ -551,6 +647,7 @@ impl From<pyo3::PyErr> for KclError {
|
|||||||
fn from(error: pyo3::PyErr) -> Self {
|
fn from(error: pyo3::PyErr) -> Self {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails {
|
||||||
source_ranges: vec![],
|
source_ranges: vec![],
|
||||||
|
backtrace: Default::default(),
|
||||||
message: error.to_string(),
|
message: error.to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -629,8 +726,13 @@ impl CompilationError {
|
|||||||
|
|
||||||
impl From<CompilationError> for KclErrorDetails {
|
impl From<CompilationError> for KclErrorDetails {
|
||||||
fn from(err: CompilationError) -> Self {
|
fn from(err: CompilationError) -> Self {
|
||||||
|
let backtrace = vec![BacktraceItem {
|
||||||
|
source_range: err.source_range,
|
||||||
|
fn_name: None,
|
||||||
|
}];
|
||||||
KclErrorDetails {
|
KclErrorDetails {
|
||||||
source_ranges: vec![err.source_range],
|
source_ranges: vec![err.source_range],
|
||||||
|
backtrace,
|
||||||
message: err.message,
|
message: err.message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,10 +70,10 @@ pub(super) fn expect_properties<'a>(
|
|||||||
) -> Result<&'a [Node<ObjectProperty>], KclError> {
|
) -> Result<&'a [Node<ObjectProperty>], KclError> {
|
||||||
assert_eq!(annotation.name().unwrap(), for_key);
|
assert_eq!(annotation.name().unwrap(), for_key);
|
||||||
Ok(&**annotation.properties.as_ref().ok_or_else(|| {
|
Ok(&**annotation.properties.as_ref().ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Empty `{for_key}` annotation"),
|
format!("Empty `{for_key}` annotation"),
|
||||||
source_ranges: vec![annotation.as_source_range()],
|
vec![annotation.as_source_range()],
|
||||||
})
|
))
|
||||||
})?)
|
})?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,10 +84,10 @@ pub(super) fn expect_ident(expr: &Expr) -> Result<&str, KclError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
|
"Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
|
||||||
source_ranges: vec![expr.into()],
|
vec![expr.into()],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the unparsed number literal.
|
// Returns the unparsed number literal.
|
||||||
@ -98,10 +98,10 @@ pub(super) fn expect_number(expr: &Expr) -> Result<String, KclError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Unexpected settings value, expected a number, e.g., `1.0`".to_owned(),
|
"Unexpected settings value, expected a number, e.g., `1.0`".to_owned(),
|
||||||
source_ranges: vec![expr.into()],
|
vec![expr.into()],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn get_impl(annotations: &[Node<Annotation>], source_range: SourceRange) -> Result<Option<Impl>, KclError> {
|
pub(super) fn get_impl(annotations: &[Node<Annotation>], source_range: SourceRange) -> Result<Option<Impl>, KclError> {
|
||||||
@ -113,14 +113,14 @@ pub(super) fn get_impl(annotations: &[Node<Annotation>], source_range: SourceRan
|
|||||||
if &*p.key.name == IMPL {
|
if &*p.key.name == IMPL {
|
||||||
if let Some(s) = p.value.ident_name() {
|
if let Some(s) = p.value.ident_name() {
|
||||||
return Impl::from_str(s).map(Some).map_err(|_| {
|
return Impl::from_str(s).map(Some).map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Invalid value for {} attribute, expected one of: {}",
|
"Invalid value for {} attribute, expected one of: {}",
|
||||||
IMPL,
|
IMPL,
|
||||||
IMPL_VALUES.join(", ")
|
IMPL_VALUES.join(", ")
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,12 +139,12 @@ impl UnitLen {
|
|||||||
"inch" | "in" => Ok(UnitLen::Inches),
|
"inch" | "in" => Ok(UnitLen::Inches),
|
||||||
"ft" => Ok(UnitLen::Feet),
|
"ft" => Ok(UnitLen::Feet),
|
||||||
"yd" => Ok(UnitLen::Yards),
|
"yd" => Ok(UnitLen::Yards),
|
||||||
value => Err(KclError::Semantic(KclErrorDetails {
|
value => Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Unexpected value for length units: `{value}`; expected one of `mm`, `cm`, `m`, `in`, `ft`, `yd`"
|
"Unexpected value for length units: `{value}`; expected one of `mm`, `cm`, `m`, `in`, `ft`, `yd`"
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,10 +154,10 @@ impl UnitAngle {
|
|||||||
match s {
|
match s {
|
||||||
"deg" => Ok(UnitAngle::Degrees),
|
"deg" => Ok(UnitAngle::Degrees),
|
||||||
"rad" => Ok(UnitAngle::Radians),
|
"rad" => Ok(UnitAngle::Radians),
|
||||||
value => Err(KclError::Semantic(KclErrorDetails {
|
value => Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Unexpected value for angle units: `{value}`; expected one of `deg`, `rad`"),
|
format!("Unexpected value for angle units: `{value}`; expected one of `deg`, `rad`"),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,14 @@ use crate::{
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod mermaid_tests;
|
mod mermaid_tests;
|
||||||
|
|
||||||
|
macro_rules! internal_error {
|
||||||
|
($range:expr, $($rest:tt)*) => {{
|
||||||
|
let message = format!($($rest)*);
|
||||||
|
debug_assert!(false, "{}", &message);
|
||||||
|
return Err(KclError::Internal(KclErrorDetails::new(message, vec![$range])));
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
/// A command that may create or update artifacts on the TS side. Because
|
/// A command that may create or update artifacts on the TS side. Because
|
||||||
/// engine commands are batched, we don't have the response yet when these are
|
/// engine commands are batched, we don't have the response yet when these are
|
||||||
/// created.
|
/// created.
|
||||||
@ -941,12 +949,10 @@ fn artifacts_to_update(
|
|||||||
ModelingCmd::StartPath(_) => {
|
ModelingCmd::StartPath(_) => {
|
||||||
let mut return_arr = Vec::new();
|
let mut return_arr = Vec::new();
|
||||||
let current_plane_id = path_to_plane_id_map.get(&artifact_command.cmd_id).ok_or_else(|| {
|
let current_plane_id = path_to_plane_id_map.get(&artifact_command.cmd_id).ok_or_else(|| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!(
|
format!("Expected a current plane ID when processing StartPath command, but we have none: {id:?}"),
|
||||||
"Expected a current plane ID when processing StartPath command, but we have none: {id:?}"
|
vec![range],
|
||||||
),
|
))
|
||||||
source_ranges: vec![range],
|
|
||||||
})
|
|
||||||
})?;
|
})?;
|
||||||
return_arr.push(Artifact::Path(Path {
|
return_arr.push(Artifact::Path(Path {
|
||||||
id,
|
id,
|
||||||
@ -994,7 +1000,10 @@ fn artifacts_to_update(
|
|||||||
let path_id = ArtifactId::new(match cmd {
|
let path_id = ArtifactId::new(match cmd {
|
||||||
ModelingCmd::ClosePath(c) => c.path_id,
|
ModelingCmd::ClosePath(c) => c.path_id,
|
||||||
ModelingCmd::ExtendPath(e) => e.path.into(),
|
ModelingCmd::ExtendPath(e) => e.path.into(),
|
||||||
_ => unreachable!(),
|
_ => internal_error!(
|
||||||
|
range,
|
||||||
|
"Close or extend path command variant not handled: id={id:?}, cmd={cmd:?}"
|
||||||
|
),
|
||||||
});
|
});
|
||||||
let mut return_arr = Vec::new();
|
let mut return_arr = Vec::new();
|
||||||
return_arr.push(Artifact::Segment(Segment {
|
return_arr.push(Artifact::Segment(Segment {
|
||||||
@ -1025,6 +1034,69 @@ fn artifacts_to_update(
|
|||||||
}
|
}
|
||||||
return Ok(return_arr);
|
return Ok(return_arr);
|
||||||
}
|
}
|
||||||
|
ModelingCmd::EntityMirror(kcmc::EntityMirror {
|
||||||
|
ids: original_path_ids, ..
|
||||||
|
})
|
||||||
|
| ModelingCmd::EntityMirrorAcrossEdge(kcmc::EntityMirrorAcrossEdge {
|
||||||
|
ids: original_path_ids, ..
|
||||||
|
}) => {
|
||||||
|
let face_edge_infos = match response {
|
||||||
|
OkModelingCmdResponse::EntityMirror(resp) => &resp.entity_face_edge_ids,
|
||||||
|
OkModelingCmdResponse::EntityMirrorAcrossEdge(resp) => &resp.entity_face_edge_ids,
|
||||||
|
_ => internal_error!(
|
||||||
|
range,
|
||||||
|
"Mirror response variant not handled: id={id:?}, cmd={cmd:?}, response={response:?}"
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if original_path_ids.len() != face_edge_infos.len() {
|
||||||
|
internal_error!(range, "EntityMirror or EntityMirrorAcrossEdge response has different number face edge info than original mirrored paths: id={id:?}, cmd={cmd:?}, response={response:?}");
|
||||||
|
}
|
||||||
|
let mut return_arr = Vec::new();
|
||||||
|
for (face_edge_info, original_path_id) in face_edge_infos.iter().zip(original_path_ids) {
|
||||||
|
let original_path_id = ArtifactId::new(*original_path_id);
|
||||||
|
let path_id = ArtifactId::new(face_edge_info.object_id);
|
||||||
|
// The path may be an existing path that was extended or a new
|
||||||
|
// path.
|
||||||
|
let mut path = if let Some(Artifact::Path(path)) = artifacts.get(&path_id) {
|
||||||
|
// Existing path.
|
||||||
|
path.clone()
|
||||||
|
} else {
|
||||||
|
// It's a new path. We need the original path to get some
|
||||||
|
// of its info.
|
||||||
|
let Some(Artifact::Path(original_path)) = artifacts.get(&original_path_id) else {
|
||||||
|
// We couldn't find the original path. This is a bug.
|
||||||
|
internal_error!(range, "Couldn't find original path for mirror2d: original_path_id={original_path_id:?}, cmd={cmd:?}");
|
||||||
|
};
|
||||||
|
Path {
|
||||||
|
id: path_id,
|
||||||
|
plane_id: original_path.plane_id,
|
||||||
|
seg_ids: Vec::new(),
|
||||||
|
sweep_id: None,
|
||||||
|
solid2d_id: None,
|
||||||
|
code_ref: code_ref.clone(),
|
||||||
|
composite_solid_id: None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
face_edge_info.edges.iter().for_each(|edge_id| {
|
||||||
|
let edge_id = ArtifactId::new(*edge_id);
|
||||||
|
return_arr.push(Artifact::Segment(Segment {
|
||||||
|
id: edge_id,
|
||||||
|
path_id: path.id,
|
||||||
|
surface_id: None,
|
||||||
|
edge_ids: Vec::new(),
|
||||||
|
edge_cut_id: None,
|
||||||
|
code_ref: code_ref.clone(),
|
||||||
|
common_surface_ids: Vec::new(),
|
||||||
|
}));
|
||||||
|
// Add the edge ID to the path.
|
||||||
|
path.seg_ids.push(edge_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
return_arr.push(Artifact::Path(path));
|
||||||
|
}
|
||||||
|
return Ok(return_arr);
|
||||||
|
}
|
||||||
ModelingCmd::Extrude(kcmc::Extrude { target, .. })
|
ModelingCmd::Extrude(kcmc::Extrude { target, .. })
|
||||||
| ModelingCmd::Revolve(kcmc::Revolve { target, .. })
|
| ModelingCmd::Revolve(kcmc::Revolve { target, .. })
|
||||||
| ModelingCmd::RevolveAboutEdge(kcmc::RevolveAboutEdge { target, .. })
|
| ModelingCmd::RevolveAboutEdge(kcmc::RevolveAboutEdge { target, .. })
|
||||||
@ -1034,7 +1106,7 @@ fn artifacts_to_update(
|
|||||||
ModelingCmd::Revolve(_) => SweepSubType::Revolve,
|
ModelingCmd::Revolve(_) => SweepSubType::Revolve,
|
||||||
ModelingCmd::RevolveAboutEdge(_) => SweepSubType::RevolveAboutEdge,
|
ModelingCmd::RevolveAboutEdge(_) => SweepSubType::RevolveAboutEdge,
|
||||||
ModelingCmd::Sweep(_) => SweepSubType::Sweep,
|
ModelingCmd::Sweep(_) => SweepSubType::Sweep,
|
||||||
_ => unreachable!(),
|
_ => internal_error!(range, "Sweep-like command variant not handled: id={id:?}, cmd={cmd:?}",),
|
||||||
};
|
};
|
||||||
let mut return_arr = Vec::new();
|
let mut return_arr = Vec::new();
|
||||||
let target = ArtifactId::from(target);
|
let target = ArtifactId::from(target);
|
||||||
@ -1065,10 +1137,10 @@ fn artifacts_to_update(
|
|||||||
// TODO: Using the first one. Make sure to revisit this
|
// TODO: Using the first one. Make sure to revisit this
|
||||||
// choice, don't think it matters for now.
|
// choice, don't think it matters for now.
|
||||||
path_id: ArtifactId::new(*loft_cmd.section_ids.first().ok_or_else(|| {
|
path_id: ArtifactId::new(*loft_cmd.section_ids.first().ok_or_else(|| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"),
|
format!("Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"),
|
||||||
source_ranges: vec![range],
|
vec![range],
|
||||||
})
|
))
|
||||||
})?),
|
})?),
|
||||||
surface_ids: Vec::new(),
|
surface_ids: Vec::new(),
|
||||||
edge_ids: Vec::new(),
|
edge_ids: Vec::new(),
|
||||||
@ -1108,12 +1180,12 @@ fn artifacts_to_update(
|
|||||||
};
|
};
|
||||||
last_path = Some(path);
|
last_path = Some(path);
|
||||||
let path_sweep_id = path.sweep_id.ok_or_else(|| {
|
let path_sweep_id = path.sweep_id.ok_or_else(|| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message:format!(
|
format!(
|
||||||
"Expected a sweep ID on the path when processing Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
|
"Expected a sweep ID on the path when processing Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
|
||||||
),
|
),
|
||||||
source_ranges: vec![range],
|
vec![range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let extra_artifact = exec_artifacts.values().find(|a| {
|
let extra_artifact = exec_artifacts.values().find(|a| {
|
||||||
if let Artifact::StartSketchOnFace(s) = a {
|
if let Artifact::StartSketchOnFace(s) = a {
|
||||||
@ -1162,12 +1234,12 @@ fn artifacts_to_update(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let path_sweep_id = path.sweep_id.ok_or_else(|| {
|
let path_sweep_id = path.sweep_id.ok_or_else(|| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message:format!(
|
format!(
|
||||||
"Expected a sweep ID on the path when processing last path's Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
|
"Expected a sweep ID on the path when processing last path's Solid3dGetExtrusionFaceInfo command, but we have none: {id:?}, {path:?}"
|
||||||
),
|
),
|
||||||
source_ranges: vec![range],
|
vec![range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let extra_artifact = exec_artifacts.values().find(|a| {
|
let extra_artifact = exec_artifacts.values().find(|a| {
|
||||||
if let Artifact::StartSketchOnFace(s) = a {
|
if let Artifact::StartSketchOnFace(s) = a {
|
||||||
@ -1299,7 +1371,13 @@ fn artifacts_to_update(
|
|||||||
let edge_id = if let Some(edge_id) = cmd.edge_id {
|
let edge_id = if let Some(edge_id) = cmd.edge_id {
|
||||||
ArtifactId::new(edge_id)
|
ArtifactId::new(edge_id)
|
||||||
} else {
|
} else {
|
||||||
cmd.edge_ids.first().unwrap().into()
|
let Some(edge_id) = cmd.edge_ids.first() else {
|
||||||
|
internal_error!(
|
||||||
|
range,
|
||||||
|
"Solid3dFilletEdge command has no edge ID: id={id:?}, cmd={cmd:?}"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
edge_id.into()
|
||||||
};
|
};
|
||||||
return_arr.push(Artifact::EdgeCut(EdgeCut {
|
return_arr.push(Artifact::EdgeCut(EdgeCut {
|
||||||
id,
|
id,
|
||||||
@ -1368,7 +1446,10 @@ fn artifacts_to_update(
|
|||||||
let solid_ids = union.solid_ids.iter().copied().map(ArtifactId::new).collect::<Vec<_>>();
|
let solid_ids = union.solid_ids.iter().copied().map(ArtifactId::new).collect::<Vec<_>>();
|
||||||
(CompositeSolidSubType::Union, solid_ids, Vec::new())
|
(CompositeSolidSubType::Union, solid_ids, Vec::new())
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => internal_error!(
|
||||||
|
range,
|
||||||
|
"Boolean or composite command variant not handled: id={id:?}, cmd={cmd:?}"
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut new_solid_ids = vec![id];
|
let mut new_solid_ids = vec![id];
|
||||||
|
@ -86,7 +86,7 @@ impl ExecutorContext {
|
|||||||
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError> {
|
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError> {
|
||||||
crate::log::log(format!("enter module {path} {}", exec_state.stack()));
|
crate::log::log(format!("enter module {path} {}", exec_state.stack()));
|
||||||
|
|
||||||
let mut local_state = ModuleState::new(path.std_path(), exec_state.stack().memory.clone(), Some(module_id));
|
let mut local_state = ModuleState::new(path.clone(), exec_state.stack().memory.clone(), Some(module_id));
|
||||||
if !preserve_mem {
|
if !preserve_mem {
|
||||||
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
||||||
}
|
}
|
||||||
@ -131,16 +131,21 @@ impl ExecutorContext {
|
|||||||
match statement {
|
match statement {
|
||||||
BodyItem::ImportStatement(import_stmt) => {
|
BodyItem::ImportStatement(import_stmt) => {
|
||||||
if !matches!(body_type, BodyType::Root) {
|
if !matches!(body_type, BodyType::Root) {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Imports are only supported at the top-level of a file.".to_owned(),
|
"Imports are only supported at the top-level of a file.".to_owned(),
|
||||||
source_ranges: vec![import_stmt.into()],
|
vec![import_stmt.into()],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let source_range = SourceRange::from(import_stmt);
|
let source_range = SourceRange::from(import_stmt);
|
||||||
let attrs = &import_stmt.outer_attrs;
|
let attrs = &import_stmt.outer_attrs;
|
||||||
|
let module_path = ModulePath::from_import_path(
|
||||||
|
&import_stmt.path,
|
||||||
|
&self.settings.project_directory,
|
||||||
|
&exec_state.mod_local.path,
|
||||||
|
)?;
|
||||||
let module_id = self
|
let module_id = self
|
||||||
.open_module(&import_stmt.path, attrs, exec_state, source_range)
|
.open_module(&import_stmt.path, attrs, &module_path, exec_state, source_range)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
match &import_stmt.selector {
|
match &import_stmt.selector {
|
||||||
@ -157,28 +162,25 @@ impl ExecutorContext {
|
|||||||
let mut ty = mem.get_from(&ty_name, env_ref, import_item.into(), 0).cloned();
|
let mut ty = mem.get_from(&ty_name, env_ref, import_item.into(), 0).cloned();
|
||||||
|
|
||||||
if value.is_err() && ty.is_err() {
|
if value.is_err() && ty.is_err() {
|
||||||
return Err(KclError::UndefinedValue(KclErrorDetails {
|
return Err(KclError::UndefinedValue(KclErrorDetails::new(
|
||||||
message: format!("{} is not defined in module", import_item.name.name),
|
format!("{} is not defined in module", import_item.name.name),
|
||||||
source_ranges: vec![SourceRange::from(&import_item.name)],
|
vec![SourceRange::from(&import_item.name)],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the item is allowed to be imported (in at least one namespace).
|
// Check that the item is allowed to be imported (in at least one namespace).
|
||||||
if value.is_ok() && !module_exports.contains(&import_item.name.name) {
|
if value.is_ok() && !module_exports.contains(&import_item.name.name) {
|
||||||
value = Err(KclError::Semantic(KclErrorDetails {
|
value = Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
|
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
|
||||||
import_item.name.name
|
import_item.name.name
|
||||||
),
|
),
|
||||||
source_ranges: vec![SourceRange::from(&import_item.name)],
|
vec![SourceRange::from(&import_item.name)],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ty.is_ok() && !module_exports.contains(&ty_name) {
|
if ty.is_ok() && !module_exports.contains(&ty_name) {
|
||||||
ty = Err(KclError::Semantic(KclErrorDetails {
|
ty = Err(KclError::Semantic(KclErrorDetails::new(String::new(), vec![])));
|
||||||
message: String::new(),
|
|
||||||
source_ranges: vec![],
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if value.is_err() && ty.is_err() {
|
if value.is_err() && ty.is_err() {
|
||||||
@ -225,10 +227,10 @@ impl ExecutorContext {
|
|||||||
.memory
|
.memory
|
||||||
.get_from(name, env_ref, source_range, 0)
|
.get_from(name, env_ref, source_range, 0)
|
||||||
.map_err(|_err| {
|
.map_err(|_err| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("{} is not defined in module (but was exported?)", name),
|
format!("{} is not defined in module (but was exported?)", name),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?
|
})?
|
||||||
.clone();
|
.clone();
|
||||||
exec_state.mut_stack().add(name.to_owned(), item, source_range)?;
|
exec_state.mut_stack().add(name.to_owned(), item, source_range)?;
|
||||||
@ -284,7 +286,14 @@ impl ExecutorContext {
|
|||||||
|
|
||||||
// Track exports.
|
// Track exports.
|
||||||
if let ItemVisibility::Export = variable_declaration.visibility {
|
if let ItemVisibility::Export = variable_declaration.visibility {
|
||||||
exec_state.mod_local.module_exports.push(var_name);
|
if matches!(body_type, BodyType::Root) {
|
||||||
|
exec_state.mod_local.module_exports.push(var_name);
|
||||||
|
} else {
|
||||||
|
exec_state.err(CompilationError::err(
|
||||||
|
variable_declaration.as_source_range(),
|
||||||
|
"Exports are only supported at the top-level of a file. Remove `export` or move it to the top-level.",
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Variable declaration can be the return value of a module.
|
// Variable declaration can be the return value of a module.
|
||||||
last_expr = matches!(body_type, BodyType::Root).then_some(value);
|
last_expr = matches!(body_type, BodyType::Root).then_some(value);
|
||||||
@ -294,13 +303,13 @@ impl ExecutorContext {
|
|||||||
let impl_kind = annotations::get_impl(&ty.outer_attrs, metadata.source_range)?.unwrap_or_default();
|
let impl_kind = annotations::get_impl(&ty.outer_attrs, metadata.source_range)?.unwrap_or_default();
|
||||||
match impl_kind {
|
match impl_kind {
|
||||||
annotations::Impl::Rust => {
|
annotations::Impl::Rust => {
|
||||||
let std_path = match &exec_state.mod_local.std_path {
|
let std_path = match &exec_state.mod_local.path {
|
||||||
Some(p) => p,
|
ModulePath::Std { value } => value,
|
||||||
None => {
|
ModulePath::Local { .. } | ModulePath::Main => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "User-defined types are not yet supported.".to_owned(),
|
"User-defined types are not yet supported.".to_owned(),
|
||||||
source_ranges: vec![metadata.source_range],
|
vec![metadata.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (t, props) = crate::std::std_ty(std_path, &ty.name.name);
|
let (t, props) = crate::std::std_ty(std_path, &ty.name.name);
|
||||||
@ -313,10 +322,10 @@ impl ExecutorContext {
|
|||||||
.mut_stack()
|
.mut_stack()
|
||||||
.add(name_in_mem.clone(), value, metadata.source_range)
|
.add(name_in_mem.clone(), value, metadata.source_range)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Redefinition of type {}.", ty.name.name),
|
format!("Redefinition of type {}.", ty.name.name),
|
||||||
source_ranges: vec![metadata.source_range],
|
vec![metadata.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if let ItemVisibility::Export = ty.visibility {
|
if let ItemVisibility::Export = ty.visibility {
|
||||||
@ -343,10 +352,10 @@ impl ExecutorContext {
|
|||||||
.mut_stack()
|
.mut_stack()
|
||||||
.add(name_in_mem.clone(), value, metadata.source_range)
|
.add(name_in_mem.clone(), value, metadata.source_range)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Redefinition of type {}.", ty.name.name),
|
format!("Redefinition of type {}.", ty.name.name),
|
||||||
source_ranges: vec![metadata.source_range],
|
vec![metadata.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if let ItemVisibility::Export = ty.visibility {
|
if let ItemVisibility::Export = ty.visibility {
|
||||||
@ -354,10 +363,10 @@ impl ExecutorContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "User-defined types are not yet supported.".to_owned(),
|
"User-defined types are not yet supported.".to_owned(),
|
||||||
source_ranges: vec![metadata.source_range],
|
vec![metadata.source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -368,10 +377,10 @@ impl ExecutorContext {
|
|||||||
let metadata = Metadata::from(return_statement);
|
let metadata = Metadata::from(return_statement);
|
||||||
|
|
||||||
if matches!(body_type, BodyType::Root) {
|
if matches!(body_type, BodyType::Root) {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Cannot return from outside a function.".to_owned(),
|
"Cannot return from outside a function.".to_owned(),
|
||||||
source_ranges: vec![metadata.source_range],
|
vec![metadata.source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = self
|
let value = self
|
||||||
@ -387,10 +396,10 @@ impl ExecutorContext {
|
|||||||
.mut_stack()
|
.mut_stack()
|
||||||
.add(memory::RETURN_NAME.to_owned(), value, metadata.source_range)
|
.add(memory::RETURN_NAME.to_owned(), value, metadata.source_range)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Multiple returns from a single function.".to_owned(),
|
"Multiple returns from a single function.".to_owned(),
|
||||||
source_ranges: vec![metadata.source_range],
|
vec![metadata.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
last_expr = None;
|
last_expr = None;
|
||||||
}
|
}
|
||||||
@ -416,16 +425,15 @@ impl ExecutorContext {
|
|||||||
&self,
|
&self,
|
||||||
path: &ImportPath,
|
path: &ImportPath,
|
||||||
attrs: &[Node<Annotation>],
|
attrs: &[Node<Annotation>],
|
||||||
|
resolved_path: &ModulePath,
|
||||||
exec_state: &mut ExecState,
|
exec_state: &mut ExecState,
|
||||||
source_range: SourceRange,
|
source_range: SourceRange,
|
||||||
) -> Result<ModuleId, KclError> {
|
) -> Result<ModuleId, KclError> {
|
||||||
let resolved_path = ModulePath::from_import_path(path, &self.settings.project_directory);
|
|
||||||
|
|
||||||
match path {
|
match path {
|
||||||
ImportPath::Kcl { .. } => {
|
ImportPath::Kcl { .. } => {
|
||||||
exec_state.global.mod_loader.cycle_check(&resolved_path, source_range)?;
|
exec_state.global.mod_loader.cycle_check(resolved_path, source_range)?;
|
||||||
|
|
||||||
if let Some(id) = exec_state.id_for_module(&resolved_path) {
|
if let Some(id) = exec_state.id_for_module(resolved_path) {
|
||||||
return Ok(id);
|
return Ok(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -436,12 +444,12 @@ impl ExecutorContext {
|
|||||||
exec_state.add_id_to_source(id, source.clone());
|
exec_state.add_id_to_source(id, source.clone());
|
||||||
// TODO handle parsing errors properly
|
// TODO handle parsing errors properly
|
||||||
let parsed = crate::parsing::parse_str(&source.source, id).parse_errs_as_err()?;
|
let parsed = crate::parsing::parse_str(&source.source, id).parse_errs_as_err()?;
|
||||||
exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed, None));
|
exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Kcl(parsed, None));
|
||||||
|
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
ImportPath::Foreign { .. } => {
|
ImportPath::Foreign { .. } => {
|
||||||
if let Some(id) = exec_state.id_for_module(&resolved_path) {
|
if let Some(id) = exec_state.id_for_module(resolved_path) {
|
||||||
return Ok(id);
|
return Ok(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -451,11 +459,11 @@ impl ExecutorContext {
|
|||||||
exec_state.add_path_to_source_id(resolved_path.clone(), id);
|
exec_state.add_path_to_source_id(resolved_path.clone(), id);
|
||||||
let format = super::import::format_from_annotations(attrs, path, source_range)?;
|
let format = super::import::format_from_annotations(attrs, path, source_range)?;
|
||||||
let geom = super::import::import_foreign(path, format, exec_state, self, source_range).await?;
|
let geom = super::import::import_foreign(path, format, exec_state, self, source_range).await?;
|
||||||
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom, None));
|
exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Foreign(geom, None));
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
ImportPath::Std { .. } => {
|
ImportPath::Std { .. } => {
|
||||||
if let Some(id) = exec_state.id_for_module(&resolved_path) {
|
if let Some(id) = exec_state.id_for_module(resolved_path) {
|
||||||
return Ok(id);
|
return Ok(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -467,7 +475,7 @@ impl ExecutorContext {
|
|||||||
let parsed = crate::parsing::parse_str(&source.source, id)
|
let parsed = crate::parsing::parse_str(&source.source, id)
|
||||||
.parse_errs_as_err()
|
.parse_errs_as_err()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed, None));
|
exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Kcl(parsed, None));
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -493,10 +501,10 @@ impl ExecutorContext {
|
|||||||
*cache = Some((val, er, items.clone()));
|
*cache = Some((val, er, items.clone()));
|
||||||
(er, items)
|
(er, items)
|
||||||
}),
|
}),
|
||||||
ModuleRepr::Foreign(geom, _) => Err(KclError::Semantic(KclErrorDetails {
|
ModuleRepr::Foreign(geom, _) => Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Cannot import items from foreign modules".to_owned(),
|
"Cannot import items from foreign modules".to_owned(),
|
||||||
source_ranges: vec![geom.source_range],
|
vec![geom.source_range],
|
||||||
})),
|
))),
|
||||||
ModuleRepr::Dummy => unreachable!("Looking up {}, but it is still being interpreted", path),
|
ModuleRepr::Dummy => unreachable!("Looking up {}, but it is still being interpreted", path),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -572,13 +580,13 @@ impl ExecutorContext {
|
|||||||
err.override_source_ranges(vec![source_range])
|
err.override_source_ranges(vec![source_range])
|
||||||
} else {
|
} else {
|
||||||
// TODO would be great to have line/column for the underlying error here
|
// TODO would be great to have line/column for the underlying error here
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Error loading imported file ({path}). Open it to view more details.\n {}",
|
"Error loading imported file ({path}). Open it to view more details.\n {}",
|
||||||
err.message()
|
err.message()
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -628,7 +636,7 @@ impl ExecutorContext {
|
|||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if rust_impl {
|
if rust_impl {
|
||||||
if let Some(std_path) = &exec_state.mod_local.std_path {
|
if let ModulePath::Std { value: std_path } = &exec_state.mod_local.path {
|
||||||
let (func, props) = crate::std::std_fn(std_path, statement_kind.expect_name());
|
let (func, props) = crate::std::std_fn(std_path, statement_kind.expect_name());
|
||||||
KclValue::Function {
|
KclValue::Function {
|
||||||
value: FunctionSource::Std {
|
value: FunctionSource::Std {
|
||||||
@ -639,11 +647,10 @@ impl ExecutorContext {
|
|||||||
meta: vec![metadata.to_owned()],
|
meta: vec![metadata.to_owned()],
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Rust implementation of functions is restricted to the standard library"
|
"Rust implementation of functions is restricted to the standard library".to_owned(),
|
||||||
.to_owned(),
|
vec![metadata.source_range],
|
||||||
source_ranges: vec![metadata.source_range],
|
)));
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Snapshotting memory here is crucial for semantics so that we close
|
// Snapshotting memory here is crucial for semantics so that we close
|
||||||
@ -667,18 +674,18 @@ impl ExecutorContext {
|
|||||||
"you cannot declare variable {name} as %, because % can only be used in function calls"
|
"you cannot declare variable {name} as %, because % can only be used in function calls"
|
||||||
);
|
);
|
||||||
|
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message,
|
message,
|
||||||
source_ranges: vec![pipe_substitution.into()],
|
vec![pipe_substitution.into()],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() {
|
StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() {
|
||||||
Some(x) => x,
|
Some(x) => x,
|
||||||
None => {
|
None => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "cannot use % outside a pipe expression".to_owned(),
|
"cannot use % outside a pipe expression".to_owned(),
|
||||||
source_ranges: vec![pipe_substitution.into()],
|
vec![pipe_substitution.into()],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -726,23 +733,7 @@ fn apply_ascription(
|
|||||||
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into())
|
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into())
|
||||||
.map_err(|e| KclError::Semantic(e.into()))?;
|
.map_err(|e| KclError::Semantic(e.into()))?;
|
||||||
|
|
||||||
let mut value = value.clone();
|
value.coerce(&ty, false, exec_state).map_err(|_| {
|
||||||
|
|
||||||
// If the number has unknown units but the user is explicitly specifying them, treat the value as having had it's units erased,
|
|
||||||
// rather than forcing the user to explicitly erase them.
|
|
||||||
if let KclValue::Number { value: n, meta, .. } = &value {
|
|
||||||
if let RuntimeType::Primitive(PrimitiveType::Number(num)) = &ty {
|
|
||||||
if num.is_fully_specified() {
|
|
||||||
value = KclValue::Number {
|
|
||||||
ty: NumericType::Any,
|
|
||||||
value: *n,
|
|
||||||
meta: meta.clone(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
value.coerce(&ty, exec_state).map_err(|_| {
|
|
||||||
let suggestion = if ty == RuntimeType::length() {
|
let suggestion = if ty == RuntimeType::length() {
|
||||||
", you might try coercing to a fully specified numeric type such as `number(mm)`"
|
", you might try coercing to a fully specified numeric type such as `number(mm)`"
|
||||||
} else if ty == RuntimeType::angle() {
|
} else if ty == RuntimeType::angle() {
|
||||||
@ -750,13 +741,13 @@ fn apply_ascription(
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"could not coerce {} value to type {ty}{suggestion}",
|
"could not coerce value of type {} to type {ty}{suggestion}",
|
||||||
value.human_friendly_type()
|
value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -783,10 +774,10 @@ impl Node<Name> {
|
|||||||
ctx: &ExecutorContext,
|
ctx: &ExecutorContext,
|
||||||
) -> Result<&'a KclValue, KclError> {
|
) -> Result<&'a KclValue, KclError> {
|
||||||
if self.abs_path {
|
if self.abs_path {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Absolute paths (names beginning with `::` are not yet supported)".to_owned(),
|
"Absolute paths (names beginning with `::` are not yet supported)".to_owned(),
|
||||||
source_ranges: self.as_source_ranges(),
|
self.as_source_ranges(),
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.path.is_empty() {
|
if self.path.is_empty() {
|
||||||
@ -798,10 +789,10 @@ impl Node<Name> {
|
|||||||
let value = match mem_spec {
|
let value = match mem_spec {
|
||||||
Some((env, exports)) => {
|
Some((env, exports)) => {
|
||||||
if !exports.contains(&p.name) {
|
if !exports.contains(&p.name) {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Item {} not found in module's exported items", p.name),
|
format!("Item {} not found in module's exported items", p.name),
|
||||||
source_ranges: p.as_source_ranges(),
|
p.as_source_ranges(),
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
exec_state
|
exec_state
|
||||||
@ -813,13 +804,13 @@ impl Node<Name> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let KclValue::Module { value: module_id, .. } = value else {
|
let KclValue::Module { value: module_id, .. } = value else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Identifier in path must refer to a module, found {}",
|
"Identifier in path must refer to a module, found {}",
|
||||||
value.human_friendly_type()
|
value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: p.as_source_ranges(),
|
p.as_source_ranges(),
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
mem_spec = Some(
|
mem_spec = Some(
|
||||||
@ -830,10 +821,10 @@ impl Node<Name> {
|
|||||||
|
|
||||||
let (env, exports) = mem_spec.unwrap();
|
let (env, exports) = mem_spec.unwrap();
|
||||||
if !exports.contains(&self.name.name) {
|
if !exports.contains(&self.name.name) {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Item {} not found in module's exported items", self.name.name),
|
format!("Item {} not found in module's exported items", self.name.name),
|
||||||
source_ranges: self.name.as_source_ranges(),
|
self.name.as_source_ranges(),
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
exec_state
|
exec_state
|
||||||
@ -861,46 +852,44 @@ impl Node<MemberExpression> {
|
|||||||
if let Some(value) = map.get(&property) {
|
if let Some(value) = map.get(&property) {
|
||||||
Ok(value.to_owned())
|
Ok(value.to_owned())
|
||||||
} else {
|
} else {
|
||||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
Err(KclError::UndefinedValue(KclErrorDetails::new(
|
||||||
message: format!("Property '{property}' not found in object"),
|
format!("Property '{property}' not found in object"),
|
||||||
source_ranges: vec![self.clone().into()],
|
vec![self.clone().into()],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(KclValue::Object { .. }, Property::String(property), true) => Err(KclError::Semantic(KclErrorDetails {
|
(KclValue::Object { .. }, Property::String(property), true) => {
|
||||||
message: format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"),
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.clone().into()],
|
format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"),
|
||||||
})),
|
vec![self.clone().into()],
|
||||||
|
)))
|
||||||
|
}
|
||||||
(KclValue::Object { .. }, p, _) => {
|
(KclValue::Object { .. }, p, _) => {
|
||||||
let t = p.type_name();
|
let t = p.type_name();
|
||||||
let article = article_for(t);
|
let article = article_for(t);
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!("Only strings can be used as the property of an object, but you're using {article} {t}",),
|
||||||
"Only strings can be used as the property of an object, but you're using {article} {t}",
|
vec![self.clone().into()],
|
||||||
),
|
)))
|
||||||
source_ranges: vec![self.clone().into()],
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
(KclValue::HomArray { value: arr, .. }, Property::UInt(index), _) => {
|
(KclValue::HomArray { value: arr, .. }, Property::UInt(index), _) => {
|
||||||
let value_of_arr = arr.get(index);
|
let value_of_arr = arr.get(index);
|
||||||
if let Some(value) = value_of_arr {
|
if let Some(value) = value_of_arr {
|
||||||
Ok(value.to_owned())
|
Ok(value.to_owned())
|
||||||
} else {
|
} else {
|
||||||
Err(KclError::UndefinedValue(KclErrorDetails {
|
Err(KclError::UndefinedValue(KclErrorDetails::new(
|
||||||
message: format!("The array doesn't have any item at index {index}"),
|
format!("The array doesn't have any item at index {index}"),
|
||||||
source_ranges: vec![self.clone().into()],
|
vec![self.clone().into()],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(KclValue::HomArray { .. }, p, _) => {
|
(KclValue::HomArray { .. }, p, _) => {
|
||||||
let t = p.type_name();
|
let t = p.type_name();
|
||||||
let article = article_for(t);
|
let article = article_for(t);
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!("Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",),
|
||||||
"Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",
|
vec![self.clone().into()],
|
||||||
),
|
)))
|
||||||
source_ranges: vec![self.clone().into()],
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
(KclValue::Solid { value }, Property::String(prop), false) if prop == "sketch" => Ok(KclValue::Sketch {
|
(KclValue::Solid { value }, Property::String(prop), false) if prop == "sketch" => Ok(KclValue::Sketch {
|
||||||
value: Box::new(value.sketch),
|
value: Box::new(value.sketch),
|
||||||
@ -918,10 +907,10 @@ impl Node<MemberExpression> {
|
|||||||
(being_indexed, _, _) => {
|
(being_indexed, _, _) => {
|
||||||
let t = being_indexed.human_friendly_type();
|
let t = being_indexed.human_friendly_type();
|
||||||
let article = article_for(&t);
|
let article = article_for(&t);
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
|
format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
|
||||||
source_ranges: vec![self.clone().into()],
|
vec![self.clone().into()],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -996,26 +985,26 @@ impl Node<BinaryExpression> {
|
|||||||
meta: _,
|
meta: _,
|
||||||
} = left_value
|
} = left_value
|
||||||
else {
|
else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Cannot apply logical operator to non-boolean value: {}",
|
"Cannot apply logical operator to non-boolean value: {}",
|
||||||
left_value.human_friendly_type()
|
left_value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: vec![self.left.clone().into()],
|
vec![self.left.clone().into()],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
let KclValue::Bool {
|
let KclValue::Bool {
|
||||||
value: right_value,
|
value: right_value,
|
||||||
meta: _,
|
meta: _,
|
||||||
} = right_value
|
} = right_value
|
||||||
else {
|
else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Cannot apply logical operator to non-boolean value: {}",
|
"Cannot apply logical operator to non-boolean value: {}",
|
||||||
right_value.human_friendly_type()
|
right_value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: vec![self.right.clone().into()],
|
vec![self.right.clone().into()],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
let raw_value = match self.operator {
|
let raw_value = match self.operator {
|
||||||
BinaryOperator::Or => left_value || right_value,
|
BinaryOperator::Or => left_value || right_value,
|
||||||
@ -1115,13 +1104,13 @@ impl Node<UnaryExpression> {
|
|||||||
meta: _,
|
meta: _,
|
||||||
} = value
|
} = value
|
||||||
else {
|
else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Cannot apply unary operator ! to non-boolean value: {}",
|
"Cannot apply unary operator ! to non-boolean value: {}",
|
||||||
value.human_friendly_type()
|
value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: vec![self.into()],
|
vec![self.into()],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
let meta = vec![Metadata {
|
let meta = vec![Metadata {
|
||||||
source_range: self.into(),
|
source_range: self.into(),
|
||||||
@ -1136,13 +1125,13 @@ impl Node<UnaryExpression> {
|
|||||||
|
|
||||||
let value = &self.argument.get_result(exec_state, ctx).await?;
|
let value = &self.argument.get_result(exec_state, ctx).await?;
|
||||||
let err = || {
|
let err = || {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"You can only negate numbers, planes, or lines, but this is a {}",
|
"You can only negate numbers, planes, or lines, but this is a {}",
|
||||||
value.human_friendly_type()
|
value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: vec![self.into()],
|
vec![self.into()],
|
||||||
})
|
))
|
||||||
};
|
};
|
||||||
match value {
|
match value {
|
||||||
KclValue::Number { value, ty, .. } => {
|
KclValue::Number { value, ty, .. } => {
|
||||||
@ -1239,10 +1228,10 @@ pub(crate) async fn execute_pipe_body(
|
|||||||
ctx: &ExecutorContext,
|
ctx: &ExecutorContext,
|
||||||
) -> Result<KclValue, KclError> {
|
) -> Result<KclValue, KclError> {
|
||||||
let Some((first, body)) = body.split_first() else {
|
let Some((first, body)) = body.split_first() else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Pipe expressions cannot be empty".to_owned(),
|
"Pipe expressions cannot be empty".to_owned(),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
// Evaluate the first element in the pipeline.
|
// 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 pipe_value from some AST node above this, so that if pipe expression is nested in a larger pipe expression,
|
||||||
@ -1277,10 +1266,10 @@ async fn inner_execute_pipe_body(
|
|||||||
) -> Result<KclValue, KclError> {
|
) -> Result<KclValue, KclError> {
|
||||||
for expression in body {
|
for expression in body {
|
||||||
if let Expr::TagDeclarator(_) = expression {
|
if let Expr::TagDeclarator(_) = expression {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("This cannot be in a PipeExpression: {:?}", expression),
|
format!("This cannot be in a PipeExpression: {:?}", expression),
|
||||||
source_ranges: vec![expression.into()],
|
vec![expression.into()],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
let metadata = Metadata {
|
let metadata = Metadata {
|
||||||
source_range: SourceRange::from(expression),
|
source_range: SourceRange::from(expression),
|
||||||
@ -1349,35 +1338,37 @@ impl Node<ArrayRangeExpression> {
|
|||||||
StatementKind::Expression,
|
StatementKind::Expression,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let (start, start_ty) = start_val.as_int_with_ty().ok_or(KclError::Semantic(KclErrorDetails {
|
let (start, start_ty) = start_val
|
||||||
source_ranges: vec![self.into()],
|
.as_int_with_ty()
|
||||||
message: format!("Expected int but found {}", start_val.human_friendly_type()),
|
.ok_or(KclError::Semantic(KclErrorDetails::new(
|
||||||
}))?;
|
format!("Expected int but found {}", start_val.human_friendly_type()),
|
||||||
|
vec![self.into()],
|
||||||
|
)))?;
|
||||||
let metadata = Metadata::from(&self.end_element);
|
let metadata = Metadata::from(&self.end_element);
|
||||||
let end_val = ctx
|
let end_val = ctx
|
||||||
.execute_expr(&self.end_element, exec_state, &metadata, &[], StatementKind::Expression)
|
.execute_expr(&self.end_element, exec_state, &metadata, &[], StatementKind::Expression)
|
||||||
.await?;
|
.await?;
|
||||||
let (end, end_ty) = end_val.as_int_with_ty().ok_or(KclError::Semantic(KclErrorDetails {
|
let (end, end_ty) = end_val.as_int_with_ty().ok_or(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.into()],
|
format!("Expected int but found {}", end_val.human_friendly_type()),
|
||||||
message: format!("Expected int but found {}", end_val.human_friendly_type()),
|
vec![self.into()],
|
||||||
}))?;
|
)))?;
|
||||||
|
|
||||||
if start_ty != end_ty {
|
if start_ty != end_ty {
|
||||||
let start = start_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: start_ty });
|
let start = start_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: start_ty });
|
||||||
let start = fmt::human_display_number(start.n, start.ty);
|
let start = fmt::human_display_number(start.n, start.ty);
|
||||||
let end = end_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: end_ty });
|
let end = end_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: end_ty });
|
||||||
let end = fmt::human_display_number(end.n, end.ty);
|
let end = fmt::human_display_number(end.n, end.ty);
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.into()],
|
format!("Range start and end must be of the same type, but found {start} and {end}"),
|
||||||
message: format!("Range start and end must be of the same type, but found {start} and {end}"),
|
vec![self.into()],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if end < start {
|
if end < start {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![self.into()],
|
format!("Range start is greater than range end: {start} .. {end}"),
|
||||||
message: format!("Range start is greater than range end: {start} .. {end}"),
|
vec![self.into()],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let range: Vec<_> = if self.end_inclusive {
|
let range: Vec<_> = if self.end_inclusive {
|
||||||
@ -1438,10 +1429,10 @@ fn article_for<S: AsRef<str>>(s: S) -> &'static str {
|
|||||||
fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> {
|
fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> {
|
||||||
v.as_ty_f64().ok_or_else(|| {
|
v.as_ty_f64().ok_or_else(|| {
|
||||||
let actual_type = v.human_friendly_type();
|
let actual_type = v.human_friendly_type();
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![source_range],
|
format!("Expected a number, but found {actual_type}",),
|
||||||
message: format!("Expected a number, but found {actual_type}",),
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1530,16 +1521,16 @@ impl Property {
|
|||||||
if let Some(x) = crate::try_f64_to_usize(value) {
|
if let Some(x) = crate::try_f64_to_usize(value) {
|
||||||
Ok(Property::UInt(x))
|
Ok(Property::UInt(x))
|
||||||
} else {
|
} else {
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: property_sr,
|
format!("{value} is not a valid index, indices must be whole numbers >= 0"),
|
||||||
message: format!("{value} is not a valid index, indices must be whole numbers >= 0"),
|
property_sr,
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Err(KclError::Semantic(KclErrorDetails {
|
_ => Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![sr],
|
"Only numbers (>= 0) can be indexes".to_owned(),
|
||||||
message: "Only numbers (>= 0) can be indexes".to_owned(),
|
vec![sr],
|
||||||
})),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1547,12 +1538,7 @@ impl Property {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> {
|
fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> {
|
||||||
let make_err = |message: String| {
|
let make_err = |message: String| Err::<Property, _>(KclError::Semantic(KclErrorDetails::new(message, property_sr)));
|
||||||
Err::<Property, _>(KclError::Semantic(KclErrorDetails {
|
|
||||||
source_ranges: property_sr,
|
|
||||||
message,
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
match value {
|
match value {
|
||||||
KclValue::Number{value: num, .. } => {
|
KclValue::Number{value: num, .. } => {
|
||||||
let num = *num;
|
let num = *num;
|
||||||
@ -1633,8 +1619,8 @@ arr1 = [42]: [number(cm)]
|
|||||||
assert_eq!(*ty, RuntimeType::known_length(UnitLen::Cm));
|
assert_eq!(*ty, RuntimeType::known_length(UnitLen::Cm));
|
||||||
// Compare, ignoring meta.
|
// Compare, ignoring meta.
|
||||||
if let KclValue::Number { value, ty, .. } = &value[0] {
|
if let KclValue::Number { value, ty, .. } = &value[0] {
|
||||||
// Converted from mm to cm.
|
// It should not convert units.
|
||||||
assert_eq!(*value, 4.2);
|
assert_eq!(*value, 42.0);
|
||||||
assert_eq!(*ty, NumericType::Known(UnitType::Length(UnitLen::Cm)));
|
assert_eq!(*ty, NumericType::Known(UnitType::Length(UnitLen::Cm)));
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected a number; found {:?}", value[0]);
|
panic!("Expected a number; found {:?}", value[0]);
|
||||||
@ -1650,7 +1636,7 @@ a = 42: string
|
|||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
err.to_string()
|
err.to_string()
|
||||||
.contains("could not coerce number(default units) value to type string"),
|
.contains("could not coerce value of type number(default units) to type string"),
|
||||||
"Expected error but found {err:?}"
|
"Expected error but found {err:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1661,7 +1647,7 @@ a = 42: Plane
|
|||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
err.to_string()
|
err.to_string()
|
||||||
.contains("could not coerce number(default units) value to type Plane"),
|
.contains("could not coerce value of type number(default units) to type Plane"),
|
||||||
"Expected error but found {err:?}"
|
"Expected error but found {err:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1671,8 +1657,9 @@ arr = [0]: [string]
|
|||||||
let result = parse_execute(program).await;
|
let result = parse_execute(program).await;
|
||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
err.to_string()
|
err.to_string().contains(
|
||||||
.contains("could not coerce [any; 1] value to type [string]"),
|
"could not coerce value of type array of number(default units) with 1 value to type [string]"
|
||||||
|
),
|
||||||
"Expected error but found {err:?}"
|
"Expected error but found {err:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1683,7 +1670,7 @@ mixedArr = [0, "a"]: [number(mm)]
|
|||||||
let err = result.unwrap_err();
|
let err = result.unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
err.to_string()
|
err.to_string()
|
||||||
.contains("could not coerce [any; 2] value to type [number(mm)]"),
|
.contains("could not coerce value of type array of number(default units), string with 2 values to type [number(mm)]"),
|
||||||
"Expected error but found {err:?}"
|
"Expected error but found {err:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1795,10 +1782,10 @@ d = b + c
|
|||||||
crate::engine::conn_mock::EngineConnection::new()
|
crate::engine::conn_mock::EngineConnection::new()
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
KclError::Internal(crate::errors::KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("Failed to create mock engine connection: {}", err),
|
format!("Failed to create mock engine connection: {}", err),
|
||||||
source_ranges: vec![SourceRange::default()],
|
vec![SourceRange::default()],
|
||||||
})
|
))
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)),
|
)),
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
use crate::execution::cad_op::{Group, OpArg, OpKclValue, Operation};
|
use super::{types::ArrayLen, EnvironmentRef};
|
||||||
use crate::{
|
use crate::{
|
||||||
docs::StdLibFn,
|
docs::StdLibFn,
|
||||||
errors::{KclError, KclErrorDetails},
|
errors::{KclError, KclErrorDetails},
|
||||||
execution::{
|
execution::{
|
||||||
kcl_value::FunctionSource, memory, types::RuntimeType, BodyType, ExecState, ExecutorContext, KclValue,
|
cad_op::{Group, OpArg, OpKclValue, Operation},
|
||||||
Metadata, StatementKind, TagEngineInfo, TagIdentifier,
|
kcl_value::FunctionSource,
|
||||||
|
memory,
|
||||||
|
types::RuntimeType,
|
||||||
|
BodyType, ExecState, ExecutorContext, KclValue, Metadata, StatementKind, TagEngineInfo, TagIdentifier,
|
||||||
},
|
},
|
||||||
parsing::ast::types::{CallExpressionKw, DefaultParamVal, FunctionExpression, Node, Program, Type},
|
parsing::ast::types::{CallExpressionKw, DefaultParamVal, FunctionExpression, Node, Program, Type},
|
||||||
source_range::SourceRange,
|
source_range::SourceRange,
|
||||||
@ -15,9 +18,6 @@ use crate::{
|
|||||||
CompilationError,
|
CompilationError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::types::ArrayLen;
|
|
||||||
use super::EnvironmentRef;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
/// Positional args.
|
/// Positional args.
|
||||||
@ -281,6 +281,13 @@ impl Node<CallExpressionKw> {
|
|||||||
def.call_kw(Some(func.name()), exec_state, ctx, args, callsite)
|
def.call_kw(Some(func.name()), exec_state, ctx, args, callsite)
|
||||||
.await
|
.await
|
||||||
.map(Option::unwrap)
|
.map(Option::unwrap)
|
||||||
|
.map_err(|e| {
|
||||||
|
// This is used for the backtrace display. We don't add
|
||||||
|
// another location the way we do for user-defined
|
||||||
|
// functions because the error uses the Args, which
|
||||||
|
// already points here.
|
||||||
|
e.set_last_backtrace_fn_name(Some(func.name()))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// Clone the function so that we can use a mutable reference to
|
// Clone the function so that we can use a mutable reference to
|
||||||
@ -288,10 +295,10 @@ impl Node<CallExpressionKw> {
|
|||||||
let func = fn_name.get_result(exec_state, ctx).await?.clone();
|
let func = fn_name.get_result(exec_state, ctx).await?.clone();
|
||||||
|
|
||||||
let Some(fn_src) = func.as_fn() else {
|
let Some(fn_src) = func.as_fn() else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "cannot call this because it isn't a function".to_string(),
|
"cannot call this because it isn't a function".to_string(),
|
||||||
source_ranges: vec![callsite],
|
vec![callsite],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let return_value = fn_src
|
let return_value = fn_src
|
||||||
@ -299,7 +306,10 @@ impl Node<CallExpressionKw> {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
// Add the call expression to the source ranges.
|
// Add the call expression to the source ranges.
|
||||||
e.add_source_ranges(vec![callsite])
|
//
|
||||||
|
// TODO: Use the name that the function was defined
|
||||||
|
// with, not the identifier it was used with.
|
||||||
|
e.add_unwind_location(Some(fn_name.name.name.clone()), callsite)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let result = return_value.ok_or_else(move || {
|
let result = return_value.ok_or_else(move || {
|
||||||
@ -308,10 +318,10 @@ impl Node<CallExpressionKw> {
|
|||||||
if let KclValue::Function { meta, .. } = func {
|
if let KclValue::Function { meta, .. } = func {
|
||||||
source_ranges = meta.iter().map(|m| m.source_range).collect();
|
source_ranges = meta.iter().map(|m| m.source_range).collect();
|
||||||
};
|
};
|
||||||
KclError::UndefinedValue(KclErrorDetails {
|
KclError::UndefinedValue(KclErrorDetails::new(
|
||||||
message: format!("Result of user-defined function {} is undefined", fn_name),
|
format!("Result of user-defined function {} is undefined", fn_name),
|
||||||
source_ranges,
|
source_ranges,
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@ -490,10 +500,10 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
|
|||||||
let tag_id = if let Some(t) = value.sketch.tags.get(&tag.name) {
|
let tag_id = if let Some(t) = value.sketch.tags.get(&tag.name) {
|
||||||
let mut t = t.clone();
|
let mut t = t.clone();
|
||||||
let Some(info) = t.get_cur_info() else {
|
let Some(info) = t.get_cur_info() else {
|
||||||
return Err(KclError::Internal(KclErrorDetails {
|
return Err(KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("Tag {} does not have path info", tag.name),
|
format!("Tag {} does not have path info", tag.name),
|
||||||
source_ranges: vec![tag.into()],
|
vec![tag.into()],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut info = info.clone();
|
let mut info = info.clone();
|
||||||
@ -596,6 +606,7 @@ fn type_check_params_kw(
|
|||||||
.value
|
.value
|
||||||
.coerce(
|
.coerce(
|
||||||
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range).map_err(|e| KclError::Semantic(e.into()))?,
|
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range).map_err(|e| KclError::Semantic(e.into()))?,
|
||||||
|
true,
|
||||||
exec_state,
|
exec_state,
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@ -608,10 +619,10 @@ fn type_check_params_kw(
|
|||||||
// TODO if we have access to the AST for the argument we could choose which example to suggest.
|
// TODO if we have access to the AST for the argument we could choose which example to suggest.
|
||||||
message = format!("{message}\n\nYou may need to add information about the type of the argument, for example:\n using a numeric suffix: `42{ty}`\n or using type ascription: `foo(): number({ty})`");
|
message = format!("{message}\n\nYou may need to add information about the type of the argument, for example:\n using a numeric suffix: `42{ty}`\n or using type ascription: `foo(): number({ty})`");
|
||||||
}
|
}
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message,
|
message,
|
||||||
source_ranges: vec![arg.source_range],
|
vec![arg.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -670,11 +681,12 @@ fn type_check_params_kw(
|
|||||||
.coerce(
|
.coerce(
|
||||||
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
|
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
|
||||||
.map_err(|e| KclError::Semantic(e.into()))?,
|
.map_err(|e| KclError::Semantic(e.into()))?,
|
||||||
|
true,
|
||||||
exec_state,
|
exec_state,
|
||||||
)
|
)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"The input argument of {} requires a value with type `{}`, but found {}",
|
"The input argument of {} requires a value with type `{}`, but found {}",
|
||||||
fn_name
|
fn_name
|
||||||
.map(|n| format!("`{}`", n))
|
.map(|n| format!("`{}`", n))
|
||||||
@ -682,8 +694,8 @@ fn type_check_params_kw(
|
|||||||
ty,
|
ty,
|
||||||
arg.1.value.human_friendly_type()
|
arg.1.value.human_friendly_type()
|
||||||
),
|
),
|
||||||
source_ranges: vec![arg.1.source_range],
|
vec![arg.1.source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
} else if let Some((name, _)) = &fn_def.input_arg {
|
} else if let Some((name, _)) = &fn_def.input_arg {
|
||||||
@ -730,13 +742,13 @@ fn assign_args_to_params_kw(
|
|||||||
.add(name.clone(), value, default_val.source_range())?;
|
.add(name.clone(), value, default_val.source_range())?;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges,
|
format!(
|
||||||
message: format!(
|
|
||||||
"This function requires a parameter {}, but you haven't passed it one.",
|
"This function requires a parameter {}, but you haven't passed it one.",
|
||||||
name
|
name
|
||||||
),
|
),
|
||||||
}));
|
source_ranges,
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -747,16 +759,15 @@ fn assign_args_to_params_kw(
|
|||||||
|
|
||||||
let Some(unlabeled) = unlabelled else {
|
let Some(unlabeled) = unlabelled else {
|
||||||
return Err(if args.kw_args.labeled.contains_key(param_name) {
|
return Err(if args.kw_args.labeled.contains_key(param_name) {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
|
format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"),
|
||||||
source_ranges,
|
source_ranges,
|
||||||
message: format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"),
|
))
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
|
"This function expects an unlabeled first parameter, but you haven't passed it one.".to_owned(),
|
||||||
source_ranges,
|
source_ranges,
|
||||||
message: "This function expects an unlabeled first parameter, but you haven't passed it one."
|
))
|
||||||
.to_owned(),
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
exec_state.mut_stack().add(
|
exec_state.mut_stack().add(
|
||||||
@ -788,15 +799,15 @@ fn coerce_result_type(
|
|||||||
if let RuntimeType::Array(inner, ArrayLen::NonEmpty) = &ty {
|
if let RuntimeType::Array(inner, ArrayLen::NonEmpty) = &ty {
|
||||||
ty = RuntimeType::Union(vec![(**inner).clone(), ty]);
|
ty = RuntimeType::Union(vec![(**inner).clone(), ty]);
|
||||||
}
|
}
|
||||||
let val = val.coerce(&ty, exec_state).map_err(|_| {
|
let val = val.coerce(&ty, true, exec_state).map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"This function requires its result to be of type `{}`, but found {}",
|
"This function requires its result to be of type `{}`, but found {}",
|
||||||
ty.human_friendly_type(),
|
ty.human_friendly_type(),
|
||||||
val.human_friendly_type(),
|
val.human_friendly_type(),
|
||||||
),
|
),
|
||||||
source_ranges: ret_ty.as_source_ranges(),
|
ret_ty.as_source_ranges(),
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
Ok(Some(val))
|
Ok(Some(val))
|
||||||
} else {
|
} else {
|
||||||
@ -873,10 +884,10 @@ mod test {
|
|||||||
"all params required, none given, should error",
|
"all params required, none given, should error",
|
||||||
vec![req_param("x")],
|
vec![req_param("x")],
|
||||||
vec![],
|
vec![],
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![SourceRange::default()],
|
"This function requires a parameter x, but you haven't passed it one.".to_owned(),
|
||||||
message: "This function requires a parameter x, but you haven't passed it one.".to_owned(),
|
vec![SourceRange::default()],
|
||||||
})),
|
))),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"all params optional, none given, should be OK",
|
"all params optional, none given, should be OK",
|
||||||
@ -888,10 +899,10 @@ mod test {
|
|||||||
"mixed params, too few given",
|
"mixed params, too few given",
|
||||||
vec![req_param("x"), opt_param("y")],
|
vec![req_param("x"), opt_param("y")],
|
||||||
vec![],
|
vec![],
|
||||||
Err(KclError::Semantic(KclErrorDetails {
|
Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
source_ranges: vec![SourceRange::default()],
|
"This function requires a parameter x, but you haven't passed it one.".to_owned(),
|
||||||
message: "This function requires a parameter x, but you haven't passed it one.".to_owned(),
|
vec![SourceRange::default()],
|
||||||
})),
|
))),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"mixed params, minimum given, should be OK",
|
"mixed params, minimum given, should be OK",
|
||||||
|
@ -469,18 +469,18 @@ impl TryFrom<PlaneData> for PlaneInfo {
|
|||||||
PlaneData::NegYZ => PlaneName::NegYz,
|
PlaneData::NegYZ => PlaneName::NegYz,
|
||||||
PlaneData::Plane(_) => {
|
PlaneData::Plane(_) => {
|
||||||
// We will never get here since we already checked for PlaneData::Plane.
|
// We will never get here since we already checked for PlaneData::Plane.
|
||||||
return Err(KclError::Internal(KclErrorDetails {
|
return Err(KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("PlaneData {:?} not found", value),
|
format!("PlaneData {:?} not found", value),
|
||||||
source_ranges: Default::default(),
|
Default::default(),
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
|
let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
|
||||||
KclError::Internal(KclErrorDetails {
|
KclError::Internal(KclErrorDetails::new(
|
||||||
message: format!("Plane {} not found", name),
|
format!("Plane {} not found", name),
|
||||||
source_ranges: Default::default(),
|
Default::default(),
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(info.clone())
|
Ok(info.clone())
|
||||||
|
@ -37,53 +37,43 @@ pub async fn import_foreign(
|
|||||||
) -> Result<PreImportedGeometry, KclError> {
|
) -> Result<PreImportedGeometry, KclError> {
|
||||||
// Make sure the file exists.
|
// Make sure the file exists.
|
||||||
if !ctxt.fs.exists(file_path, source_range).await? {
|
if !ctxt.fs.exists(file_path, source_range).await? {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("File `{}` does not exist.", file_path.display()),
|
format!("File `{}` does not exist.", file_path.display()),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let ext_format = get_import_format_from_extension(file_path.extension().ok_or_else(|| {
|
let ext_format = get_import_format_from_extension(file_path.extension().ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("No file extension found for `{}`", file_path.display()),
|
format!("No file extension found for `{}`", file_path.display()),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?)
|
})?)
|
||||||
.map_err(|e| {
|
.map_err(|e| KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?;
|
||||||
KclError::Semantic(KclErrorDetails {
|
|
||||||
message: e.to_string(),
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Get the format type from the extension of the file.
|
// Get the format type from the extension of the file.
|
||||||
let format = if let Some(format) = format {
|
let format = if let Some(format) = format {
|
||||||
// Validate the given format with the extension format.
|
// Validate the given format with the extension format.
|
||||||
validate_extension_format(ext_format, format.clone()).map_err(|e| {
|
validate_extension_format(ext_format, format.clone())
|
||||||
KclError::Semantic(KclErrorDetails {
|
.map_err(|e| KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?;
|
||||||
message: e.to_string(),
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
format
|
format
|
||||||
} else {
|
} else {
|
||||||
ext_format
|
ext_format
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the file contents for each file path.
|
// Get the file contents for each file path.
|
||||||
let file_contents = ctxt.fs.read(file_path, source_range).await.map_err(|e| {
|
let file_contents = ctxt
|
||||||
KclError::Semantic(KclErrorDetails {
|
.fs
|
||||||
message: e.to_string(),
|
.read(file_path, source_range)
|
||||||
source_ranges: vec![source_range],
|
.await
|
||||||
})
|
.map_err(|e| KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?;
|
||||||
})?;
|
|
||||||
|
|
||||||
// We want the file_path to be without the parent.
|
// We want the file_path to be without the parent.
|
||||||
let file_name = file_path.file_name().map(|p| p.to_string()).ok_or_else(|| {
|
let file_name = file_path.file_name().map(|p| p.to_string()).ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Could not get the file name from the path `{}`", file_path.display()),
|
format!("Could not get the file name from the path `{}`", file_path.display()),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
let mut import_files = vec![kcmc::ImportFile {
|
let mut import_files = vec![kcmc::ImportFile {
|
||||||
path: file_name.to_string(),
|
path: file_name.to_string(),
|
||||||
@ -96,12 +86,8 @@ pub async fn import_foreign(
|
|||||||
// Check if the file is a binary gltf file, in that case we don't need to import the bin
|
// Check if the file is a binary gltf file, in that case we don't need to import the bin
|
||||||
// file.
|
// file.
|
||||||
if !file_contents.starts_with(b"glTF") {
|
if !file_contents.starts_with(b"glTF") {
|
||||||
let json = gltf_json::Root::from_slice(&file_contents).map_err(|e| {
|
let json = gltf_json::Root::from_slice(&file_contents)
|
||||||
KclError::Semantic(KclErrorDetails {
|
.map_err(|e| KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range])))?;
|
||||||
message: e.to_string(),
|
|
||||||
source_ranges: vec![source_range],
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Read the gltf file and check if there is a bin file.
|
// Read the gltf file and check if there is a bin file.
|
||||||
for buffer in json.buffers.iter() {
|
for buffer in json.buffers.iter() {
|
||||||
@ -109,18 +95,16 @@ pub async fn import_foreign(
|
|||||||
if !uri.starts_with("data:") {
|
if !uri.starts_with("data:") {
|
||||||
// We want this path relative to the file_path given.
|
// We want this path relative to the file_path given.
|
||||||
let bin_path = file_path.parent().map(|p| p.join(uri)).ok_or_else(|| {
|
let bin_path = file_path.parent().map(|p| p.join(uri)).ok_or_else(|| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!("Could not get the parent path of the file `{}`", file_path.display()),
|
format!("Could not get the parent path of the file `{}`", file_path.display()),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
})
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let bin_contents = ctxt.fs.read(&bin_path, source_range).await.map_err(|e| {
|
let bin_contents =
|
||||||
KclError::Semantic(KclErrorDetails {
|
ctxt.fs.read(&bin_path, source_range).await.map_err(|e| {
|
||||||
message: e.to_string(),
|
KclError::Semantic(KclErrorDetails::new(e.to_string(), vec![source_range]))
|
||||||
source_ranges: vec![source_range],
|
})?;
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
import_files.push(ImportFile {
|
import_files.push(ImportFile {
|
||||||
path: uri.to_string(),
|
path: uri.to_string(),
|
||||||
@ -157,13 +141,13 @@ pub(super) fn format_from_annotations(
|
|||||||
if p.key.name == annotations::IMPORT_FORMAT {
|
if p.key.name == annotations::IMPORT_FORMAT {
|
||||||
result = Some(
|
result = Some(
|
||||||
get_import_format_from_extension(annotations::expect_ident(&p.value)?).map_err(|_| {
|
get_import_format_from_extension(annotations::expect_ident(&p.value)?).map_err(|_| {
|
||||||
KclError::Semantic(KclErrorDetails {
|
KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Unknown format for import, expected one of: {}",
|
"Unknown format for import, expected one of: {}",
|
||||||
crate::IMPORT_FILE_EXTENSIONS.join(", ")
|
crate::IMPORT_FILE_EXTENSIONS.join(", ")
|
||||||
),
|
),
|
||||||
source_ranges: vec![p.as_source_range()],
|
vec![p.as_source_range()],
|
||||||
})
|
))
|
||||||
})?,
|
})?,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -175,10 +159,10 @@ pub(super) fn format_from_annotations(
|
|||||||
path.extension()
|
path.extension()
|
||||||
.and_then(|ext| get_import_format_from_extension(ext).ok())
|
.and_then(|ext| get_import_format_from_extension(ext).ok())
|
||||||
})
|
})
|
||||||
.ok_or(KclError::Semantic(KclErrorDetails {
|
.ok_or(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: "Unknown or missing extension, and no specified format for imported file".to_owned(),
|
"Unknown or missing extension, and no specified format for imported file".to_owned(),
|
||||||
source_ranges: vec![import_source_range],
|
vec![import_source_range],
|
||||||
}))?;
|
)))?;
|
||||||
|
|
||||||
for p in props {
|
for p in props {
|
||||||
match p.key.name.as_str() {
|
match p.key.name.as_str() {
|
||||||
@ -190,15 +174,15 @@ pub(super) fn format_from_annotations(
|
|||||||
}
|
}
|
||||||
annotations::IMPORT_FORMAT => {}
|
annotations::IMPORT_FORMAT => {}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Unexpected annotation for import, expected one of: {}, {}, {}",
|
"Unexpected annotation for import, expected one of: {}, {}, {}",
|
||||||
annotations::IMPORT_FORMAT,
|
annotations::IMPORT_FORMAT,
|
||||||
annotations::IMPORT_COORDS,
|
annotations::IMPORT_COORDS,
|
||||||
annotations::IMPORT_LENGTH_UNIT
|
annotations::IMPORT_LENGTH_UNIT
|
||||||
),
|
),
|
||||||
source_ranges: vec![p.as_source_range()],
|
vec![p.as_source_range()],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -215,8 +199,8 @@ fn set_coords(fmt: &mut InputFormat3d, coords_str: &str, source_range: SourceRan
|
|||||||
}
|
}
|
||||||
|
|
||||||
let Some(coords) = coords else {
|
let Some(coords) = coords else {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"Unknown coordinate system: {coords_str}, expected one of: {}",
|
"Unknown coordinate system: {coords_str}, expected one of: {}",
|
||||||
annotations::IMPORT_COORDS_VALUES
|
annotations::IMPORT_COORDS_VALUES
|
||||||
.iter()
|
.iter()
|
||||||
@ -224,8 +208,8 @@ fn set_coords(fmt: &mut InputFormat3d, coords_str: &str, source_range: SourceRan
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ")
|
.join(", ")
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
match fmt {
|
match fmt {
|
||||||
@ -233,13 +217,13 @@ fn set_coords(fmt: &mut InputFormat3d, coords_str: &str, source_range: SourceRan
|
|||||||
InputFormat3d::Ply(opts) => opts.coords = coords,
|
InputFormat3d::Ply(opts) => opts.coords = coords,
|
||||||
InputFormat3d::Stl(opts) => opts.coords = coords,
|
InputFormat3d::Stl(opts) => opts.coords = coords,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"`{}` option cannot be applied to the specified format",
|
"`{}` option cannot be applied to the specified format",
|
||||||
annotations::IMPORT_COORDS
|
annotations::IMPORT_COORDS
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,13 +238,13 @@ fn set_length_unit(fmt: &mut InputFormat3d, units_str: &str, source_range: Sourc
|
|||||||
InputFormat3d::Ply(opts) => opts.units = units.into(),
|
InputFormat3d::Ply(opts) => opts.units = units.into(),
|
||||||
InputFormat3d::Stl(opts) => opts.units = units.into(),
|
InputFormat3d::Stl(opts) => opts.units = units.into(),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(KclError::Semantic(KclErrorDetails {
|
return Err(KclError::Semantic(KclErrorDetails::new(
|
||||||
message: format!(
|
format!(
|
||||||
"`{}` option cannot be applied to the specified format",
|
"`{}` option cannot be applied to the specified format",
|
||||||
annotations::IMPORT_LENGTH_UNIT
|
annotations::IMPORT_LENGTH_UNIT
|
||||||
),
|
),
|
||||||
source_ranges: vec![source_range],
|
vec![source_range],
|
||||||
}))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|