Compare commits

...

43 Commits

Author SHA1 Message Date
8340f6b906 Give a default value to projectName in Text-to-CAD Create command to allow invocation through command palette (#7061)
Give a default value to projectName

This argument is hidden since it's marked as skip, so browser users who
must now invoke the command through the command palette have no way of
setting the value. Since it is also required, the command palette fails
silently on submission (which we need to fix more broadly). This gives
it a default value so that users can submit properly.
2025-05-19 18:13:47 +00:00
ddb034b14d Show KCL backtraces (#7033)
* Add backtrace to errors

* Add display of backtraces with hints

* Change pane badge to only show count of errors

* Fix property name to not collide with Error superclass

* Increase min stack again

* Add e2e test that checks that the diagnostics are created in CodeMirror

* Remove unneeded code

* Change to the new hotness
2025-05-19 18:13:10 +00:00
bfa2f67393 Add a 1s client side rate limit / cache to billing requests (#7067)
* Add a 1s client side rate limit / cache to billing requests

* Stop using err; use isErr instead
2025-05-19 14:11:33 -04:00
447069a97b Revert "Remove Create with Text-to-CAD from toolbar, make commands in command palette more distinct" (#7068)
Revert "Remove Create with Text-to-CAD from toolbar, make commands in command…"

This reverts commit 5734cc7fc3.
2025-05-19 14:05:18 -04:00
49b78d726a Submit selection to command on unmount of selection arg input (#7047)
* Submit selection to command on unmount of selection arg input

This fixes #7024 by saving the user's selection to the command even if
they click another argument in the command palette's header. It does no
harm to save the selection to the argument, even if it's being torn down
because the user dismissed it has no negative effect.

* Refactor to not auto-submit before selection is cleared on mount

Thanks E2E test suite

* Update failing E2E tests with new behavior, which allows skip with preselection

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-05-19 13:21:43 -04:00
5b4cddd0b0 Fix React re-render (#7038)
feat: memoize share button to avoid re-renders
2025-05-19 12:58:35 -04:00
8878c148ed Fix reset project dir setting (#7064)
* Fix reset project dir setting
Fixes #6432

* Lint
2025-05-19 16:51:08 +00:00
c3c2ded795 Fix cleanup in network status hook (#7034)
fix: remove event listeners on unmount
2025-05-19 11:38:53 -04:00
fb35fdcc38 Disallow segment selection in all sweeps and change Sketches display name to Profiles (#7045)
* Disallow segment selection in sweep, plus displayName: Profiles for clarity
Fixes #7044

* Change selection hints for solid2d to be profile instead of face

* Update tests

* More fixes

* Fix tests following behavior change: we don't select segments in code anymore but profiles
2025-05-19 11:21:29 -04:00
e76ba9921c No onboarding toast if query params (#7060)
* No onboarding toast if query params

* Changed dependences
2025-05-19 09:38:38 -04:00
b19acd550d Type check and coerce arguments to user functions and return values from std Rust functions (#6958)
* Shuffle around function call code

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Refactor function calls to share more code

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Hack to leave the result of revolve as a singleton rather than array

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-05-19 16:50:15 +12:00
f3e9d110c0 KCL: Default circular pattern rotateDuplicates=true, arcDegrees=360 (#7052)
KCL: Default circular pattern rotateDuplicates=true, arcDeg=360

Seems like most users would want these.
2025-05-19 14:46:00 +12:00
658497da1d Allow same syntax for patterns as mirror revolve (#7054)
* allow named axis for patterns

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

* docs

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

* images

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

* Fix typo

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
2025-05-19 02:25:35 +00:00
bd01059a92 more csg regression tests (#7032)
* more csg regression tests

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

* artifacts

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-05-18 16:50:51 -07:00
57a977e6be Fix simulation_test file load error (#7042)
Co-authored-by: Lucas Kent <rubickent@gmail.com>
2025-05-18 16:28:59 -07:00
94b0cc1f3e Remove remaining links to nightly (#7051) 2025-05-18 11:21:31 -04:00
5734cc7fc3 Remove Create with Text-to-CAD from toolbar, make commands in command palette more distinct (#7048)
* Remove Create with Text-to-CAD from the toolbar

* Remove "prompt-to-edit" wording, call commands "create" and "edit"

* Use sparkles for the ML feature, not chat

* lints

* Start fixing up tests, there are probably more though

* Fix up a few more tests

* Fix up prompt-to-edit tests (yay using fixtures!)

* Fix native file menu tests

* Update snapshots

* Fix menu test

* Fix snaps

---------

Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
2025-05-18 10:24:48 +00:00
3168c22de7 Remove false positive missing messages for other module SourceRanges (#7050) 2025-05-18 06:21:10 -04:00
3c94fe9047 Make warning toast not appear if the URL has any search params (#7046)
* Make warning toast not appear if the URL has any search params

This should avoid the scenario where someone clicks an "open sample"
type link and dismisses the command palette before they can finish what
they're doing.

* Update src/App.tsx

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-18 01:23:43 +00:00
cdd6b56d42 Change wasm init failed banner to a toast (#7043)
* Change wasm init failed banner to a toast
Closes #6976 for good!

* Move :(

* Rm testing bit

* Align left and bot suggestions

* Update src/components/WasmErrToast.tsx

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-17 18:01:12 -04:00
75ac3bc61b Replace map with forEach in CLI arg tests (#7035)
test: use forEach for command-line arg cases
2025-05-17 14:20:39 -04:00
29d511d085 [Fix] Updating docs for mirror2d (#7013)
* fix: mirror2d works on closed sketches

* fix: generating docs
2025-05-17 13:00:29 -05:00
max
b0a41939e8 Max's KCL samples (#7041)
* 3d models

* Update kcl-samples simulation test output

* typos

* Update kcl-samples simulation test output

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-05-17 11:16:22 -04:00
7d2c1061ba Allow global commands to be invoked from the command palette via URL (#6973) 2025-05-17 07:51:25 -04:00
d768073d17 [Fix] Using main.kcl for more of the workflows that generate kcl files (#7017)
* fix: when creating a t2c in a new project make the file name main.kcl

* fix: when creating a sample in a new project and there is only 1 file make the filename main.kcl

* fix: auto fixes

* fix: share links generate main.kcl

* fix: codespell typoe

* fix: fixing E2E tests

* Fix 3 more tests

* fix: share url link e2e file name fix

---------

Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
2025-05-17 03:43:25 +00:00
dc8496c62e Change download-app banner to a toast & permanent button (#7010)
* pierremtb/issue6976-download-app-toast

* Cleaning up, more testing

* we goin places ft. new icon thanks @franknoirot

* Add app-version to masks

* Revert "Add app-version to masks"

This reverts commit 9624c3f434.

* Update dialog logic and snapshots

* Update snapssss

* Hook up settingsActor

* Polish

* Fix snap

* Quick fix, thanks bot

* Cleaning up linksssssssss
2025-05-16 23:25:04 -04:00
416de9a9fb allow more than one tool (#6945)
* allow more than one tool

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

* updates

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

* updates

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

* updates

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

* update tests

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

* fmt

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

* bump kcl

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-05-16 23:22:50 +00:00
da65426ddc Fix to account for cached body items when computing NodePath (#7030) 2025-05-16 23:22:01 +00:00
585b485852 Revert "stupid paths filtering" (#7028)
Revert "stupid paths filtering (#7027)"

This reverts commit e85f16ff9c.
2025-05-16 22:29:34 +00:00
e85f16ff9c stupid paths filtering (#7027)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-05-16 15:02:58 -07:00
e7d2289a14 Revolve adjacency info (#7008)
* check edge colinear

* cleanup

* revert cargo.toml

* revert cargo.toml properly

* undo yarn.lock

* clippy

* add new sim test

* move tolerance2

* typo

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

* fmt

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

* update snap

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

* push real snap

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

* update tests

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
2025-05-16 22:02:30 +00:00
d35531758d fix read_to_string to give error (#7022)
updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-05-16 16:56:19 -04:00
729e0a7949 add a subtract regression test (#7018)
* add a subtraact regression test

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

* also rename some github actions job so we can require them;

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

* artifacts

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-05-16 20:06:56 +00:00
620b7401aa Update Rust tests to use internal KCL samples on CI (#7014)
* Update Rust tests to use internal KCL samples on CI

* Regenerate manifest with internal KCL samples

* try again

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

* remove the needs

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

* updates

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

* updates

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

* updates

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

* updates

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

* features

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

* features

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

* updates

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

* secret

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

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
2025-05-16 19:38:55 +00:00
e3e67b00d5 Run the component tests on CI (#7009)
* Run the component tests on CI

* Share WASM build

* Add basic token permissions
2025-05-16 15:51:43 +00:00
49d4f8e5c3 Remove the warningMessage field for command args (#7005) 2025-05-16 10:41:33 -05:00
47b159c605 Remove check on the Start Sketch button (#7004) 2025-05-16 10:38:18 -05:00
c7b086fa69 Change warning message on Chamfer for a more discreet note in tooltip (#7002) 2025-05-16 14:00:31 +00:00
203db79204 Fix light mode code mirror tooltip (#7001)
A CSS selector just needed `.dark` prepended to it
2025-05-16 13:37:41 +00:00
48a4fd8373 Organization and Pro tier link sharing features exposure (#6986)
* Improve url sharing for orgs and pros

* Remove sharing via menu item

* fmt

* Not sure what's different but it is

* fmt & lint

* whoops

* Update snapshots

* Typos from codespell

* Fix alignment

* Update snapshots

* Prune

---------

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Pierre Jacquier <pierre@zoo.dev>
2025-05-16 09:51:08 +00:00
17eb84325f Quick follow up fixing up experimetal progress/submit button colors (#6999) 2025-05-16 19:44:10 +10:00
ebf048478d Make Modify with Text-to-CAD selection arg optional by honoring skip: false on non-required args (#6992)
* Make "skip = false" non-required args appear in header

* Make non-required, unskippable selection args work

* Make prompt-to-edit's selection arg optional but non-skippable

* Update src/components/CommandBar/CommandBarSelectionInput.tsx

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Fix dumb logic bug

Thanks for user testing @Irev-dev

* Update mixed input to show selection

Feel free to revert @Irev-Dev if this is the wrong move, but I found it
odd that this component doesn't show the current selection in the text
like the other selection input does, so I copied that over.

* Merge branch 'main' into franknoirot/adhoc/optional-selection-args

* Merge branch 'main' into franknoirot/adhoc/optional-selection-args

* Merge remote-tracking branch 'origin' into franknoirot/adhoc/optional-selection-args

* fix tests

* change copy again

* Update src/components/CommandBar/CommandBarSelectionMixedInput.tsx

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-05-16 05:15:52 -04:00
28a8cd2421 [Bug] fix big screen boot up bug (#6998)
fix big screen boot up bug
2025-05-16 04:55:09 -04:00
495 changed files with 168721 additions and 16462 deletions

56
.github/workflows/build-wasm.yml vendored Normal file
View File

@ -0,0 +1,56 @@
name: Build WASM
on:
workflow_call:
permissions:
contents: read
jobs:
npm-build-wasm:
runs-on: runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Use correct Rust toolchain
shell: bash
run: |
[ -e rust-toolchain.toml ] || cp rust/rust-toolchain.toml ./
- name: Install rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false # configured below
- uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc
with:
tool: wasm-pack
- name: Use Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: './rust'
- name: Build Wasm
shell: bash
run: npm run build:wasm
- uses: actions/upload-artifact@v4
with:
name: prepared-wasm
path: |
rust/kcl-wasm-lib/pkg/kcl_wasm_lib*
- uses: actions/upload-artifact@v4
with:
name: prepared-ts-rs-bindings
path: |
rust/kcl-lib/bindings/*

View File

@ -88,6 +88,7 @@ jobs:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
ZOO_HOST: https://api.dev.zoo.dev
RUST_BACKTRACE: full
RUST_MIN_STACK: 10485760000
- name: Commit differences
if: steps.path-changes.outputs.outside-kcl-samples == 'false' && steps.cargo-test-kcl-samples.outcome == 'failure'
shell: bash
@ -119,6 +120,7 @@ jobs:
# Configure nextest when it's run by insta (via just).
NEXTEST_PROFILE: ci
RUST_BACKTRACE: full
RUST_MIN_STACK: 10485760000
- name: Build and archive tests
run: |
cd rust
@ -155,7 +157,7 @@ jobs:
shell: bash
run: |
[ -e rust-toolchain.toml ] || cp rust/rust-toolchain.toml ./
- name: Install rust
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false # Configured below.
@ -182,6 +184,7 @@ jobs:
env:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
ZOO_HOST: https://api.dev.zoo.dev
RUST_MIN_STACK: 10485760000
- name: Upload results
if: always()
run: .github/ci-cd-scripts/upload-results.sh
@ -190,6 +193,55 @@ jobs:
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
run-internal-kcl-samples:
name: cargo test (internal-kcl-samples)
runs-on:
- runs-on=${{ github.run_id }}
- runner=32cpu-linux-x64
- extras=s3-cache
steps:
- uses: runs-on/action@v1
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.MODELING_APP_GH_APP_ID }}
private-key: ${{ secrets.MODELING_APP_GH_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
- name: Use correct Rust toolchain
shell: bash
run: |
[ -e rust-toolchain.toml ] || cp rust/rust-toolchain.toml ./
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false # Configured below.
- name: Start Vector
run: .github/ci-cd-scripts/start-vector-ubuntu.sh
env:
GH_ACTIONS_AXIOM_TOKEN: ${{ secrets.GH_ACTIONS_AXIOM_TOKEN }}
OS_NAME: ${{ env.OS_NAME }}
- uses: taiki-e/install-action@nextest
- name: Download internal KCL samples
run: git clone --depth=1 https://x-access-token:${{ secrets.GH_PAT_KCL_SAMPLES_INTERNAL }}@github.com/KittyCAD/kcl-samples-internal public/kcl-samples/internal
- name: Run tests
shell: bash
run: |-
cd rust/kcl-lib
cargo nextest run \
--retries=10 --no-fail-fast --features artifact-graph --profile=ci \
internal \
2>&1 | tee /tmp/github-actions.log
env:
TWENTY_TWENTY: overwrite
INSTA_UPDATE: always
EXPECTORATE: overwrite
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN_DEV}}
ZOO_HOST: https://api.dev.zoo.dev
MODELING_APP_INTERNAL_SAMPLES_SECRET: ${{secrets.MODELING_APP_INTERNAL_SAMPLES_SECRET}}
RUST_MIN_STACK: 10485760000
run-wasm-tests:
name: Run wasm tests
strategy:

View File

@ -21,14 +21,11 @@ on:
- '**.rs'
- .github/workflows/kcl-language-server.yml
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
@ -38,10 +35,9 @@ env:
MACOSX_DEPLOYMENT_TARGET: 10.15
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER: arm-linux-gnueabihf-gcc
jobs:
test:
name: vscode tests
name: kcl-language-server (vscode tests)
strategy:
fail-fast: false
matrix:
@ -77,22 +73,20 @@ jobs:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
code-target:
win32-x64
#- os: windows-latest
#target: i686-pc-windows-msvc
#code-target:
#win32-ia32
#- os: windows-latest
#target: aarch64-pc-windows-msvc
#code-target: win32-arm64
code-target: win32-x64
#- os: windows-latest
#target: i686-pc-windows-msvc
#code-target:
#win32-ia32
#- os: windows-latest
#target: aarch64-pc-windows-msvc
#code-target: win32-arm64
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
code-target:
linux-x64
#- os: ubuntu-latest
#target: aarch64-unknown-linux-musl
#code-target: linux-arm64
code-target: linux-x64
#- os: ubuntu-latest
#target: aarch64-unknown-linux-musl
#code-target: linux-arm64
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
code-target: linux-arm64
@ -105,41 +99,33 @@ jobs:
- os: macos-latest
target: aarch64-apple-darwin
code-target: darwin-arm64
name: build-release (${{ matrix.target }})
name: kcl-language-server build-release (${{ matrix.target }})
runs-on: ${{ matrix.os }}
container: ${{ matrix.container }}
env:
RA_TARGET: ${{ matrix.target }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: ${{ env.FETCH_DEPTH }}
- name: Use correct Rust toolchain
shell: bash
run: |
rm rust/rust-toolchain.toml
- name: Install rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: rust
components: rust-src
target: ${{ matrix.target }}
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Update apt repositories
if: matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'arm-unknown-linux-gnueabihf' || matrix.os == 'ubuntu-latest'
run: sudo apt-get update
- if: ${{ matrix.os == 'ubuntu-latest' }}
name: Install deps
shell: bash
@ -164,64 +150,53 @@ jobs:
zlib1g-dev
cargo install cross
- name: Install AArch64 target toolchain
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: sudo apt-get install gcc-aarch64-linux-gnu
- name: Install ARM target toolchain
if: matrix.target == 'arm-unknown-linux-gnueabihf'
run: sudo apt-get install gcc-arm-linux-gnueabihf
- name: build
run: |
cd rust
cargo kcl-language-server-release build --client-patch-version ${{ github.run_number }}
- name: Install dependencies
run: |
cd rust/kcl-language-server
# npm will symlink which will cause issues w tarballing later
yarn install
- name: Package Extension (release)
if: startsWith(github.event.ref, 'refs/tags/')
run: |
cd rust/kcl-language-server
npx vsce package --yarn -o "../build/kcl-language-server-${{ matrix.code-target }}.vsix" --target ${{ matrix.code-target }}
- name: Package Extension (nightly)
if: startsWith(github.event.ref, 'refs/tags/') == false
run: |
cd rust/kcl-language-server
npx vsce package --yarn -o "../build/kcl-language-server-${{ matrix.code-target }}.vsix" --target ${{ matrix.code-target }} --pre-release
- name: remove server
if: matrix.target == 'x86_64-unknown-linux-gnu'
run: |
cd rust/kcl-language-server
rm -rf server
- name: Package Extension (no server, release)
if: matrix.target == 'x86_64-unknown-linux-gnu' && startsWith(github.event.ref, 'refs/tags/')
run: |
cd rust/kcl-language-server
npx vsce package --yarn -o ../build/kcl-language-server-no-server.vsix
- name: Package Extension (no server, nightly)
if: matrix.target == 'x86_64-unknown-linux-gnu' && startsWith(github.event.ref, 'refs/tags/') == false
run: |
cd rust/kcl-language-server
npx vsce package --yarn -o ../build/kcl-language-server-no-server.vsix --pre-release
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: release-${{ matrix.target }}
path: ./rust/build
build-release-x86_64-unknown-linux-musl:
name: build-release (x86_64-unknown-linux-musl)
name: kcl-language-server build-release (x86_64-unknown-linux-musl)
runs-on: ubuntu-latest
env:
RA_TARGET: x86_64-unknown-linux-musl
@ -231,7 +206,6 @@ jobs:
image: alpine:latest
volumes:
- /usr/local/cargo/registry:/usr/local/cargo/registry
steps:
- name: Install dependencies
run: |
@ -245,55 +219,46 @@ jobs:
nodejs \
npm \
yarn
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: ${{ env.FETCH_DEPTH }}
- name: Use correct Rust toolchain
shell: bash
run: |
rm rust/rust-toolchain.toml
- name: Install rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: rust
components: rust-src
target: ${{ matrix.target }}
- name: build
run: |
cd rust
cargo kcl-language-server-release build --client-patch-version ${{ github.run_number }}
- name: Install dependencies
run: |
cd rust/kcl-language-server
# npm will symlink which will cause issues w tarballing later
yarn install
- name: Package Extension (release)
if: startsWith(github.event.ref, 'refs/tags/')
run: |
cd rust/kcl-language-server
npx vsce package --yarn -o "../build/kcl-language-server-alpine-x64.vsix" --target alpine-x64
- name: Package Extension (release)
if: startsWith(github.event.ref, 'refs/tags/') == false
run: |
cd rust/kcl-language-server
npx vsce package --yarn -o "../build/kcl-language-server-alpine-x64.vsix" --target alpine-x64 --pre-release
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: release-x86_64-unknown-linux-musl
path: ./rust/build
publish:
name: publish
name: kcl-language-server (publish)
runs-on: ubuntu-latest
needs: ["build-release", "build-release-x86_64-unknown-linux-musl"]
if: startsWith(github.event.ref, 'refs/tags')
@ -301,22 +266,17 @@ jobs:
contents: write
steps:
- run: echo "TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- run: 'echo "TAG: $TAG"'
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: ${{ env.FETCH_DEPTH }}
- name: Install Nodejs
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- run: echo "HEAD_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
- run: 'echo "HEAD_SHA: $HEAD_SHA"'
- uses: actions/download-artifact@v4
with:
name: release-aarch64-apple-darwin
@ -344,33 +304,29 @@ jobs:
- uses: actions/download-artifact@v4
with:
name: release-x86_64-pc-windows-msvc
path:
rust/build
#- uses: actions/download-artifact@v4
#with:
#name: release-i686-pc-windows-msvc
#path:
#build
#- uses: actions/download-artifact@v4
#with:
#name: release-aarch64-pc-windows-msvc
#path: rust/build
path: rust/build
#- uses: actions/download-artifact@v4
#with:
#name: release-i686-pc-windows-msvc
#path:
#build
#- uses: actions/download-artifact@v4
#with:
#name: release-aarch64-pc-windows-msvc
#path: rust/build
- run: ls -al ./rust/build
- name: Publish Release
uses: ./.github/actions/github-release
with:
files: "rust/build/*"
name: ${{ env.TAG }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: move files to dir for upload
shell: bash
run: |
cd rust
mkdir -p releases/language-server/${{ env.TAG }}
cp -r build/* releases/language-server/${{ env.TAG }}
- name: "Authenticate to Google Cloud"
uses: "google-github-actions/auth@v2.1.8"
with:
@ -385,15 +341,12 @@ jobs:
with:
path: rust/releases
destination: dl.kittycad.io
- run: rm rust/build/kcl-language-server-no-server.vsix
- name: Publish Extension (Code Marketplace, release)
# token from https://dev.azure.com/kcl-language-server/
run: |
cd rust/kcl-language-server
npx vsce publish --pat ${{ secrets.VSCE_PAT }} --packagePath ../build/kcl-language-server-*.vsix
- name: Publish Extension (OpenVSX, release)
run: |
cd rust/kcl-language-server

View File

@ -4,7 +4,6 @@
# maturin generate-ci github
#
name: kcl-python-bindings
on:
push:
branches:
@ -27,16 +26,14 @@ on:
- '**.rs'
- .github/workflows/kcl-python-bindings.yml
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
linux-x86_64:
name: kcl-python-bindings (linux-x86_64)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -58,8 +55,8 @@ jobs:
with:
name: wheels-linux-x86_64
path: rust/kcl-python-bindings/dist
windows:
name: kcl-python-bindings (windows)
runs-on: windows-16-cores
strategy:
matrix:
@ -84,8 +81,8 @@ jobs:
with:
name: wheels-windows-${{ matrix.target }}
path: rust/kcl-python-bindings/dist
macos:
name: kcl-python-bindings (macos)
runs-on: macos-latest
strategy:
matrix:
@ -110,8 +107,8 @@ jobs:
with:
name: wheels-macos-${{ matrix.target }}
path: rust/kcl-python-bindings/dist
test:
name: kcl-python-bindings (test)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -127,8 +124,8 @@ jobs:
env:
KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
ZOO_HOST: https://api.dev.zoo.dev
sdist:
name: kcl-python-bindings (sdist)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -136,10 +133,10 @@ jobs:
uses: astral-sh/setup-uv@v5
- name: Install codespell
run: |
uv venv .venv
echo "VIRTUAL_ENV=.venv" >> $GITHUB_ENV
echo "$PWD/.venv/bin" >> $GITHUB_PATH
uv pip install pip --upgrade
uv venv .venv
echo "VIRTUAL_ENV=.venv" >> $GITHUB_ENV
echo "$PWD/.venv/bin" >> $GITHUB_PATH
uv pip install pip --upgrade
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
@ -151,7 +148,6 @@ jobs:
with:
name: wheels-sdist
path: rust/kcl-python-bindings/dist
release:
name: Release
runs-on: ubuntu-latest
@ -168,11 +164,11 @@ jobs:
uses: astral-sh/setup-uv@v5
- name: do uv things
run: |
cd rust/kcl-python-bindings
uv venv .venv
echo "VIRTUAL_ENV=.venv" >> $GITHUB_ENV
echo "$PWD/.venv/bin" >> $GITHUB_PATH
uv pip install pip --upgrade
cd rust/kcl-python-bindings
uv venv .venv
echo "VIRTUAL_ENV=.venv" >> $GITHUB_ENV
echo "$PWD/.venv/bin" >> $GITHUB_PATH
uv pip install pip --upgrade
- name: Publish to PyPI
uses: PyO3/maturin-action@v1
env:

View File

@ -28,53 +28,7 @@ jobs:
- run: npm run fmt:check
npm-build-wasm:
# Build the wasm blob once on the fastest runner.
runs-on: runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Use correct Rust toolchain
shell: bash
run: |
[ -e rust-toolchain.toml ] || cp rust/rust-toolchain.toml ./
- name: Install rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache: false # Configured below.
- uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc
with:
tool: wasm-pack
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: './rust'
- name: Build Wasm
shell: bash
run: npm run build:wasm
- uses: actions/upload-artifact@v4
with:
name: prepared-wasm
path: |
rust/kcl-wasm-lib/pkg/kcl_wasm_lib*
- uses: actions/upload-artifact@v4
with:
name: prepared-ts-rs-bindings
path: |
rust/kcl-lib/bindings/*
uses: ./.github/workflows/build-wasm.yml
npm-tsc:
runs-on: ubuntu-latest
@ -173,122 +127,3 @@ jobs:
uses: actions/checkout@v4
- name: Run codespell
uses: crate-ci/typos@v1.32.0
npm-unit-test-kcl-samples:
runs-on: ubuntu-latest
needs: npm-build-wasm
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- run: npm install
- uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc
with:
tool: wasm-pack
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Copy prepared wasm
run: |
ls -R prepared-wasm
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
mkdir rust/kcl-wasm-lib/pkg
cp prepared-wasm/kcl_wasm_lib* rust/kcl-wasm-lib/pkg
- name: Copy prepared ts-rs bindings
run: |
ls -R prepared-ts-rs-bindings
mkdir rust/kcl-lib/bindings
cp -r prepared-ts-rs-bindings/* rust/kcl-lib/bindings/
- run: npm run simpleserver:bg
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
- name: Install Chromium Browser
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: npm run playwright install chromium --with-deps
- name: Download internal KCL samples
run: git clone --depth=1 https://x-access-token:${{ secrets.GH_PAT_KCL_SAMPLES_INTERNAL }}@github.com/KittyCAD/kcl-samples-internal public/kcl-samples/internal
- name: Regenerate KCL samples manifest
run: cd rust/kcl-lib && EXPECTORATE=overwrite cargo test generate_manifest
- name: Check public and internal KCL samples
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: npm run test:unit:kcl-samples
env:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
npm-unit-test:
runs-on: ubuntu-latest
needs: npm-build-wasm
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- run: npm install
- uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc
with:
tool: wasm-pack
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Copy prepared wasm
run: |
ls -R prepared-wasm
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
mkdir rust/kcl-wasm-lib/pkg
cp prepared-wasm/kcl_wasm_lib* rust/kcl-wasm-lib/pkg
- name: Copy prepared ts-rs bindings
run: |
ls -R prepared-ts-rs-bindings
mkdir rust/kcl-lib/bindings
cp -r prepared-ts-rs-bindings/* rust/kcl-lib/bindings/
- run: npm run simpleserver:bg
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
- name: Install Chromium Browser
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: npm run playwright install chromium --with-deps
- name: Run unit tests
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: xvfb-run -a npm run test:unit
env:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Check for changes
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
id: git-check
run: |
git add src/lang/std/artifactMapGraphs
if git status src/lang/std/artifactMapGraphs | grep -q "Changes to be committed"
then echo "modified=true" >> $GITHUB_OUTPUT
else echo "modified=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes, if any
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' && steps.git-check.outputs.modified == 'true' }}
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git fetch origin
echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }}
# TODO when webkit works on ubuntu remove the os part of the commit message
git commit -am "Look at this (photo)Graph *in the voice of Nickelback*" || true
git push
git push origin ${{ github.head_ref }}

124
.github/workflows/unit-tests.yml vendored Normal file
View File

@ -0,0 +1,124 @@
name: Unit Tests
on:
pull_request:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: write
pull-requests: write
actions: read
jobs:
npm-build-wasm:
uses: ./.github/workflows/build-wasm.yml
npm-test-unit:
runs-on: ubuntu-latest
needs: npm-build-wasm
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- run: npm install
- uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc
with:
tool: wasm-pack
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Copy prepared wasm
run: |
ls -R prepared-wasm
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
mkdir rust/kcl-wasm-lib/pkg
cp prepared-wasm/kcl_wasm_lib* rust/kcl-wasm-lib/pkg
- name: Copy prepared ts-rs bindings
run: |
ls -R prepared-ts-rs-bindings
mkdir rust/kcl-lib/bindings
cp -r prepared-ts-rs-bindings/* rust/kcl-lib/bindings/
- run: npm run simpleserver:bg
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
- name: Install Chromium Browser
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: npm run playwright install chromium --with-deps
- name: Run unit tests
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
run: xvfb-run -a npm run test:unit
env:
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
- name: Check for changes
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' }}
id: git-check
run: |
git add src/lang/std/artifactMapGraphs
if git status src/lang/std/artifactMapGraphs | grep -q "Changes to be committed"
then echo "modified=true" >> $GITHUB_OUTPUT
else echo "modified=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes, if any
if: ${{ github.event_name != 'release' && github.event_name != 'schedule' && steps.git-check.outputs.modified == 'true' }}
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
git fetch origin
echo ${{ github.head_ref }}
git checkout ${{ github.head_ref }}
# TODO when webkit works on ubuntu remove the os part of the commit message
git commit -am "Look at this (photo)Graph *in the voice of Nickelback*" || true
git push
git push origin ${{ github.head_ref }}
npm-test-unit-components:
runs-on: ubuntu-latest
needs: npm-build-wasm
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
- run: npm install
- uses: taiki-e/install-action@d4635f2de61c8b8104d59cd4aede2060638378cc
with:
tool: wasm-pack
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Copy prepared wasm
run: |
ls -R prepared-wasm
cp prepared-wasm/kcl_wasm_lib_bg.wasm public
mkdir rust/kcl-wasm-lib/pkg
cp prepared-wasm/kcl_wasm_lib* rust/kcl-wasm-lib/pkg
- name: Copy prepared ts-rs bindings
run: |
ls -R prepared-ts-rs-bindings
mkdir rust/kcl-lib/bindings
cp -r prepared-ts-rs-bindings/* rust/kcl-lib/bindings/
- name: Run component tests
run: npm run test:unit:components

2
.gitignore vendored
View File

@ -58,6 +58,8 @@ trace.zip
/public/kcl-samples/.github
/public/kcl-samples/screenshots/main.kcl
/public/kcl-samples/step/main.kcl
/public/kcl-samples/internal
/rust/kcl-lib/tests/kcl_samples/internal
/test-results/
/playwright-report/
/blob-report/

View File

@ -114,7 +114,6 @@ test-unit: install ## Run the unit tests
npm run test:unit:components
@ curl -fs localhost:3000 >/dev/null || ( echo "Error: localhost:3000 not available, 'make run-web' first" && exit 1 )
npm run test:unit
npm run test:unit:kcl-samples
.PHONY: test-e2e
test-e2e: test-e2e-$(TARGET)

View File

@ -42,8 +42,6 @@ The 3D view in Design Studio is just a video stream from our hosted geometry eng
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.
If you'd like to try out upcoming changes sooner, you can also download those from our [nightly releases](https://zoo.dev/modeling-app/download/nightly) page.
## 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.

View File

@ -21,7 +21,7 @@ extend-exclude = [
]
[default.extend-words]
metalness = "metalness" # appearance API
metalness = "metalness" # appearance API
Hom = "Hom" # short for homogenous
typ = "typ" # used to declare a variable named 'type' which is a reserved keyword in Rust
ue = "ue" # short for UnaryExpression
@ -29,6 +29,7 @@ THRE = "THRE" # Weird bug that wrongly detects THREEjs as a typo
nwo = "nwo" # don't know what this is about tbh
"ot" = "ot" # some abbreviation, idk what
"oe" = "oe" # some abbreviation, idk what
"colinear" = "colinear" # some engine shit, kidding
[default]
extend-ignore-identifiers-re = [

View File

@ -177,7 +177,7 @@ You can also import the whole module. This is useful if you want to use the
result of a module as a variable, like a part.
```norun
import "tests/inputs/cube.kcl" as cube
import "cube.kcl"
cube
|> translate(x=10)
```
@ -241,7 +241,7 @@ If you want to have multiple instances of the same object, you can use the
[`clone`](/docs/kcl/clone) function. This will render a new instance of the object in memory.
```norun
import cube from "tests/inputs/cube.kcl"
import cube from "cube.kcl"
cube
|> translate(x=10)
@ -257,7 +257,7 @@ separate objects in memory, and can be manipulated independently.
Here is an example with a file from another CAD system:
```kcl
import "tests/inputs/cube.step" as cube
import "tests/inputs/cube.step"
cube
|> translate(x=10)

View File

@ -12,7 +12,7 @@ reduce(
@array: [any],
initial: any,
f: fn(any, accum: any): any,
): [any]
): any
```
Take a starting value. Then, for each element of an array, calculate the next value,
@ -28,7 +28,7 @@ using the previous value and the element.
### Returns
[`[any]`](/docs/kcl-std/types/std-types-any)
[`any`](/docs/kcl-std/types/std-types-any)
### Examples

View File

@ -11,7 +11,7 @@ Compute the length of the given leg.
legLen(
hypotenuse: number(Length),
leg: number(Length),
): number(deg)
): number(Length)
```
@ -25,7 +25,7 @@ legLen(
### Returns
[`number(deg)`](/docs/kcl-std/types/std-types-number) - A number.
[`number(Length)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples

View File

@ -17,7 +17,7 @@ revolve(
bidirectionalAngle?: number(Angle),
tagStart?: tag,
tagEnd?: tag,
): Solid
): [Solid; 1+]
```
This, like extrude, is able to create a 3-dimensional solid from a
@ -46,7 +46,7 @@ revolved around the same axis.
### Returns
[`Solid`](/docs/kcl-std/types/std-types-Solid) - A solid is a collection of extruded surfaces.
[`[Solid; 1+]`](/docs/kcl-std/types/std-types-Solid)
### Examples

View File

@ -14,8 +14,6 @@ mirror2d(
): Sketch
```
Only works on unclosed sketches for now.
Mirror occurs around a local sketch axis rather than a global axis.
### Arguments

View File

@ -12,8 +12,8 @@ patternCircular2d(
@sketchSet: [Sketch],
instances: number,
center: Point2d,
arcDegrees: number,
rotateDuplicates: bool,
arcDegrees?: number,
rotateDuplicates?: bool,
useOriginal?: bool,
): [Sketch]
```
@ -27,8 +27,8 @@ patternCircular2d(
| `sketchSet` | [`[Sketch]`](/docs/kcl-std/types/std-types-Sketch) | Which sketch(es) to pattern | 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 |
| `center` | [`Point2d`](/docs/kcl-std/types/std-types-Point2d) | The center about which to make the pattern. This is a 2D vector. | Yes |
| `arcDegrees` | [`number`](/docs/kcl-std/types/std-types-number) | The arc angle (in degrees) to place the repetitions. Must be greater than 0. | Yes |
| `rotateDuplicates` | [`bool`](/docs/kcl-std/types/std-types-bool) | Whether or not to rotate the duplicates as they are copied. | Yes |
| `arcDegrees` | [`number`](/docs/kcl-std/types/std-types-number) | The arc angle (in degrees) to place the repetitions. Must be greater than 0. Defaults to 360. | No |
| `rotateDuplicates` | [`bool`](/docs/kcl-std/types/std-types-bool) | Whether or not to rotate the duplicates as they are copied. Defaults to true. | 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

View File

@ -13,8 +13,8 @@ patternCircular3d(
instances: number,
axis: [number],
center: Point3d,
arcDegrees: number,
rotateDuplicates: bool,
arcDegrees?: number,
rotateDuplicates?: bool,
useOriginal?: bool,
): [Solid]
```
@ -29,8 +29,8 @@ patternCircular3d(
| `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 |
| `axis` | [`[number]`](/docs/kcl-std/types/std-types-number) | The axis around which to make the pattern. This is a 3D vector | Yes |
| `center` | [`Point3d`](/docs/kcl-std/types/std-types-Point3d) | The center about which to make the pattern. This is a 3D vector. | Yes |
| `arcDegrees` | [`number`](/docs/kcl-std/types/std-types-number) | The arc angle (in degrees) to place the repetitions. Must be greater than 0. | Yes |
| `rotateDuplicates` | [`bool`](/docs/kcl-std/types/std-types-bool) | Whether or not to rotate the duplicates as they are copied. | Yes |
| `arcDegrees` | [`number`](/docs/kcl-std/types/std-types-number) | The arc angle (in degrees) to place the repetitions. Must be greater than 0. Defaults to 360. | No |
| `rotateDuplicates` | [`bool`](/docs/kcl-std/types/std-types-bool) | Whether or not to rotate the duplicates as they are copied. Defaults to true. | 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -127364,9 +127364,10 @@
"type": "number",
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "double",
"title": "Nullable_double",
"type": "number",
"format": "double",
"nullable": true,
"definitions": {
"Sketch": {
"type": "object",
@ -128959,9 +128960,8 @@
}
}
},
"required": true,
"includeInSnippet": true,
"description": "The arc angle (in degrees) to place the repetitions. Must be greater than 0.",
"required": false,
"description": "The arc angle (in degrees) to place the repetitions. Must be greater than 0. Defaults to 360.",
"labelRequired": true
},
{
@ -128969,8 +128969,9 @@
"type": "bool",
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "Boolean",
"title": "Nullable_Boolean",
"type": "boolean",
"nullable": true,
"definitions": {
"Sketch": {
"type": "object",
@ -130563,9 +130564,8 @@
}
}
},
"required": true,
"includeInSnippet": true,
"description": "Whether or not to rotate the duplicates as they are copied.",
"required": false,
"description": "Whether or not to rotate the duplicates as they are copied. Defaults to true.",
"labelRequired": true
},
{
@ -140234,9 +140234,10 @@
"type": "number",
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "double",
"title": "Nullable_double",
"type": "number",
"format": "double",
"nullable": true,
"definitions": {
"Solid": {
"type": "object",
@ -141829,9 +141830,8 @@
}
}
},
"required": true,
"includeInSnippet": true,
"description": "The arc angle (in degrees) to place the repetitions. Must be greater than 0.",
"required": false,
"description": "The arc angle (in degrees) to place the repetitions. Must be greater than 0. Defaults to 360.",
"labelRequired": true
},
{
@ -141839,8 +141839,9 @@
"type": "bool",
"schema": {
"$schema": "https://spec.openapis.org/oas/3.0/schema/2019-04-02#/definitions/Schema",
"title": "Boolean",
"title": "Nullable_Boolean",
"type": "boolean",
"nullable": true,
"definitions": {
"Solid": {
"type": "object",
@ -143433,9 +143434,8 @@
}
}
},
"required": true,
"includeInSnippet": true,
"description": "Whether or not to rotate the duplicates as they are copied.",
"required": false,
"description": "Whether or not to rotate the duplicates as they are copied. Defaults to true.",
"labelRequired": true
},
{
@ -156307,7 +156307,11 @@
"deprecated": false,
"examples": [
[
"exampleSketch = startSketchOn(XZ)\n |> circle(center = [0, 0], radius = 1)\n |> patternLinear2d(axis = [1, 0], instances = 7, distance = 4)\n\nexample = extrude(exampleSketch, length = 1)",
"// / Pattern using a named axis.\n\n\nexampleSketch = startSketchOn(XZ)\n |> circle(center = [0, 0], radius = 1)\n |> patternLinear2d(axis = X, instances = 7, distance = 4)\n\nexample = extrude(exampleSketch, length = 1)",
false
],
[
"// / Pattern using a raw axis.\n\n\nexampleSketch = startSketchOn(XZ)\n |> circle(center = [0, 0], radius = 1)\n |> patternLinear2d(axis = [1, 0], instances = 7, distance = 4)\n\nexample = extrude(exampleSketch, length = 1)",
false
]
]
@ -165963,7 +165967,11 @@
"deprecated": false,
"examples": [
[
"exampleSketch = startSketchOn(XZ)\n |> startProfile(at = [0, 0])\n |> line(end = [0, 2])\n |> line(end = [3, 1])\n |> line(end = [0, -4])\n |> close()\n\nexample = extrude(exampleSketch, length = 1)\n |> patternLinear3d(axis = [1, 0, 1], instances = 7, distance = 6)",
"// / Pattern using a named axis.\n\n\nexampleSketch = startSketchOn(XZ)\n |> startProfile(at = [0, 0])\n |> line(end = [0, 2])\n |> line(end = [3, 1])\n |> line(end = [0, -4])\n |> close()\n\nexample = extrude(exampleSketch, length = 1)\n |> patternLinear3d(axis = X, instances = 7, distance = 6)",
false
],
[
"// / Pattern using a raw axis.\n\n\nexampleSketch = startSketchOn(XZ)\n |> startProfile(at = [0, 0])\n |> line(end = [0, 2])\n |> line(end = [3, 1])\n |> line(end = [0, -4])\n |> close()\n\nexample = extrude(exampleSketch, length = 1)\n |> patternLinear3d(axis = [1, 0, 1], instances = 7, distance = 6)",
false
],
[

View File

@ -235,6 +235,48 @@ extrude001 = extrude(sketch001, length = 5)`
.first()
).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(

View File

@ -36,27 +36,29 @@ test.describe('Command bar tests', () => {
await u.closeDebugPanel()
// Click the line of code for xLine.
await page.getByText(`close()`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.getByText(`startProfile(at = [-10, -10])`).click()
// Wait for the selection to register (TODO: we need a definitive way to wait for this)
await page.waitForTimeout(200)
await toolbar.extrudeButton.click()
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Extrude',
currentArgKey: 'sketches',
currentArgValue: '',
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: {
Sketches: '',
Profiles: '1 profile',
Length: '',
},
highlightedHeaderArg: 'sketches',
highlightedHeaderArg: 'length',
})
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
commandName: 'Extrude',
headerArguments: {
Sketches: '1 segment',
Profiles: '1 profile',
Length: '5',
},
})
@ -286,7 +288,7 @@ test.describe('Command bar tests', () => {
await cmdBar.cmdOptions.getByText('Extrude').click()
// Assert that we're on the selection step
await expect(page.getByRole('button', { name: 'sketches' })).toBeDisabled()
await expect(page.getByRole('button', { name: 'Profiles' })).toBeDisabled()
// Select a face
await page.mouse.move(700, 200)
await page.mouse.click(700, 200)
@ -399,7 +401,6 @@ test.describe('Command bar tests', () => {
sortBy: 'last-modified-desc',
})
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
await test.step(`Submit the command`, async () => {
@ -410,7 +411,7 @@ test.describe('Command bar tests', () => {
currentArgValue: '',
headerArguments: {
Method: '',
Name: 'test',
Name: 'main.kcl',
Code: '1 line',
},
highlightedHeaderArg: 'method',
@ -421,7 +422,7 @@ test.describe('Command bar tests', () => {
commandName: 'Import file from URL',
headerArguments: {
Method: 'New project',
Name: 'test',
Name: 'main.kcl',
Code: '1 line',
},
})
@ -463,7 +464,6 @@ test.describe('Command bar tests', () => {
sortBy: 'last-modified-desc',
})
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
await test.step(`Submit the command`, async () => {
@ -474,7 +474,7 @@ test.describe('Command bar tests', () => {
currentArgValue: '',
headerArguments: {
Method: '',
Name: 'test',
Name: 'main.kcl',
Code: '1 line',
},
highlightedHeaderArg: 'method',
@ -487,7 +487,7 @@ test.describe('Command bar tests', () => {
currentArgValue: '',
headerArguments: {
Method: 'Existing project',
Name: 'test',
Name: 'main.kcl',
ProjectName: '',
Code: '1 line',
},
@ -500,7 +500,7 @@ test.describe('Command bar tests', () => {
headerArguments: {
Method: 'Existing project',
ProjectName: 'testProjectDir',
Name: 'test',
Name: 'main.kcl',
Code: '1 line',
},
})
@ -510,7 +510,7 @@ test.describe('Command bar tests', () => {
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
await editor.expectEditor.toContain('extrusionDistance = 12')
await toolbar.openPane('files')
await toolbar.expectFileTreeState(['main.kcl', 'test.kcl'])
await toolbar.expectFileTreeState(['main-1.kcl', 'main.kcl'])
})
})
@ -661,4 +661,27 @@ c = 3 + a`
`a = 5b = a * amyParameter001 = ${newValue}c = 3 + a`
)
})
test('Command palette can be opened via query parameter', async ({
page,
homePage,
cmdBar,
}) => {
await page.goto(`${page.url()}/?cmd=app.theme&groupId=settings`)
await homePage.expectState({
projectCards: [],
sortBy: 'last-modified-desc',
})
await cmdBar.expectState({
stage: 'arguments',
commandName: 'Settings · app · theme',
currentArgKey: 'value',
currentArgValue: '',
headerArguments: {
Level: 'user',
Value: '',
},
highlightedHeaderArg: 'value',
})
})
})

View File

@ -1131,14 +1131,15 @@ sketch001 = startSketchOn(XZ)
await page.waitForTimeout(100)
await page.getByText('startProfile(at = [4.61, -14.01])').click()
// Wait for the selection to register (TODO: we need a definitive way to wait for this)
await page.waitForTimeout(200)
await toolbar.extrudeButton.click()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: {
Sketches: '1 face',
Profiles: '1 profile',
Length: '',
},
highlightedHeaderArg: 'length',
@ -1148,7 +1149,7 @@ sketch001 = startSketchOn(XZ)
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Sketches: '1 face',
Profiles: '1 profile',
Length: '5',
},
commandName: 'Extrude',
@ -1354,7 +1355,9 @@ sketch001 = startSketchOn(XZ)
const u = await getUtils(page)
const projectLink = page.getByRole('link', { name: 'cube' })
const gizmo = page.locator('[aria-label*=gizmo]')
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
const resetCameraButton = page.getByRole('button', {
name: 'Reset view',
})
const locationToHaveColor = async (
position: { x: number; y: number },
color: [number, number, number]

View File

@ -146,9 +146,7 @@ export class CmdBarFixture {
await this.cmdBarOpenBtn.click()
await expect(this.page.getByPlaceholder('Search commands')).toBeVisible()
if (selectCmd === 'promptToEdit') {
const promptEditCommand = this.page.getByText(
'Use Zoo AI to edit your parts and code.'
)
const promptEditCommand = this.selectOption({ name: 'Text-to-CAD Edit' })
await expect(promptEditCommand.first()).toBeVisible()
await promptEditCommand.first().scrollIntoViewIfNeeded()
await promptEditCommand.first().click()

View File

@ -121,11 +121,13 @@ export class HomePageFixture {
await projectCard.click()
}
goToModelingScene = async (name: string = 'testDefault') => {
/** Returns the project name in case caller has used the default and needs it */
goToModelingScene = async (name = 'testDefault') => {
// On web this is a no-op. There is no project view.
if (process.env.PLATFORM === 'web') return
if (process.env.PLATFORM === 'web') return ''
await this.createAndGoToProject(name)
return name
}
isNativeFileMenuCreated = async () => {

View File

@ -197,18 +197,6 @@ test.describe(
await clickElectronNativeMenuById(tronApp, 'File.Export current part')
await cmdBar.expectCommandName('Export')
})
await test.step('Modeling.File.Share part via Zoo link', async () => {
await page.waitForTimeout(250)
await clickElectronNativeMenuById(
tronApp,
'File.Share part via Zoo link'
)
const textToCheck =
'Link copied to clipboard. Anyone who clicks this link will get a copy of this file. Share carefully!'
// Check if text appears anywhere in the page
const isTextVisible = page.getByText(textToCheck)
await expect(isTextVisible).toBeVisible({ timeout: 10000 })
})
await test.step('Modeling.File.Preferences.Project settings', async () => {
await page.waitForTimeout(250)
await clickElectronNativeMenuById(
@ -264,7 +252,7 @@ test.describe(
tronApp,
'Edit.Modify with Zoo Text-To-CAD'
)
await cmdBar.expectCommandName('Prompt-to-edit')
await cmdBar.expectCommandName('Text-to-CAD Edit')
})
await test.step('Modeling.Edit.Edit parameter', async () => {
await page.waitForTimeout(250)
@ -530,7 +518,7 @@ test.describe(
'Design.Create with Zoo Text-To-CAD'
)
await cmdBar.toBeOpened()
await cmdBar.expectCommandName('Text to CAD')
await cmdBar.expectCommandName('Text-to-CAD Create')
})
await test.step('Modeling.Design.Modify with Zoo Text-To-CAD', async () => {
@ -540,7 +528,7 @@ test.describe(
'Design.Modify with Zoo Text-To-CAD'
)
await cmdBar.toBeOpened()
await cmdBar.expectCommandName('Prompt-to-edit')
await cmdBar.expectCommandName('Text-to-CAD Edit')
})
await test.step('Modeling.Help.KCL code samples', async () => {

View File

@ -74,20 +74,11 @@ test.describe('Point-and-click tests', () => {
await test.step('do extrude flow and check extrude code is added to editor', async () => {
await toolbar.extrudeButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: { Sketches: '', Length: '' },
highlightedHeaderArg: 'sketches',
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: { Sketches: '1 face', Length: '' },
headerArguments: { Profiles: '1 profile', Length: '' },
highlightedHeaderArg: 'length',
commandName: 'Extrude',
})
@ -98,7 +89,7 @@ test.describe('Point-and-click tests', () => {
await cmdBar.expectState({
stage: 'review',
headerArguments: { Sketches: '1 face', Length: '5' },
headerArguments: { Profiles: '1 profile', Length: '5' },
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
@ -1634,15 +1625,15 @@ sketch002 = startSketchOn(plane001)
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: { Sketches: '' },
highlightedHeaderArg: 'sketches',
headerArguments: { Profiles: '' },
highlightedHeaderArg: 'Profiles',
commandName: 'Loft',
})
await selectSketches()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Sketches: '2 faces' },
headerArguments: { Profiles: '2 profiles' },
commandName: 'Loft',
})
await cmdBar.submit()
@ -1654,18 +1645,9 @@ sketch002 = startSketchOn(plane001)
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
await toolbar.loftButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: { Sketches: '' },
highlightedHeaderArg: 'sketches',
commandName: 'Loft',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Sketches: '2 faces' },
headerArguments: { Profiles: '2 profiles' },
commandName: 'Loft',
})
await cmdBar.submit()
@ -1830,10 +1812,10 @@ sketch002 = startSketchOn(XZ)
currentArgValue: '',
headerArguments: {
Sectional: '',
Sketches: '',
Profiles: '',
Path: '',
},
highlightedHeaderArg: 'sketches',
highlightedHeaderArg: 'Profiles',
stage: 'arguments',
})
await clickOnSketch1()
@ -1844,7 +1826,7 @@ sketch002 = startSketchOn(XZ)
currentArgValue: '',
headerArguments: {
Sectional: '',
Sketches: '1 face',
Profiles: '1 profile',
Path: '',
},
highlightedHeaderArg: 'path',
@ -1857,7 +1839,7 @@ sketch002 = startSketchOn(XZ)
currentArgValue: '',
headerArguments: {
Sectional: '',
Sketches: '1 face',
Profiles: '1 profile',
Path: '',
},
highlightedHeaderArg: 'path',
@ -1867,7 +1849,7 @@ sketch002 = startSketchOn(XZ)
await cmdBar.expectState({
commandName: 'Sweep',
headerArguments: {
Sketches: '1 face',
Profiles: '1 profile',
Path: '1 segment',
Sectional: '',
},
@ -1968,10 +1950,10 @@ profile001 = ${circleCode}`
currentArgValue: '',
headerArguments: {
Sectional: '',
Sketches: '',
Profiles: '',
Path: '',
},
highlightedHeaderArg: 'sketches',
highlightedHeaderArg: 'Profiles',
stage: 'arguments',
})
await editor.scrollToText(circleCode)
@ -1983,7 +1965,7 @@ profile001 = ${circleCode}`
currentArgValue: '',
headerArguments: {
Sectional: '',
Sketches: '1 face',
Profiles: '1 profile',
Path: '',
},
highlightedHeaderArg: 'path',
@ -1997,7 +1979,7 @@ profile001 = ${circleCode}`
currentArgValue: '',
headerArguments: {
Sectional: '',
Sketches: '1 face',
Profiles: '1 profile',
Path: '',
},
highlightedHeaderArg: 'path',
@ -2007,7 +1989,7 @@ profile001 = ${circleCode}`
await cmdBar.expectState({
commandName: 'Sweep',
headerArguments: {
Sketches: '1 face',
Profiles: '1 profile',
Path: '1 helix',
Sectional: '',
},
@ -2106,18 +2088,6 @@ extrude001 = extrude(sketch001, length = -12)
await test.step(`Apply fillet to the preselected edge`, async () => {
await page.waitForTimeout(100)
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({
commandName: 'Fillet',
highlightedHeaderArg: 'radius',
@ -2647,18 +2617,6 @@ extrude001 = extrude(profile001, length = 5)
await test.step(`Apply fillet`, async () => {
await page.waitForTimeout(100)
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({
commandName: 'Fillet',
highlightedHeaderArg: 'radius',
@ -2764,19 +2722,6 @@ extrude001 = extrude(sketch001, length = -12)
await test.step(`Apply chamfer to the preselected edge`, async () => {
await page.waitForTimeout(100)
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({
commandName: 'Chamfer',
highlightedHeaderArg: 'length',
@ -3260,8 +3205,6 @@ extrude001 = extrude(sketch001, length = 30)
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
await toolbar.shellButton.click()
await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
@ -3691,16 +3634,17 @@ tag=$rectangleSegmentC002,
await scene.settled(cmdBar)
// select line of code
const codeToSelection = `segAng(rectangleSegmentA002) - 90,`
const codeToSelection = `startProfile(at = [-66.77, 84.81])`
// revolve
await editor.scrollToText(codeToSelection)
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 cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
const newCodeToFind = `revolve001 = revolve(sketch002, angle = 360, axis = X)`
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
@ -4629,24 +4573,12 @@ path001 = startProfile(sketch001, at = [0, 0])
await test.step('Go through command bar flow', async () => {
await toolbar.extrudeButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sketches: '',
Length: '',
},
highlightedHeaderArg: 'sketches',
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: {
Sketches: '2 faces',
Profiles: '2 profiles',
Length: '',
},
highlightedHeaderArg: 'length',
@ -4657,7 +4589,7 @@ path001 = startProfile(sketch001, at = [0, 0])
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Sketches: '2 faces',
Profiles: '2 profiles',
Length: '1',
},
commandName: 'Extrude',
@ -4723,25 +4655,12 @@ path001 = startProfile(sketch001, at = [0, 0])
await test.step('Go through command bar flow', async () => {
await toolbar.sweepButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sketches: '',
Path: '',
Sectional: '',
},
highlightedHeaderArg: 'sketches',
commandName: 'Sweep',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Sketches: '2 faces',
Profiles: '2 profiles',
Path: '',
Sectional: '',
},
@ -4754,7 +4673,7 @@ path001 = startProfile(sketch001, at = [0, 0])
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Sketches: '2 faces',
Profiles: '2 profiles',
Path: '1 segment',
Sectional: '',
},
@ -4820,25 +4739,12 @@ path001 = startProfile(sketch001, at = [0, 0])
await test.step('Go through command bar flow', async () => {
await toolbar.closePane('code')
await toolbar.revolveButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sketches: '',
AxisOrEdge: '',
Angle: '',
},
highlightedHeaderArg: 'sketches',
commandName: 'Revolve',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'axisOrEdge',
currentArgValue: '',
headerArguments: {
Sketches: '2 faces',
Profiles: '2 profiles',
AxisOrEdge: '',
Angle: '',
},
@ -4854,7 +4760,7 @@ path001 = startProfile(sketch001, at = [0, 0])
currentArgKey: 'angle',
currentArgValue: '360',
headerArguments: {
Sketches: '2 faces',
Profiles: '2 profiles',
AxisOrEdge: 'Edge',
Edge: '1 segment',
Angle: '',
@ -4867,7 +4773,7 @@ path001 = startProfile(sketch001, at = [0, 0])
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Sketches: '2 faces',
Profiles: '2 profiles',
AxisOrEdge: 'Edge',
Edge: '1 segment',
Angle: '180',

View File

@ -99,6 +99,8 @@ test.describe('edit with AI example snapshots', () => {
await test.step('fire off edit prompt', async () => {
await cmdBar.captureTextToCadRequestSnapshot(test.info())
await cmdBar.openCmdBar('promptToEdit')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
// being specific about the color with a hex means asserting pixel color is more stable
await page
.getByTestId('cmd-bar-arg-value')

View File

@ -88,6 +88,8 @@ test.describe('Prompt-to-edit tests', () => {
await test.step('fire off edit prompt', async () => {
await cmdBar.openCmdBar('promptToEdit')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
// being specific about the color with a hex means asserting pixel color is more stable
await page
.getByTestId('cmd-bar-arg-value')
@ -165,6 +167,8 @@ test.describe('Prompt-to-edit tests', () => {
await test.step('fire of bad prompt', async () => {
await cmdBar.openCmdBar('promptToEdit')
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await page
.getByTestId('cmd-bar-arg-value')
.fill('ansheusha asnthuatshoeuhtaoetuhthaeu laughs in dvorak')

View File

@ -995,8 +995,8 @@ profile001 = startProfile(sketch001, at = [${roundOff(scale * 69.6)}, ${roundOff
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// click "line(end = [1.32, 0.38])"
await page.getByText(`line(end = [1.32, 0.38])`).click()
// click profile in code
await page.getByText(`startProfile(at = [-0.45, 0.87])`).click()
await page.waitForTimeout(100)
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeEnabled(
{ timeout: 10_000 }
@ -1014,14 +1014,13 @@ profile001 = startProfile(sketch001, at = [${roundOff(scale * 69.6)}, ${roundOff
// click extrude
await toolbar.extrudeButton.click()
// sketch selection should already have been made. "Sketches: 1 face" only show up when the selection has been made already
// sketch selection should already have been made.
// otherwise the cmdbar would be waiting for a selection.
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: { Sketches: '1 segment', Length: '' },
headerArguments: { Profiles: '1 profile', Length: '' },
highlightedHeaderArg: 'length',
commandName: 'Extrude',
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -551,11 +551,6 @@ export async function getUtils(page: Page, test_?: typeof test) {
createNewFile: async (name: string) => {
return test?.step(`Create a file named ${name}`, async () => {
// If the application is in the middle of connecting a stream
// then creating a new file won't work in the end.
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByTestId('create-file-button').click()
await page.getByTestId('tree-input-field').fill(name)
await page.keyboard.press('Enter')

View File

@ -4,9 +4,10 @@ import type { Page } from '@playwright/test'
import { createProject, getUtils } from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
test.describe('Text-to-CAD tests', () => {
test('basic lego happy case', async ({ page, homePage }) => {
test('basic lego happy case', async ({ page, homePage, cmdBar }) => {
const u = await getUtils(page)
await test.step('Set up', async () => {
@ -15,7 +16,11 @@ test.describe('Text-to-CAD tests', () => {
await u.waitForPageLoad()
})
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x4 lego',
cmdBar
)
// Find the toast.
// Look out for the toast message
@ -56,6 +61,7 @@ test.describe('Text-to-CAD tests', () => {
test('success model, then ignore success toast, user can create new prompt from command bar', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
@ -64,7 +70,11 @@ test.describe('Text-to-CAD tests', () => {
await homePage.goToModelingScene()
await u.waitForPageLoad()
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x6 lego')
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x6 lego',
cmdBar
)
// Find the toast.
// Look out for the toast message
@ -82,7 +92,11 @@ test.describe('Text-to-CAD tests', () => {
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
// Can send a new prompt from the command bar.
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x4 lego',
cmdBar
)
// Find the toast.
// Look out for the toast message
@ -100,6 +114,7 @@ test.describe('Text-to-CAD tests', () => {
test('you can reject text-to-cad output and it does nothing', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
@ -108,7 +123,11 @@ test.describe('Text-to-CAD tests', () => {
await homePage.goToModelingScene()
await u.waitForPageLoad()
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x4 lego',
cmdBar
)
// Find the toast.
// Look out for the toast message
@ -141,6 +160,7 @@ test.describe('Text-to-CAD tests', () => {
test('sending a bad prompt fails, can dismiss', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
@ -150,7 +170,11 @@ test.describe('Text-to-CAD tests', () => {
await u.waitForPageLoad()
const randomPrompt = `aslkdfja;` + Date.now() + `FFFFEIWJF`
await sendPromptFromCommandBarTriggeredByButton(page, randomPrompt)
await sendPromptFromCommandBarAndSetExistingProject(
page,
randomPrompt,
cmdBar
)
// Find the toast.
// Look out for the toast message
@ -188,6 +212,7 @@ test.describe('Text-to-CAD tests', () => {
test('sending a bad prompt fails, can start over from toast', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
@ -197,7 +222,7 @@ test.describe('Text-to-CAD tests', () => {
await u.waitForPageLoad()
const badPrompt = 'akjsndladf lajbhflauweyfaaaljhr472iouafyvsssssss'
await sendPromptFromCommandBarTriggeredByButton(page, badPrompt)
await sendPromptFromCommandBarAndSetExistingProject(page, badPrompt, cmdBar)
// Find the toast.
// Look out for the toast message
@ -256,6 +281,7 @@ test.describe('Text-to-CAD tests', () => {
test('sending a bad prompt fails, can ignore toast, can start over from command bar', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
@ -265,7 +291,7 @@ test.describe('Text-to-CAD tests', () => {
await u.waitForPageLoad()
const badPrompt = 'akjsndladflajbhflauweyf15;'
await sendPromptFromCommandBarTriggeredByButton(page, badPrompt)
await sendPromptFromCommandBarAndSetExistingProject(page, badPrompt, cmdBar)
// Find the toast.
// Look out for the toast message
@ -292,7 +318,11 @@ test.describe('Text-to-CAD tests', () => {
await expect(page.getByText(`Text-to-CAD failed`)).toBeVisible()
// They should be able to try again from the command bar.
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x4 lego',
cmdBar
)
// Find the toast.
// Look out for the toast message
@ -310,17 +340,40 @@ test.describe('Text-to-CAD tests', () => {
test('ensure you can shift+enter in the prompt box', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
const projectName = await homePage.goToModelingScene()
await u.waitForPageLoad()
const promptWithNewline = `a 2x4\nlego`
await page.getByTestId('text-to-cad').click()
await test.step('Get to the prompt step to test', async () => {
await cmdBar.openCmdBar()
await cmdBar.selectOption({ name: 'Text-to-CAD Create' }).click()
await cmdBar.currentArgumentInput.fill('existing')
await cmdBar.progressCmdBar()
await cmdBar.currentArgumentInput.fill(projectName)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Text-to-CAD Create',
stage: 'arguments',
currentArgKey: 'prompt',
currentArgValue: '',
highlightedHeaderArg: 'prompt',
headerArguments: {
Method: 'Existing project',
ProjectName: projectName,
Prompt: '',
},
})
})
// Type the prompt.
await page.keyboard.type('a 2x4')
@ -354,6 +407,7 @@ test.describe('Text-to-CAD tests', () => {
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)
@ -365,11 +419,23 @@ test.describe('Text-to-CAD tests', () => {
await homePage.goToModelingScene()
await u.waitForPageLoad()
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x4 lego',
cmdBar
)
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x8 lego')
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x8 lego',
cmdBar
)
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x10 lego')
await sendPromptFromCommandBarAndSetExistingProject(
page,
'a 2x10 lego',
cmdBar
)
// Find the toast.
// Look out for the toast message
@ -440,6 +506,7 @@ test.describe('Text-to-CAD tests', () => {
test('can do many at once with errors, clicking dismiss error does not dismiss all', async ({
page,
homePage,
cmdBar,
}) => {
const u = await getUtils(page)
@ -448,11 +515,16 @@ test.describe('Text-to-CAD tests', () => {
await homePage.goToModelingScene()
await u.waitForPageLoad()
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
await sendPromptFromCommandBarTriggeredByButton(
await sendPromptFromCommandBarAndSetExistingProject(
page,
'alkjsdnlajshdbfjlhsbdf a;askjdnf'
'a 2x4 lego',
cmdBar
)
await sendPromptFromCommandBarAndSetExistingProject(
page,
'alkjsdnlajshdbfjlhsbdf a;askjdnf',
cmdBar
)
// Find the toast.
@ -526,7 +598,9 @@ async function _sendPromptFromCommandBar(page: Page, promptStr: string) {
const cmdSearchBar = page.getByPlaceholder('Search commands')
await expect(cmdSearchBar).toBeVisible()
const textToCadCommand = page.getByText('Use the Zoo Text-to-CAD API')
const textToCadCommand = page.getByRole('option', {
name: 'Text-to-CAD Create',
})
await expect(textToCadCommand.first()).toBeVisible()
// Click the Text-to-CAD command
await textToCadCommand.first().scrollIntoViewIfNeeded()
@ -544,29 +618,67 @@ async function _sendPromptFromCommandBar(page: Page, promptStr: string) {
})
}
async function sendPromptFromCommandBarTriggeredByButton(
async function sendPromptFromCommandBarAndSetExistingProject(
page: Page,
promptStr: string
promptStr: string,
cmdBar: CmdBarFixture,
projectName = 'testDefault'
) {
await page.waitForTimeout(1000)
await test.step(`Send prompt from command bar: ${promptStr}`, async () => {
await page.getByTestId('text-to-cad').click()
await cmdBar.openCmdBar()
await cmdBar.selectOption({ name: 'Text-to-CAD Create' }).click()
// Enter the prompt.
const prompt = page.getByRole('textbox', { name: 'Prompt' })
await expect(prompt.first()).toBeVisible()
await cmdBar.expectState({
commandName: 'Text-to-CAD Create',
stage: 'arguments',
currentArgKey: 'method',
currentArgValue: '',
highlightedHeaderArg: 'method',
headerArguments: {
Method: '',
Prompt: '',
},
})
await cmdBar.currentArgumentInput.fill('existing')
await cmdBar.progressCmdBar()
// Type the prompt.
await page.keyboard.type(promptStr)
await page.waitForTimeout(200)
await page.keyboard.press('Enter')
await cmdBar.expectState({
commandName: 'Text-to-CAD Create',
stage: 'arguments',
currentArgKey: 'projectName',
currentArgValue: '',
highlightedHeaderArg: 'projectName',
headerArguments: {
Method: 'Existing project',
ProjectName: '',
Prompt: '',
},
})
await cmdBar.currentArgumentInput.fill(projectName)
await cmdBar.progressCmdBar()
await cmdBar.expectState({
commandName: 'Text-to-CAD Create',
stage: 'arguments',
currentArgKey: 'prompt',
currentArgValue: '',
highlightedHeaderArg: 'prompt',
headerArguments: {
Method: 'Existing project',
ProjectName: projectName,
Prompt: '',
},
})
await cmdBar.currentArgumentInput.fill(promptStr)
await cmdBar.progressCmdBar()
})
}
test(
'Text-to-CAD functionality',
{ tag: '@electron' },
async ({ context, page }, testInfo) => {
async ({ context, page, cmdBar }, testInfo) => {
const projectName = 'project-000'
const prompt = 'lego 2x4'
const textToCadFileName = 'lego-2x4.kcl'
@ -603,7 +715,12 @@ test(
await openKclCodePanel()
await test.step(`Test file creation`, async () => {
await sendPromptFromCommandBarTriggeredByButton(page, prompt)
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()
@ -773,12 +890,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')).toContainText(
'2x2x2-cube.kcl'
'main.kcl'
)
await u.openFilePanel()
await expect(
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
page.getByTestId('file-tree-item').getByText('main.kcl')
).toBeVisible()
}
)
@ -913,7 +1030,7 @@ 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')).toContainText(
'2x2x2-cube.kcl'
'main.kcl'
)
await page.getByRole('button', { name: 'Reject' }).click()
@ -961,7 +1078,7 @@ 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')).toContainText(
'2x2x2-cube.kcl'
'main.kcl'
)
}
)
@ -1213,18 +1330,14 @@ 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')).toContainText(
'2x2x2-cube.kcl'
'main.kcl'
)
// Check file is created
await u.openFilePanel()
await expect(
page.getByTestId('file-tree-item').getByText('2x2x2-cube.kcl')
).toBeVisible()
await expect(
page.getByTestId('file-tree-item').getByText('main.kcl')
).not.toBeVisible()
).toBeVisible()
}
)

View File

@ -573,7 +573,6 @@ profile001 = startProfile(sketch002, at = [-12.34, 12.34])
await expect(page.getByTestId('command-bar')).toBeVisible()
await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await expect(page.getByText('Confirm Extrude')).toBeVisible()
await cmdBar.progressCmdBar()

View File

@ -94,7 +94,6 @@
"build:wasm:dev": "./scripts/build-wasm-dev.sh",
"build:wasm:dev:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/build-wasm-dev.ps1",
"pretest": "npm run remove-importmeta",
"test:rust": "(cd rust && just test && just lint)",
"simpleserver": "npm run pretest && http-server ./public --cors -p 3000",
"simpleserver:ci": "npm run pretest && http-server ./public --cors -p 3000 &",
"simpleserver:bg": "npm run pretest && http-server ./public --cors -p 3000 &",
@ -130,15 +129,14 @@
"tronb:package:prod": "npm run tronb:vite:prod && electron-builder --config electron-builder.yml --publish always",
"test-setup": "npm install && npm run build:wasm",
"test": "vitest --mode development",
"test:rust": "(cd rust && just test && just lint)",
"test:snapshots": "PLATFORM=web NODE_ENV=development playwright test --config=playwright.config.ts --grep=@snapshot --trace=on --shard=1/1",
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts --exclude **/jest-component-unit-tests/*",
"test:unit": "vitest run --mode development --exclude **/jest-component-unit-tests/*",
"test:unit:components": "jest -c jest-component-unit-tests/jest.config.ts --rootDir jest-component-unit-tests/",
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts",
"test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot",
"test:playwright:electron:local": "npm run tronb:vite:dev && playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot --grep-invert=\"$(curl --silent https://test-analysis-bot.hawk-dinosaur.ts.net/projects/KittyCAD/modeling-app/tests/disabled/regex)\"",
"test:playwright:electron:local-engine": "npm run tronb:vite:dev && playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot|@skipLocalEngine' --grep-invert=\"$(curl --silent https://test-analysis-bot.hawk-dinosaur.ts.net/projects/KittyCAD/modeling-app/tests/disabled/regex)\"",
"test:unit:local": "npm run simpleserver:bg && npm run test:unit; kill-port 3000",
"test:unit:kcl-samples:local": "npm run simpleserver:bg && npm run test:unit:kcl-samples; kill-port 3000"
"test:unit:local": "npm run simpleserver:bg && npm run test:unit; kill-port 3000"
},
"browserslist": {
"production": [

View File

@ -49,12 +49,16 @@ When you submit a PR to add or modify KCL samples, images will be generated and
[![countersunk-plate](screenshots/countersunk-plate.png)](countersunk-plate/main.kcl)
#### [cpu-cooler](cpu-cooler/main.kcl) ([screenshot](screenshots/cpu-cooler.png))
[![cpu-cooler](screenshots/cpu-cooler.png)](cpu-cooler/main.kcl)
#### [curtain-wall-anchor-plate](curtain-wall-anchor-plate/main.kcl) ([screenshot](screenshots/curtain-wall-anchor-plate.png))
[![curtain-wall-anchor-plate](screenshots/curtain-wall-anchor-plate.png)](curtain-wall-anchor-plate/main.kcl)
#### [cycloidal-gear](cycloidal-gear/main.kcl) ([screenshot](screenshots/cycloidal-gear.png))
[![cycloidal-gear](screenshots/cycloidal-gear.png)](cycloidal-gear/main.kcl)
#### [dodecahedron](dodecahedron/main.kcl) ([screenshot](screenshots/dodecahedron.png))
[![dodecahedron](screenshots/dodecahedron.png)](dodecahedron/main.kcl)
#### [enclosure](enclosure/main.kcl) ([screenshot](screenshots/enclosure.png))
[![enclosure](screenshots/enclosure.png)](enclosure/main.kcl)
#### [engine-valve](engine-valve/main.kcl) ([screenshot](screenshots/engine-valve.png))
[![engine-valve](screenshots/engine-valve.png)](engine-valve/main.kcl)
#### [exhaust-manifold](exhaust-manifold/main.kcl) ([screenshot](screenshots/exhaust-manifold.png))
[![exhaust-manifold](screenshots/exhaust-manifold.png)](exhaust-manifold/main.kcl)
#### [flange](flange/main.kcl) ([screenshot](screenshots/flange.png))
@ -103,6 +107,8 @@ When you submit a PR to add or modify KCL samples, images will be generated and
[![mounting-plate](screenshots/mounting-plate.png)](mounting-plate/main.kcl)
#### [multi-axis-robot](multi-axis-robot/main.kcl) ([screenshot](screenshots/multi-axis-robot.png))
[![multi-axis-robot](screenshots/multi-axis-robot.png)](multi-axis-robot/main.kcl)
#### [pdu-faceplate](pdu-faceplate/main.kcl) ([screenshot](screenshots/pdu-faceplate.png))
[![pdu-faceplate](screenshots/pdu-faceplate.png)](pdu-faceplate/main.kcl)
#### [pillow-block-bearing](pillow-block-bearing/main.kcl) ([screenshot](screenshots/pillow-block-bearing.png))
[![pillow-block-bearing](screenshots/pillow-block-bearing.png)](pillow-block-bearing/main.kcl)
#### [pipe](pipe/main.kcl) ([screenshot](screenshots/pipe.png))
@ -119,16 +125,24 @@ When you submit a PR to add or modify KCL samples, images will be generated and
[![router-template-cross-bar](screenshots/router-template-cross-bar.png)](router-template-cross-bar/main.kcl)
#### [router-template-slate](router-template-slate/main.kcl) ([screenshot](screenshots/router-template-slate.png))
[![router-template-slate](screenshots/router-template-slate.png)](router-template-slate/main.kcl)
#### [sash-window](sash-window/main.kcl) ([screenshot](screenshots/sash-window.png))
[![sash-window](screenshots/sash-window.png)](sash-window/main.kcl)
#### [sheet-metal-bracket](sheet-metal-bracket/main.kcl) ([screenshot](screenshots/sheet-metal-bracket.png))
[![sheet-metal-bracket](screenshots/sheet-metal-bracket.png)](sheet-metal-bracket/main.kcl)
#### [shepherds-hook-bolt](shepherds-hook-bolt/main.kcl) ([screenshot](screenshots/shepherds-hook-bolt.png))
[![shepherds-hook-bolt](screenshots/shepherds-hook-bolt.png)](shepherds-hook-bolt/main.kcl)
#### [socket-head-cap-screw](socket-head-cap-screw/main.kcl) ([screenshot](screenshots/socket-head-cap-screw.png))
[![socket-head-cap-screw](screenshots/socket-head-cap-screw.png)](socket-head-cap-screw/main.kcl)
#### [spinning-highrise-tower](spinning-highrise-tower/main.kcl) ([screenshot](screenshots/spinning-highrise-tower.png))
[![spinning-highrise-tower](screenshots/spinning-highrise-tower.png)](spinning-highrise-tower/main.kcl)
#### [spur-gear](spur-gear/main.kcl) ([screenshot](screenshots/spur-gear.png))
[![spur-gear](screenshots/spur-gear.png)](spur-gear/main.kcl)
#### [spur-reduction-gearset](spur-reduction-gearset/main.kcl) ([screenshot](screenshots/spur-reduction-gearset.png))
[![spur-reduction-gearset](screenshots/spur-reduction-gearset.png)](spur-reduction-gearset/main.kcl)
#### [surgical-drill-guide](surgical-drill-guide/main.kcl) ([screenshot](screenshots/surgical-drill-guide.png))
[![surgical-drill-guide](screenshots/surgical-drill-guide.png)](surgical-drill-guide/main.kcl)
#### [thermal-block-insert](thermal-block-insert/main.kcl) ([screenshot](screenshots/thermal-block-insert.png))
[![thermal-block-insert](screenshots/thermal-block-insert.png)](thermal-block-insert/main.kcl)
#### [tooling-nest-block](tooling-nest-block/main.kcl) ([screenshot](screenshots/tooling-nest-block.png))
[![tooling-nest-block](screenshots/tooling-nest-block.png)](tooling-nest-block/main.kcl)
#### [utility-sink](utility-sink/main.kcl) ([screenshot](screenshots/utility-sink.png))

View File

@ -0,0 +1,155 @@
// Curtain Wall Anchor Plate
// A structural steel L-plate used to anchor curtain wall systems to concrete slabs, with elongated holes for adjustability and bolts with nuts and base plates for secure fastening
// Set units in millimeters (mm)
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
// Define parameters
slabPlateBaseLength = 300
slabPlateHookLength = 80
slabPlateWidth = 200
slabPlateThickness = 8
offsetSlabRail = 200
// Generate L-shaped anchor profile with base and hook flange
// Includes fillets at internal and external corners for strength and safety
fn lProfileFn(lengthBase, lengthHook, width, thickness) {
profilePlane = startSketchOn(offsetPlane(XZ, offset = -width / 2))
profileShape = startProfile(profilePlane, at = [0, 0])
|> yLine(length = lengthHook, tag = $hookOutside)
|> xLine(length = thickness)
|> yLine(length = thickness - lengthHook, tag = $hookInside)
|> xLine(length = lengthBase - thickness, tag = $baseInside)
|> yLine(length = -thickness)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $baseOutside)
|> close()
profileBody = extrude(profileShape, length = width)
|> fillet(
radius = thickness,
tags = [
getCommonEdge(faces = [baseInside, hookInside])
],
)
|> fillet(
radius = thickness * 2,
tags = [
getCommonEdge(faces = [baseOutside, hookOutside])
],
)
return profileBody
}
// Create a hexagonal shape used for bolt and nut heads
fn hexagonFn(plane, radius) {
shape = startProfile(plane, at = [-radius, 0])
|> angledLine(angle = 60, length = radius)
|> xLine(length = radius)
|> angledLine(angle = -60, length = radius)
|> angledLine(angle = -120, length = radius)
|> xLine(length = -radius)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
return shape
}
// Build a bolt with a hexagonal head and cylindrical shaft
fn boltFn(diameter, length) {
boltHeadPlane = startSketchOn(XY)
boltHeadShape = hexagonFn(plane = boltHeadPlane, radius = diameter)
boltHeadBody = extrude(boltHeadShape, length = diameter * 0.7)
boltPlane = startSketchOn(boltHeadBody, face = START)
boltShape = circle(boltPlane, center = [0, 0], radius = diameter / 2)
boltBody = extrude(boltShape, length = length)
return boltBody
}
// Construct a bolt assembly with base plate and hex nut
// Assembles all parts for realistic anchor simulation
fn boltWithPlateAndNutFn(diameter, length, gap) {
plateSide = diameter * 3
plateplane = startSketchOn(offsetPlane(XY, offset = -gap))
plateShape = startProfile(plateplane, at = [-plateSide / 2, -plateSide / 2])
|> yLine(length = plateSide)
|> xLine(length = plateSide)
|> yLine(length = -plateSide)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
plateBody = extrude(plateShape, length = -diameter * 0.3)
nutPlane = startSketchOn(plateBody, face = START)
boltHeadShape = hexagonFn(plane = nutPlane, radius = 12)
boltHeadBody = extrude(boltHeadShape, length = diameter * 0.7)
boltBody = boltFn(diameter = diameter, length = gap + diameter + 3)
mergedBody = union([boltHeadBody, boltBody])
return mergedBody
}
// Generate the plate geometry with a vertical hook for slab attachment
slabPlate = lProfileFn(
lengthBase = slabPlateBaseLength,
lengthHook = slabPlateHookLength,
width = slabPlateWidth,
thickness = slabPlateThickness,
)
// Define oblong holes for bolts, allowing positional adjustment
wideHoleWidth = 12
wideHoleLength = 60
wideHoleOffset = 30
// Two slots mirrored across the plate width
wideHolePlane = startSketchOn(XY)
wideHoleShape = startProfile(
wideHolePlane,
at = [
-(wideHoleLength - wideHoleWidth) / 2,
wideHoleWidth / 2
],
)
|> xLine(length = wideHoleLength - wideHoleWidth)
|> tangentialArc(endAbsolute = [
(wideHoleLength - wideHoleWidth) / 2,
-wideHoleWidth / 2
])
|> xLine(length = wideHoleWidth - wideHoleLength)
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> translate(
%,
x = offsetSlabRail,
y = wideHoleOffset - (slabPlateWidth / 2),
z = -1,
)
wideHoleVoidLeft = extrude(wideHoleShape, length = slabPlateThickness + 2)
wideHoleVoidRight = clone(wideHoleVoidLeft)
|> translate(
%,
x = 0,
y = slabPlateWidth - (wideHoleOffset * 2),
z = 0,
)
// Cut the holes into the anchor plate body
slabPlatePunchOne = subtract([slabPlate], tools = [wideHoleVoidLeft])
slabPlatePunchTwo = subtract([slabPlatePunchOne], tools = [wideHoleVoidRight])
// Add two bolt assemblies into the oblong slots
// Properly rotated and spaced to match anchor hole layout
slabPlateBolts = boltWithPlateAndNutFn(diameter = 10, length = 20, gap = slabPlateThickness + 5)
|> rotate(
%,
roll = 180,
pitch = 0,
yaw = 0,
)
|> translate(
%,
x = offsetSlabRail,
y = wideHoleOffset - (slabPlateWidth / 2),
z = 5,
)
|> patternLinear3d(
%,
instances = 2,
distance = slabPlateWidth - (wideHoleOffset * 2),
axis = [0, -1, 0],
)

View File

@ -0,0 +1,79 @@
// Engine Valve
// A mechanical valve used in internal combustion engines to control intake or exhaust flow
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
// Define parameters
valveDiameter = 30
valveLength = 120
valveHeadLength = valveDiameter * 1.0
valveHeadThickness = 3
stemDiameter = 6
stemHeadLength = 9
stemLength = valveLength - valveHeadLength - stemHeadLength
// Create the valve head
valveRadius = valveDiameter / 2
valveHeadPlane = startSketchOn(XZ)
valveHeadShape = startProfile(valveHeadPlane, at = [-0.01, valveHeadLength])
|> xLine(length = 0.01 - (stemDiameter / 2))
|> line(endAbsolute = [0.01 - (stemDiameter / 2), valveRadius])
|> tangentialArc(endAbsolute = [-0.8 * valveRadius, valveHeadThickness], tag = $seg01)
|> tangentialArc(endAbsolute = [-valveRadius, 0])
|> xLine(length = 0.3 * valveRadius)
|> arc(
interiorAbsolute = [
-0.34 * valveRadius,
0.08 * valveRadius
],
endAbsolute = [
-0.02 * valveRadius,
0.11 * valveRadius
],
)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
valveHead = revolve(valveHeadShape, angle = 360, axis = Y)
// Create the valve stem
valveStemSketch = startSketchOn(offsetPlane(XY, offset = valveHeadLength))
|> circle(center = [0, 0], radius = stemDiameter / 2)
|> extrude(length = stemLength - valveHeadLength - stemHeadLength)
// Create the valve stem end
stepLength = stemHeadLength / 10
step1 = startSketchOn(valveStemSketch, face = END)
|> circle(%, center = [0, 0], radius = stemDiameter / 2 * 0.9)
|> extrude(%, length = stepLength * 2)
step2 = startSketchOn(step1, face = END)
|> circle(%, center = [0, 0], radius = stemDiameter / 2 * 0.8)
|> extrude(%, length = stepLength)
step3 = startSketchOn(step2, face = END)
|> circle(%, center = [0, 0], radius = stemDiameter / 2 * 0.9)
|> extrude(%, length = stepLength)
step4 = startSketchOn(step3, face = END)
|> circle(%, center = [0, 0], radius = stemDiameter / 2 * 0.8)
|> extrude(%, length = stepLength)
step5 = startSketchOn(step4, face = END)
|> circle(%, center = [0, 0], radius = stemDiameter / 2 * 0.9)
|> extrude(%, length = stepLength)
step6 = startSketchOn(step5, face = END)
|> circle(%, center = [0, 0], radius = stemDiameter / 2 * 0.8)
|> extrude(%, length = stepLength)
step7 = startSketchOn(step6, face = END)
|> circle(
%,
center = [0, 0],
radius = stemDiameter / 2 * 0.9,
tag = $seg02,
)
|> extrude(%, length = stepLength * 3, tagEnd = $capEnd001)
|> chamfer(
length = 0.5,
tags = [
getCommonEdge(faces = [seg02, capEnd001])
],
)

View File

@ -147,6 +147,16 @@
"removable-sticker.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "curtain-wall-anchor-plate/main.kcl",
"multipleFiles": false,
"title": "Curtain Wall Anchor Plate",
"description": "A structural steel L-plate used to anchor curtain wall systems to concrete slabs, with elongated holes for adjustability and bolts with nuts and base plates for secure fastening",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "cycloidal-gear/main.kcl",
@ -177,6 +187,16 @@
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "engine-valve/main.kcl",
"multipleFiles": false,
"title": "Engine Valve",
"description": "A mechanical valve used in internal combustion engines to control intake or exhaust flow",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "exhaust-manifold/main.kcl",
@ -422,6 +442,16 @@
"robot-rotating-base.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pdu-faceplate/main.kcl",
"multipleFiles": false,
"title": "Power Distribution Unit (PDU) faceplate with European plug sockets and switch",
"description": "Designed for standard 19-inch rack systems with 1U height and 8 sockets",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "pillow-block-bearing/main.kcl",
@ -512,6 +542,16 @@
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "sash-window/main.kcl",
"multipleFiles": false,
"title": "Sash Window",
"description": "A traditional wooden sash window with two vertically sliding panels and a central locking mechanism",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "sheet-metal-bracket/main.kcl",
@ -522,6 +562,16 @@
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "shepherds-hook-bolt/main.kcl",
"multipleFiles": false,
"title": "Shepherds Hook Bolt",
"description": "A bent bolt with a curved hook, typically used for hanging or anchoring loads. The threaded end allows secure attachment to surfaces or materials, while the curved hook resists pull-out under tension.",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "socket-head-cap-screw/main.kcl",
@ -532,6 +582,16 @@
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "spinning-highrise-tower/main.kcl",
"multipleFiles": false,
"title": "Spinning Highrise Tower",
"description": "A conceptual high-rise tower with a central core and rotating floor slabs, demonstrating dynamic form through vertical repetition and transformation",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "spur-gear/main.kcl",
@ -562,6 +622,16 @@
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "thermal-block-insert/main.kcl",
"multipleFiles": false,
"title": "Thermal Block Insert",
"description": "Interlocking insulation insert for masonry walls, designed with a tongue-and-groove profile for modular alignment and thermal efficiency",
"files": [
"main.kcl"
]
},
{
"file": "main.kcl",
"pathFromProjectDirectoryToFirstFile": "tooling-nest-block/main.kcl",

View File

@ -0,0 +1,240 @@
// Power Distribution Unit (PDU) faceplate with European plug sockets and switch
// Designed for standard 19-inch rack systems with 1U height and 8 sockets
// Set units in millimeters (mm)
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
// Define the dimensions
// Width fits standard 19” rack, height is 1U, depth is variable
faceplateWidth = 482.6 // this is standardized to fit 19-inch racks)
faceplateHeight = 44.45 // usually 1U (44.45 mm), but can be 2U (88.9 mm) or more
faceplateDepth = 100 // varies by manufacturer, but commonly between 100 mm and 300 mm
// Define dimensions of side supports (width and thickness)
supportWidth = 50
supportThickness = 3
// Main body of the PDU faceplate with integrated rack mounting flanges
faceplateShape = startSketchOn(offsetPlane(XY, offset = -faceplateHeight / 2))
|> startProfile(%, at = [-faceplateWidth / 2 - supportWidth, 0])
|> yLine(length = supportThickness)
|> xLine(length = supportWidth)
|> yLine(length = faceplateDepth - supportThickness)
|> xLine(length = faceplateWidth)
|> yLine(length = supportThickness - faceplateDepth)
|> xLine(length = supportWidth)
|> yLine(length = -supportThickness)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|> close()
faceplateBody = extrude(faceplateShape, length = faceplateHeight)
faceplateFrontFace = startSketchOn(faceplateBody, face = seg01)
// Creates recessed volume within the faceplate for inserting modules
nestWall = 2
nestWidth = faceplateWidth - (nestWall * 2)
nestHeight = faceplateHeight - (nestWall * 2)
nestDepth = faceplateDepth - nestWall
nestShape = startProfile(faceplateFrontFace, at = [-nestWidth / 2, nestHeight / 2])
|> xLine(length = nestWidth)
|> yLine(length = -nestHeight)
|> xLine(length = -nestWidth)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
nestVoid = extrude(nestShape, length = -nestDepth)
// Spacer block on the left side, used to position components correctly
moduleHeight = nestHeight
moduleWidth = nestHeight
moduleDepth = nestHeight
leftSpacerWidth = moduleWidth * 1.5
leftSpacerPosition = leftSpacerWidth / 2 - (nestWidth / 2)
fn boxModuleFn(width) {
shape = startSketchOn(XZ)
|> startProfile(%, at = [-width / 2, moduleHeight / 2])
|> xLine(length = width)
|> yLine(length = -moduleHeight)
|> xLine(length = -width)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
body = extrude(shape, length = -moduleDepth)
return body
}
leftSpacerShape = boxModuleFn(width = leftSpacerWidth)
|> translate(
%,
x = leftSpacerPosition,
y = 0,
z = 0,
)
// Module for power switch including front plate and red rocker button
switchPosition = leftSpacerPosition + leftSpacerWidth / 2 + moduleWidth / 2
swtichWidth = moduleWidth
// Switch Body
switchBody = boxModuleFn(width = moduleWidth)
// Switch Plate
swtichPlateWidth = 20
switchPlateHeight = 30
switchPlateThickness = 3
switchPlateShape = startSketchOn(switchBody, face = END)
|> startProfile(
%,
at = [
-swtichPlateWidth / 2,
-switchPlateHeight / 2
],
)
|> yLine(length = switchPlateHeight)
|> xLine(length = swtichPlateWidth)
|> yLine(length = -switchPlateHeight)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
switchPlateBody = extrude(switchPlateShape, length = switchPlateThickness)
|> translate(
%,
x = switchPosition,
y = 0,
z = 0,
)
// Switch Button
switchButtonHeight = 26
swtichButtonWidth = 15
switchButtonShape = startSketchOn(offsetPlane(-YZ, offset = -swtichButtonWidth / 2))
|> startProfile(
%,
at = [
switchPlateThickness,
switchButtonHeight / 2
],
)
|> line(end = [3, -1])
|> arc(interiorAbsolute = [6, 0], endAbsolute = [12, -9])
|> line(endAbsolute = [
switchPlateThickness,
-switchButtonHeight / 2
])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
switchButtonBody = extrude(switchButtonShape, length = swtichButtonWidth)
|> translate(
%,
x = switchPosition,
y = 0,
z = 0,
)
|> appearance(%, color = "#ff0000")
// Spacer between switch and plug modules for layout alignment
secondSpacerWidth = moduleWidth / 2
secondSpacerPosition = switchPosition + swtichWidth / 2 + secondSpacerWidth / 2
secondSpacerBody = boxModuleFn(width = secondSpacerWidth)
|> translate(
%,
x = secondSpacerPosition,
y = 0,
z = 0,
)
// European power plug modules with circular sockets and two-pin holes
// 8 identical sockets, each with grounding notch and dual-pin recesses
powerPlugWidth = moduleWidth
powerPlugCount = 8
powerPlugOveralWidth = powerPlugWidth * powerPlugCount
firstPowerPlugPosition = secondSpacerPosition + secondSpacerWidth / 2 + powerPlugWidth / 2
lastPowerPlugPosition = firstPowerPlugPosition + powerPlugWidth * (powerPlugCount - 1)
powerPlugBody = boxModuleFn(width = powerPlugWidth)
|> translate(
%,
x = firstPowerPlugPosition,
y = 0,
z = 0,
)
plugShape = startSketchOn(powerPlugBody, face = END)
|> circle(%, center = [0, 0], radius = 17)
|> translate(
%,
x = firstPowerPlugPosition,
y = 0,
z = 0,
)
plugBody = extrude(plugShape, length = -20)
plugHoleDistance = 20
plugHoleShape = startSketchOn(plugBody, face = START)
|> circle(%, center = [-plugHoleDistance / 2, 0], radius = 3)
|> translate(
%,
x = firstPowerPlugPosition,
y = 0,
z = 0,
)
|> patternLinear2d(
%,
instances = 2,
distance = plugHoleDistance,
axis = [1, 0],
)
plugHoleBody = extrude(plugHoleShape, length = -5)
|> patternLinear3d(
%,
instances = powerPlugCount,
distance = powerPlugWidth,
axis = [1, 0, 0],
)
// Rightmost spacer to fill in remaining horizontal space
rightSpacerWidth = nestWidth / 2 - lastPowerPlugPosition - (powerPlugWidth / 2)
rightSpacerPosition = lastPowerPlugPosition + powerPlugWidth / 2 + rightSpacerWidth / 2
rightSpacerBody = boxModuleFn(width = rightSpacerWidth)
|> translate(
%,
x = rightSpacerPosition,
y = 0,
z = 0,
)
// Rack mounting holes on flanges, elongated for alignment flexibility
holeWidth = 25
holeDiameter = 5
holeStraightSegment = holeWidth - holeDiameter
holeVerticalDistance = faceplateHeight * 0.3
holeShapes = startProfile(
faceplateFrontFace,
at = [
-holeStraightSegment / 2,
holeDiameter / 2
],
)
|> xLine(length = holeStraightSegment)
|> tangentialArc(endAbsolute = [
holeStraightSegment / 2,
-holeDiameter / 2
])
|> xLine(length = -holeStraightSegment)
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> translate(
%,
x = -faceplateWidth / 2 - (supportWidth / 2),
y = 0,
z = -holeVerticalDistance,
)
|> patternLinear2d(
%,
instances = 3,
distance = holeVerticalDistance,
axis = [0, 1],
)
|> patternLinear2d(
%,
instances = 2,
distance = faceplateWidth + supportWidth,
axis = [1, 0],
)
holeVoid = extrude(holeShapes, length = -supportThickness)

View File

@ -0,0 +1,214 @@
// Sash Window
// A traditional wooden sash window with two vertically sliding panels and a central locking mechanism
// Set units in millimeters (mm)
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
// Window state: 0 for closed, 1 for open
windowState = 0
// Basic window dimensions
windowWidth = 500
windowHeight = 1000
// Frame thickness and depth
frameWidth = 30
frameDepth = 50
// Number of divisions per sash (horizontal and vertical)
sashOpeningCountHorizontal = 2
sashOpeningCountVertical = 1
// Derived dimensions
sashWidth = windowWidth - (frameWidth * 2)
sashHeight = (windowHeight - (frameWidth * 2)) / 2 + frameWidth / 2
sashDepth = frameDepth / 2 - 2
sashTravelDistance = sashHeight * windowState * 0.8
// Function to create panel with frame and openings
fn panelFn(plane, offset, width, height, depth, perimeter, divisionThickness, openingCountHorizontal, openingCountVertical) {
// Create panel base shape
panelPlane = startSketchOn(offsetPlane(XZ, offset = offset))
panelShape = startProfile(panelPlane, at = [-width / 2, -height / 2])
|> yLine(length = height)
|> xLine(length = width)
|> yLine(length = -height)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
panelBody = extrude(panelShape, length = depth)
// Create opening grid within the panel
voidAreaWidth = width - (perimeter * 2)
voidAreaHeight = height - (perimeter * 2)
divisionTotalThicknessHorizontal = divisionThickness * openingCountHorizontal - divisionThickness
divisionTotalThicknessVertical = divisionThickness * openingCountVertical - divisionThickness
voidWidth = (voidAreaWidth - divisionTotalThicknessHorizontal) / openingCountHorizontal
voidHeight = (voidAreaHeight - divisionTotalThicknessVertical) / openingCountVertical
voidStepHorizontal = voidWidth + divisionThickness
voidStepVertical = voidHeight + divisionThickness
voidPlane = startSketchOn(panelBody, face = END)
voidShape = startProfile(
voidPlane,
at = [
-voidAreaWidth / 2,
-voidAreaHeight / 2
],
)
|> yLine(length = voidHeight)
|> xLine(length = voidWidth)
|> yLine(length = -voidHeight)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> patternLinear2d(
%,
instances = openingCountHorizontal,
distance = voidStepHorizontal,
axis = [1, 0],
)
|> patternLinear2d(
%,
instances = openingCountVertical,
distance = voidStepVertical,
axis = [0, 1],
)
voidBody = extrude(voidShape, length = -depth)
|> appearance(color = "#a55e2c")
return panelBody
}
// Create main window frame
frame = panelFn(
plane = XZ,
offset = -frameDepth / 2,
width = windowWidth,
height = windowHeight,
depth = frameDepth,
perimeter = frameWidth,
divisionThickness = 10,
openingCountHorizontal = 1,
openingCountVertical = 1,
)
// Create bottom sliding sash
bottomSash = panelFn(
plane = XZ,
offset = (frameDepth / 2 - sashDepth) / 2,
width = sashWidth,
height = sashHeight,
depth = sashDepth,
perimeter = frameWidth,
divisionThickness = 10,
openingCountHorizontal = sashOpeningCountHorizontal,
openingCountVertical = sashOpeningCountVertical,
)
|> translate(
%,
x = 0,
y = 0,
z = frameWidth / 2 - (sashHeight / 2),
)
|> translate(
%,
x = 0,
y = 0,
z = sashTravelDistance,
) // open / close
// Latch mechanism on bottom sash
// Create latch plate
latchPlateWidth = 13
latchPlateLength = 30
latchPlateThickness = 1
latchPlatePlane = startSketchOn(offsetPlane(XY, offset = frameWidth / 2))
latchPlateShape = startProfile(
latchPlatePlane,
at = [
-latchPlateLength / 2,
-latchPlateWidth / 2
],
)
|> yLine(length = latchPlateWidth)
|> xLine(length = latchPlateLength)
|> yLine(length = -latchPlateWidth)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
latchPlateBody = extrude(latchPlateShape, length = latchPlateThickness)
|> translate(
%,
x = 0,
y = -frameDepth / 4,
z = 0,
)
|> translate(
%,
x = 0,
y = 0,
z = sashTravelDistance,
) // open / close
// Create latch cylinder
latchCylinderHeight = 5
latchCylinderPlane = startSketchOn(offsetPlane(latchPlatePlane, offset = latchPlateThickness))
latchCylinderShape = startProfile(latchCylinderPlane, at = [40, -1])
|> xLine(length = -35)
|> arc(interiorAbsolute = [-5, 0], endAbsolute = [5, 1])
|> xLine(length = 35)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
latchCylinderBody = extrude(latchCylinderShape, length = latchCylinderHeight)
|> translate(
%,
x = 0,
y = -frameDepth / 4,
z = 0,
)
|> translate(
%,
x = 0,
y = 0,
z = sashTravelDistance,
) // open / close
|> rotate(
%,
roll = 0,
pitch = 0,
yaw = -90 * windowState,
)
// Create top fixed sash
topSash = panelFn(
plane = XZ,
offset = -(frameDepth / 2 - sashDepth) / 2 - sashDepth,
width = sashWidth,
height = sashHeight,
depth = sashDepth,
perimeter = frameWidth,
divisionThickness = 10,
openingCountHorizontal = sashOpeningCountHorizontal,
openingCountVertical = sashOpeningCountVertical,
)
|> translate(
%,
x = 0,
y = 0,
z = sashHeight / 2 - (frameWidth / 2),
)
// Create latch nut on the top sash
latchNutPlane = startSketchOn(XZ)
latchNutShape = startProfile(
latchNutPlane,
at = [
-latchPlateLength / 2,
-latchPlateWidth / 2
],
)
|> yLine(length = latchPlateWidth)
|> xLine(length = latchPlateLength)
|> yLine(length = -latchPlateWidth)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
latchNutPlateBody = extrude(latchNutShape, length = latchPlateThickness)

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -0,0 +1,89 @@
// Shepherds Hook Bolt
// A bent bolt with a curved hook, typically used for hanging or anchoring loads. The threaded end allows secure attachment to surfaces or materials, while the curved hook resists pull-out under tension.
// Set units in millimeters (mm)
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
// Define bolt geometry parameters
boltDiameter = 5
hookRadius = 12
shankLength = 5
threadedEndLength = 30
nutDistance = 20
hookStartAngle = 290
hookEndAngle = 150
approximatePitch = boltDiameter * 0.15
threadDepth = 0.6134 * approximatePitch
innerRadius = boltDiameter / 2 - threadDepth
boltNumberOfRevolutions = threadedEndLength / approximatePitch
// Helper values for computing geometry transitions between straight shaft and hook arc
hypotenuse = hookRadius / cos(hookStartAngle - 270)
side = sqrt(pow(hypotenuse, exp = 2) - pow(hookRadius, exp = 2))
shankOffset = hypotenuse + side
// Converts polar coordinates to cartesian points for drawing arcs
fn polarToCartesian(radius, angle) {
x = radius * cos(angle)
y = radius * sin(angle)
return [x, y]
}
// Create the hook and shank profile path
// Includes straight segment and two connected arcs forming the hook
hookProfilePlane = startSketchOn(XZ)
hookProfileShape = startProfile(hookProfilePlane, at = [0, -shankOffset - shankLength])
|> line(endAbsolute = [0, -shankOffset])
|> tangentialArc(endAbsolute = polarToCartesian(radius = hookRadius, angle = hookStartAngle))
|> tangentialArc(endAbsolute = polarToCartesian(radius = hookRadius, angle = hookEndAngle), tag = $hook)
// Create the circular cross-section used for sweeping along the hook path
hookSectionPlane = offsetPlane(XY, offset = -shankOffset - shankLength)
hookSectionShape = circle(hookSectionPlane, center = [0, 0], radius = boltDiameter / 2)
// Sweep the section along the hook profile to form the main body of the hook bolt
hookBody = sweep(hookSectionShape, path = hookProfileShape, sectional = true)
// Add a cylindrical tip at the hook end
tipPlane = startSketchOn(hookBody, face = END)
tipShape = circle(
tipPlane,
center = [hookRadius, 0],
radius = boltDiameter / 2,
tag = $seg01,
)
tipBody = extrude(
tipShape,
length = hookRadius * 0.5,
tagStart = $startTag,
tagEnd = $capEnd001,
)
|> fillet(
radius = boltDiameter / 4,
tags = [
getCommonEdge(faces = [seg01, capEnd001])
],
)
// Create the threaded end of the bolt
// Construct the triangular profile for thread cutting
boltThreadSectionPlane = startSketchOn(XZ)
boltThreadSectionShapeForRevolve = startProfile(
boltThreadSectionPlane,
at = [
innerRadius,
-shankOffset - shankLength - threadedEndLength
],
)
|> line(end = [threadDepth, approximatePitch / 2])
|> line(end = [-threadDepth, approximatePitch / 2])
|> patternLinear2d(axis = [0, 1], instances = boltNumberOfRevolutions, distance = approximatePitch)
|> xLine(length = -innerRadius * 0.9)
|> yLine(length = -threadedEndLength)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
// Create a revolved solid representing the thread geometry by repeating and revolving the profile around the shaft
boltThreadRevolve = revolve(boltThreadSectionShapeForRevolve, angle = 360, axis = Y)

View File

@ -0,0 +1,93 @@
// Spinning Highrise Tower
// A conceptual high-rise tower with a central core and rotating floor slabs, demonstrating dynamic form through vertical repetition and transformation
@settings(defaultLengthUnit = m, kclVersion = 1.0)
// Define global parameters for floor geometry and building layout
floorCount = 17
floorHeight = 5
slabWidth = 30
slabThickness = 0.5
rotationAngleStep = 5
handrailHeight = 1.2
handrailThickness = 0.3
balconyDepth = 3
// Calculate facade and core geometry from parameters
facadeWidth = slabWidth - (balconyDepth * 2)
facadeHeight = floorHeight - slabThickness
coreHeight = floorCount * floorHeight - slabThickness
frameSide = 0.1
windowTargetWidth = 6
windowTargetCount = facadeWidth / windowTargetWidth
windowCount = round(windowTargetCount)
windowWidth = facadeWidth / windowCount
// Helper function: Creates a box from a center plane with given width and height
fn boxFn(plane, width, height) {
shape = startSketchOn(plane)
|> startProfile(%, at = [-width / 2, -width / 2])
|> line(%, end = [0, width])
|> line(%, end = [width, 0])
|> line(%, end = [0, -width])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close(%)
body = extrude(shape, length = height)
return body
}
// Helper function: Defines transformation (translation and rotation) for each floor
fn transformFn(@i) {
return {
translate = [0, 0, i * floorHeight],
rotation = { angle = rotationAngleStep * i }
}
}
// Create building base
baseThickness = 0.2
baseSlab = boxFn(plane = XY, width = slabWidth, height = -baseThickness)
|> appearance(%, color = "#dbd7d2")
// Create ground platform beneath the base
goundSize = 50
groundBody = boxFn(plane = offsetPlane(XY, offset = -baseThickness), width = goundSize, height = -5)
|> appearance(%, color = "#3a3631")
// Create a single slab with handrail height to be reused with pattern
slabAndHandrailGeometry = boxFn(plane = offsetPlane(XY, offset = floorHeight - slabThickness), width = slabWidth, height = slabThickness + handrailHeight)
slabVoidStart = -slabWidth / 2 + handrailThickness
slabVoidWidth = slabWidth - (handrailThickness * 2)
slabVoidShape = startSketchOn(slabAndHandrailGeometry, face = END)
|> startProfile(%, at = [slabVoidStart, slabVoidStart])
|> line(%, end = [0, slabVoidWidth])
|> line(%, end = [slabVoidWidth, 0])
|> line(%, end = [0, -slabVoidWidth])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close(%)
// Generate and pattern slabs with voids across all floors
slabBody = extrude(slabVoidShape, length = -handrailHeight)
|> patternTransform(instances = floorCount, transform = transformFn)
|> appearance(%, color = "#dbd7d2")
// Create structural core of the tower
coreLength = 10
coreWidth = 8
core = startSketchOn(XY)
|> startProfile(%, at = [-coreLength / 2, -coreWidth / 2])
|> line(%, end = [0, coreWidth])
|> line(%, end = [coreLength, 0])
|> line(%, end = [-0.22, -coreWidth])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close(%)
|> extrude(%, length = coreHeight)
// Create facade panels for each floor
facadeStart = facadeWidth / 2
facadeGeometry = boxFn(plane = XY, width = facadeWidth, height = facadeHeight)
|> patternTransform(instances = floorCount, transform = transformFn)
|> appearance(%, color = "#151819")

View File

@ -0,0 +1,61 @@
// Thermal Block Insert
// Interlocking insulation insert for masonry walls, designed with a tongue-and-groove profile for modular alignment and thermal efficiency
// Set units in millimeters (mm)
@settings(defaultLengthUnit = mm, kclVersion = 1.0)
// Define overall dimensions of the insert block
insertLength = 400
insertHeight = 200
insertThickness = 50
// Define tongue-and-groove profile parameters for interlocking geometry
setbackFactor = 0.25 // spacing between tongues
tongueTargetCount = insertLength / 80
tongueCount = round(tongueTargetCount)
tongueLength = insertLength / (tongueCount * (1 + setbackFactor * 2) + 1)
tongueGap = tongueLength * setbackFactor * 2
tongueStep = tongueLength + tongueGap
tongueDepth = tongueLength * 0.5
tongueSetback = tongueLength * setbackFactor
// Function to create one side of the repeating tongue geometry along the block edge
fn tongueBlockFn() {
tongueSingleBlock = xLine(length = tongueLength)
|> line(end = [-tongueSetback, tongueDepth])
|> xLine(length = tongueLength)
|> line(end = [-tongueSetback, -tongueDepth])
|> patternLinear2d(
%,
instances = tongueCount,
distance = tongueStep,
axis = [1, 0],
)
|> xLine(length = tongueLength)
return tongueSingleBlock
}
// Create top-side profile with tongues
tongueShape = startSketchOn(XY)
|> startProfile(%, at = [-insertLength / 2, insertThickness / 2])
|> tongueBlockFn()
|> yLine(length = -insertThickness / 2)
|> xLine(length = -insertLength)
|> close(%)
// Create bottom-side profile with grooves (inverse of tongue)
grooveShape = startSketchOn(XY)
|> startProfile(
%,
at = [
-insertLength / 2,
-insertThickness / 2 - tongueDepth
],
)
|> tongueBlockFn()
|> yLine(length = insertThickness / 2 + tongueDepth)
|> xLine(length = -insertLength)
|> close(%)
// Extrude both tongue and groove profiles to form the final thermal insert block
insertShape = extrude([tongueShape, grooveShape], length = insertHeight)

123
rust/Cargo.lock generated
View File

@ -1815,7 +1815,7 @@ dependencies = [
[[package]]
name = "kcl-bumper"
version = "0.1.73"
version = "0.1.74"
dependencies = [
"anyhow",
"clap",
@ -1826,7 +1826,7 @@ dependencies = [
[[package]]
name = "kcl-derive-docs"
version = "0.1.73"
version = "0.1.74"
dependencies = [
"Inflector",
"anyhow",
@ -1845,8 +1845,9 @@ dependencies = [
[[package]]
name = "kcl-directory-test-macro"
version = "0.1.73"
version = "0.1.74"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"syn 2.0.100",
@ -1854,7 +1855,7 @@ dependencies = [
[[package]]
name = "kcl-language-server"
version = "0.2.73"
version = "0.2.74"
dependencies = [
"anyhow",
"clap",
@ -1875,7 +1876,7 @@ dependencies = [
[[package]]
name = "kcl-language-server-release"
version = "0.1.73"
version = "0.1.74"
dependencies = [
"anyhow",
"clap",
@ -1895,7 +1896,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.2.73"
version = "0.2.74"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1934,6 +1935,7 @@ dependencies = [
"measurements",
"miette",
"mime_guess",
"nalgebra-glm",
"parse-display 0.10.0",
"pretty_assertions",
"pyo3",
@ -1971,7 +1973,7 @@ dependencies = [
[[package]]
name = "kcl-python-bindings"
version = "0.3.73"
version = "0.3.74"
dependencies = [
"anyhow",
"kcl-lib",
@ -1986,7 +1988,7 @@ dependencies = [
[[package]]
name = "kcl-test-server"
version = "0.1.73"
version = "0.1.74"
dependencies = [
"anyhow",
"hyper 0.14.32",
@ -1999,7 +2001,7 @@ dependencies = [
[[package]]
name = "kcl-to-core"
version = "0.1.73"
version = "0.1.74"
dependencies = [
"anyhow",
"async-trait",
@ -2013,7 +2015,7 @@ dependencies = [
[[package]]
name = "kcl-wasm-lib"
version = "0.1.73"
version = "0.1.74"
dependencies = [
"anyhow",
"bson",
@ -2253,6 +2255,16 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "matrixmultiply"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]]
name = "measurements"
version = "0.11.0"
@ -2373,6 +2385,33 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "nalgebra"
version = "0.33.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b"
dependencies = [
"approx 0.5.1",
"matrixmultiply",
"num-complex",
"num-rational",
"num-traits 0.2.19",
"simba",
"typenum",
]
[[package]]
name = "nalgebra-glm"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e441f43bccdf40cb6bd4294321e6983c5bc7b9886112d19fd4c9813976b117e4"
dependencies = [
"approx 0.5.1",
"nalgebra",
"num-traits 0.2.19",
"simba",
]
[[package]]
name = "newline-converter"
version = "0.3.0"
@ -2412,6 +2451,15 @@ dependencies = [
"num-traits 0.2.19",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits 0.2.19",
]
[[package]]
name = "num-conv"
version = "0.1.0"
@ -2442,6 +2490,17 @@ dependencies = [
"num-modular",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits 0.2.19",
]
[[package]]
name = "num-traits"
version = "0.1.43"
@ -2595,6 +2654,12 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pbkdf2"
version = "0.12.2"
@ -3093,6 +3158,12 @@ dependencies = [
"getrandom 0.3.1",
]
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rayon"
version = "1.10.0"
@ -3376,6 +3447,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "safe_arch"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
dependencies = [
"bytemuck",
]
[[package]]
name = "same-file"
version = "1.0.6"
@ -3631,6 +3711,19 @@ dependencies = [
"libc",
]
[[package]]
name = "simba"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa"
dependencies = [
"approx 0.5.1",
"num-complex",
"num-traits 0.2.19",
"paste",
"wide",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
@ -4731,6 +4824,16 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "wide"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41b5576b9a81633f3e8df296ce0063042a73507636cbe956c61133dd7034ab22"
dependencies = [
"bytemuck",
"safe_arch",
]
[[package]]
name = "winapi"
version = "0.3.9"

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-bumper"
version = "0.1.73"
version = "0.1.74"
edition = "2021"
repository = "https://github.com/KittyCAD/modeling-api"
rust-version = "1.76"

View File

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

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-directory-test-macro"
description = "A tool for generating tests from a directory of kcl files"
version = "0.1.73"
version = "0.1.74"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -11,6 +11,7 @@ proc-macro = true
bench = false
[dependencies]
convert_case = "0.8.0"
proc-macro2 = "1"
quote = "1"
syn = { version = "2.0.96", features = ["full"] }

View File

@ -1,10 +1,13 @@
use std::fs;
use convert_case::Casing;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, LitStr};
/// A macro that generates test functions for each directory within a given path.
/// To be included the test directory must have a main.kcl file.
/// This will also recursively search for directories within the given path.
///
/// # Example
///
@ -45,7 +48,11 @@ pub fn test_all_dirs(attr: TokenStream, item: TokenStream) -> TokenStream {
// Generate a test function for each directory
let test_fns = dirs.iter().map(|(dir_name, dir_path)| {
let test_fn_name = format_ident!("{}_{}", fn_name, sanitize_dir_name(dir_name));
let relative_path = dir_path
.strip_prefix(&path.to_string_lossy().to_string())
.unwrap()
.trim();
let test_fn_name = format_ident!("{}_{}", fn_name, sanitize_dir_name(relative_path));
let dir_name_str = dir_name.clone();
let dir_path_str = dir_path.clone();
@ -75,16 +82,26 @@ fn get_all_directories(path: &std::path::Path) -> Result<Vec<(String, String)>,
for entry in fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
let new_path = entry.path();
if path.is_dir() && !IGNORE_DIRS.contains(&path.file_name().and_then(|name| name.to_str()).unwrap_or("")) {
let dir_name = path
if new_path.is_dir()
&& !IGNORE_DIRS.contains(&new_path.file_name().and_then(|name| name.to_str()).unwrap_or(""))
{
// Check if the directory contains a main.kcl file.
let main_kcl_path = new_path.join("main.kcl");
if !main_kcl_path.exists() {
// Recurse into the directory.
let sub_dirs = get_all_directories(&new_path)?;
dirs.extend(sub_dirs);
continue;
}
let dir_name = new_path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("unknown")
.to_string();
let dir_path = path.to_str().unwrap_or("unknown").to_string();
let dir_path = new_path.to_str().unwrap_or("unknown").to_string();
dirs.push((dir_name, dir_path));
}
@ -95,10 +112,9 @@ fn get_all_directories(path: &std::path::Path) -> Result<Vec<(String, String)>,
/// Sanitize directory name to create a valid Rust identifier
fn sanitize_dir_name(name: &str) -> String {
let name = name.replace(|c: char| !c.is_ascii_alphanumeric() && c != '_', "_");
if name.chars().next().is_some_and(|c| c.is_numeric()) {
format!("d_{}", name)
} else {
name
}
let binding = name
.replace(|c: char| !c.is_ascii_alphanumeric() && c != '_', "_")
.replace("/", "_");
let name = binding.trim_start_matches('_').to_string();
name.to_case(convert_case::Case::Snake)
}

View File

@ -1,6 +1,6 @@
[package]
name = "kcl-language-server-release"
version = "0.1.73"
version = "0.1.74"
edition = "2021"
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
publish = false

View File

@ -2,7 +2,7 @@
name = "kcl-language-server"
description = "A language server for KCL."
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
version = "0.2.73"
version = "0.2.74"
edition = "2021"
license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -1,7 +1,7 @@
[package]
name = "kcl-lib"
description = "KittyCAD Language implementation and tools"
version = "0.2.73"
version = "0.2.74"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"
@ -50,6 +50,7 @@ lazy_static = { workspace = true }
measurements = "0.11.0"
miette = { workspace = true }
mime_guess = "2.0.5"
nalgebra-glm = "0.19.0"
parse-display = "0.10.0"
pyo3 = { workspace = true, optional = true }
regex = "1.11.1"

View File

@ -1,5 +1,7 @@
//! Cache testing framework.
#[cfg(feature = "artifact-graph")]
use kcl_lib::NodePathStep;
use kcl_lib::{bust_cache, ExecError, ExecOutcome};
use kcmc::{each_cmd as mcmd, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
@ -330,6 +332,40 @@ extrude001 = extrude(profile001, length = 4)
}
}
#[cfg(feature = "artifact-graph")]
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_cache_add_offset_plane_computes_node_path() {
let code = r#"sketch001 = startSketchOn(XY)
profile001 = startProfile(sketch001, at = [0, 0])
"#;
let code_with_more = code.to_owned()
+ r#"plane001 = offsetPlane(XY, offset = 500)
"#;
let result = cache_test(
"add_offset_plane_preserves_artifact_commands",
vec![
Variation {
code,
other_files: vec![],
settings: &Default::default(),
},
Variation {
code: code_with_more.as_str(),
other_files: vec![],
settings: &Default::default(),
},
],
)
.await;
let second = &result.last().unwrap().2;
let v = second.artifact_graph.values().collect::<Vec<_>>();
let path_step = &v[2].code_ref().unwrap().node_path.steps[0];
assert_eq!(*path_step, NodePathStep::ProgramBodyItem { index: 2 });
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_cache_empty_file_pop_cache_empty_file_planes_work() {
// Get the current working directory.

View File

@ -2,7 +2,7 @@ mod cache;
use kcl_lib::{
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.
@ -441,10 +441,15 @@ async fn kcl_test_import_file_doesnt_exist() {
model = cube"#;
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!(
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([0, 18, 0])], message: "File `thing.obj` does not exist." }"#
err.backtrace(),
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"#;
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!(
result.err().unwrap().to_string(),
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([32, 70, 0])], message: "The given format does not match the file extension. Expected: `gltf`, Given: `obj`" }"#
err.message(),
"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;
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!(
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 111, 0])], message: "Cannot have an x constrained angle of 90 degrees" }"#
err.backtrace(),
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;
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!(
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 112, 0])], message: "Cannot have an x constrained angle of 270 degrees" }"#
err.backtrace(),
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;
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!(
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 110, 0])], message: "Cannot have a y constrained angle of 0 degrees" }"#
err.backtrace(),
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;
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!(
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([70, 112, 0])], message: "Cannot have a y constrained angle of 180 degrees" }"#
err.backtrace(),
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;
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!(
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([66, 116, 0])], message: "Cannot have an x constrained angle of 90 degrees" }"#
err.backtrace(),
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() {
let code = r#"sketch001 = startSketchOn(XZ)
|> 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)
|> line(end = [0, -5])
|> close(tag = $edge3)
@ -1766,10 +1804,15 @@ extrusion = extrude(sketch001, length = 10)
"#;
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!(
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([66, 116, 0])], message: "Cannot have an x constrained angle of 90 degrees" }"#
err.backtrace(),
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;
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!(
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([95, 130, 0])], message: "Cannot have a y constrained angle of 0 degrees" }"#
err.backtrace(),
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;
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!(
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([95, 132, 0])], message: "Cannot have a y constrained angle of 180 degrees" }"#
err.backtrace(),
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;
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!(
result.err().unwrap().to_string(),
r#"type: KclErrorDetails { source_ranges: [SourceRange([95, 133, 0])], message: "Cannot have a y constrained angle of 180 degrees" }"#
err.backtrace(),
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;
assert!(result.is_err());
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert_eq!(
result.err().unwrap().to_string(),
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" }"#
err.message(),
"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;
assert!(result.is_err());
assert!(result
.err()
.unwrap()
.to_string()
.contains("Please send the following object over this websocket"));
let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap();
assert!(
err.message()
.contains("Please send the following object over this websocket"),
"actual: {}",
err.message()
);
}
#[tokio::test(flavor = "multi_thread")]

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -995,7 +995,7 @@ mod tests {
let snippet = pattern_fn.to_autocomplete_snippet().unwrap();
assert_eq!(
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}], arcDegrees = ${8:3.14}, rotateDuplicates = ${9:false})"#
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}])"#
);
}

View File

@ -439,12 +439,7 @@ impl EngineManager for EngineConnection {
request_sent: tx,
})
.await
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to send debug: {}", e),
source_ranges: vec![],
})
})?;
.map_err(|e| KclError::Engine(KclErrorDetails::new(format!("Failed to send debug: {}", e), vec![])))?;
let _ = rx.await;
Ok(())
@ -479,25 +474,25 @@ impl EngineManager for EngineConnection {
})
.await
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to send modeling command: {}", e),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
format!("Failed to send modeling command: {}", e),
vec![source_range],
))
})?;
// Wait for the request to be sent.
rx.await
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("could not send request to the engine actor: {e}"),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
format!("could not send request to the engine actor: {e}"),
vec![source_range],
))
})?
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("could not send request to the engine: {e}"),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
format!("could not send request to the engine: {e}"),
vec![source_range],
))
})?;
Ok(())
@ -521,15 +516,15 @@ impl EngineManager for EngineConnection {
// Check if we have any pending errors.
let pe = self.pending_errors.read().await;
if !pe.is_empty() {
return Err(KclError::Engine(KclErrorDetails {
message: pe.join(", ").to_string(),
source_ranges: vec![source_range],
}));
return Err(KclError::Engine(KclErrorDetails::new(
pe.join(", ").to_string(),
vec![source_range],
)));
} else {
return Err(KclError::Engine(KclErrorDetails {
message: "Modeling command failed: websocket closed early".to_string(),
source_ranges: vec![source_range],
}));
return Err(KclError::Engine(KclErrorDetails::new(
"Modeling command failed: websocket closed early".to_string(),
vec![source_range],
)));
}
}
@ -548,10 +543,10 @@ impl EngineManager for EngineConnection {
}
}
Err(KclError::Engine(KclErrorDetails {
message: format!("Modeling command timed out `{}`", id),
source_ranges: vec![source_range],
}))
Err(KclError::Engine(KclErrorDetails::new(
format!("Modeling command timed out `{}`", id),
vec![source_range],
)))
}
async fn get_session_data(&self) -> Option<ModelingSessionData> {

View File

@ -147,32 +147,27 @@ impl EngineConnection {
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
) -> Result<(), KclError> {
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize source range: {:?}", e),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
format!("Failed to serialize source range: {:?}", e),
vec![source_range],
))
})?;
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize modeling command: {:?}", e),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
format!("Failed to serialize modeling command: {:?}", e),
vec![source_range],
))
})?;
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize id to source range: {:?}", e),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
format!("Failed to serialize id to source range: {:?}", e),
vec![source_range],
))
})?;
self.manager
.fire_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
Ok(())
}
@ -185,33 +180,28 @@ impl EngineConnection {
id_to_source_range: HashMap<uuid::Uuid, SourceRange>,
) -> Result<WebSocketResponse, KclError> {
let source_range_str = serde_json::to_string(&source_range).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize source range: {:?}", e),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
format!("Failed to serialize source range: {:?}", e),
vec![source_range],
))
})?;
let cmd_str = serde_json::to_string(&cmd).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize modeling command: {:?}", e),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
format!("Failed to serialize modeling command: {:?}", e),
vec![source_range],
))
})?;
let id_to_source_range_str = serde_json::to_string(&id_to_source_range).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to serialize id to source range: {:?}", e),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
format!("Failed to serialize id to source range: {:?}", e),
vec![source_range],
))
})?;
let promise = self
.manager
.send_modeling_cmd_from_wasm(id.to_string(), source_range_str, cmd_str, id_to_source_range_str)
.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| {
// Try to parse the error as an engine error.
@ -219,53 +209,52 @@ impl EngineConnection {
if let Ok(kittycad_modeling_cmds::websocket::FailureWebSocketResponse { errors, .. }) =
serde_json::from_str(&err_str)
{
KclError::Engine(KclErrorDetails {
message: errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
vec![source_range],
))
} else if let Ok(data) =
serde_json::from_str::<Vec<kittycad_modeling_cmds::websocket::FailureWebSocketResponse>>(&err_str)
{
if let Some(data) = data.first() {
// It could also be an array of responses.
KclError::Engine(KclErrorDetails {
message: data
.errors
KclError::Engine(KclErrorDetails::new(
data.errors
.iter()
.map(|e| e.message.clone())
.collect::<Vec<_>>()
.join("\n"),
source_ranges: vec![source_range],
})
vec![source_range],
))
} else {
KclError::Engine(KclErrorDetails {
message: "Received empty response from engine".into(),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
"Received empty response from engine".into(),
vec![source_range],
))
}
} else {
KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from send modeling command: {:?}", e),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
format!("Failed to wait for promise from send modeling command: {:?}", e),
vec![source_range],
))
}
})?;
if value.is_null() || value.is_undefined() {
return Err(KclError::Engine(KclErrorDetails {
message: "Received null or undefined response from engine".into(),
source_ranges: vec![source_range],
}));
return Err(KclError::Engine(KclErrorDetails::new(
"Received null or undefined response from engine".into(),
vec![source_range],
)));
}
// Convert JsValue to a Uint8Array
let data = js_sys::Uint8Array::from(value);
let ws_result: WebSocketResponse = bson::from_slice(&data.to_vec()).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to deserialize bson response from engine: {:?}", e),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
format!("Failed to deserialize bson response from engine: {:?}", e),
vec![source_range],
))
})?;
Ok(ws_result)
@ -316,18 +305,16 @@ impl crate::engine::EngineManager for EngineConnection {
*self.default_planes.write().await = Some(new_planes);
// Start a new session.
let promise = self.manager.start_new_session().map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
let promise = self
.manager
.start_new_session()
.map_err(|e| KclError::Engine(KclErrorDetails::new(e.to_string().into(), vec![source_range])))?;
crate::wasm::JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from start new session: {:?}", e),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
format!("Failed to wait for promise from start new session: {:?}", e),
vec![source_range],
))
})?;
Ok(())

View File

@ -276,10 +276,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
{
let duration = instant::Duration::from_millis(1);
wasm_timer::Delay::new(duration).await.map_err(|err| {
KclError::Internal(KclErrorDetails {
message: format!("Failed to sleep: {:?}", err),
source_ranges: vec![source_range],
})
KclError::Internal(KclErrorDetails::new(
format!("Failed to sleep: {:?}", err),
vec![source_range],
))
})?;
}
#[cfg(not(target_arch = "wasm32"))]
@ -293,10 +293,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
return Ok(response);
}
Err(KclError::Engine(KclErrorDetails {
message: "async command timed out".to_string(),
source_ranges: vec![source_range],
}))
Err(KclError::Engine(KclErrorDetails::new(
"async command timed out".to_string(),
vec![source_range],
)))
}
/// 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);
}
_ => {
return Err(KclError::Engine(KclErrorDetails {
message: format!("The request is not a modeling command: {:?}", req),
source_ranges: vec![*range],
}));
return Err(KclError::Engine(KclErrorDetails::new(
format!("The request is not a modeling command: {:?}", req),
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)
} else {
// We should never get here.
Err(KclError::Engine(KclErrorDetails {
message: format!("Failed to get batch response: {:?}", response),
source_ranges: vec![source_range],
}))
Err(KclError::Engine(KclErrorDetails::new(
format!("Failed to get batch response: {:?}", response),
vec![source_range],
)))
}
}
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
// an error.
let source_range = id_to_source_range.get(cmd_id.as_ref()).cloned().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to get source range for command ID: {:?}", cmd_id),
source_ranges: vec![],
})
KclError::Engine(KclErrorDetails::new(
format!("Failed to get source range for command ID: {:?}", cmd_id),
vec![],
))
})?;
let ws_resp = self
.inner_send_modeling_cmd(cmd_id.into(), source_range, final_req, id_to_source_range)
.await?;
self.parse_websocket_response(ws_resp, source_range)
}
_ => Err(KclError::Engine(KclErrorDetails {
message: format!("The final request is not a modeling command: {:?}", final_req),
source_ranges: vec![source_range],
})),
_ => Err(KclError::Engine(KclErrorDetails::new(
format!("The final request is not a modeling command: {:?}", final_req),
vec![source_range],
))),
}
}
@ -729,10 +729,10 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
for (name, plane_id, color) in plane_settings {
let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
// We should never get here.
KclError::Engine(KclErrorDetails {
message: format!("Failed to get default plane info for: {:?}", name),
source_ranges: vec![source_range],
})
KclError::Engine(KclErrorDetails::new(
format!("Failed to get default plane info for: {:?}", name),
vec![source_range],
))
})?;
planes.insert(
name,
@ -763,15 +763,14 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
WebSocketResponse::Success(success) => Ok(success.resp),
WebSocketResponse::Failure(fail) => {
let _request_id = fail.request_id;
Err(KclError::Engine(KclErrorDetails {
message: fail
.errors
Err(KclError::Engine(KclErrorDetails::new(
fail.errors
.iter()
.map(|e| e.message.clone())
.collect::<Vec<_>>()
.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 } => {
// Get the source range for the command.
let source_range = id_to_source_range.get(cmd_id).cloned().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to get source range for command ID: {:?}", cmd_id),
source_ranges: vec![],
})
KclError::Engine(KclErrorDetails::new(
format!("Failed to get source range for command ID: {:?}", cmd_id),
vec![],
))
})?;
return Err(KclError::Engine(KclErrorDetails {
message: errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
source_ranges: vec![source_range],
}));
return Err(KclError::Engine(KclErrorDetails::new(
errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("\n"),
vec![source_range],
)));
}
}
}
// Return an error that we did not get an error or the response we wanted.
// This should never happen but who knows.
Err(KclError::Engine(KclErrorDetails {
message: format!("Failed to find response for command ID: {:?}", id),
source_ranges: vec![],
}))
Err(KclError::Engine(KclErrorDetails::new(
format!("Failed to find response for command ID: {:?}", id),
vec![],
)))
}
async fn modify_grid(

View File

@ -380,20 +380,39 @@ impl miette::Diagnostic for Report {
}
#[derive(Debug, Serialize, Deserialize, ts_rs::TS, Clone, PartialEq, Eq, thiserror::Error, miette::Diagnostic)]
#[serde(rename_all = "camelCase")]
#[error("{message}")]
#[ts(export)]
pub struct KclErrorDetails {
#[serde(rename = "sourceRanges")]
#[label(collection, "Errors")]
pub source_ranges: Vec<SourceRange>,
pub backtrace: Vec<BacktraceItem>,
#[serde(rename = "msg")]
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 {
pub fn internal(message: String) -> KclError {
KclError::Internal(KclErrorDetails {
source_ranges: Default::default(),
backtrace: Default::default(),
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 {
let mut new = self.clone();
match &mut new {
KclError::Lexical(e) => e.source_ranges = source_ranges,
KclError::Syntax(e) => e.source_ranges = source_ranges,
KclError::Semantic(e) => e.source_ranges = source_ranges,
KclError::ImportCycle(e) => e.source_ranges = source_ranges,
KclError::Type(e) => e.source_ranges = source_ranges,
KclError::Io(e) => e.source_ranges = source_ranges,
KclError::Unexpected(e) => e.source_ranges = source_ranges,
KclError::ValueAlreadyDefined(e) => e.source_ranges = source_ranges,
KclError::UndefinedValue(e) => e.source_ranges = source_ranges,
KclError::InvalidExpression(e) => e.source_ranges = source_ranges,
KclError::Engine(e) => e.source_ranges = source_ranges,
KclError::Internal(e) => e.source_ranges = source_ranges,
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 = source_ranges
.iter()
.map(|s| BacktraceItem {
source_range: *s,
fn_name: None,
})
.collect();
e.source_ranges = source_ranges;
}
}
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();
match &mut new {
KclError::Lexical(e) => e.source_ranges.extend(source_ranges),
KclError::Syntax(e) => e.source_ranges.extend(source_ranges),
KclError::Semantic(e) => e.source_ranges.extend(source_ranges),
KclError::ImportCycle(e) => e.source_ranges.extend(source_ranges),
KclError::Type(e) => e.source_ranges.extend(source_ranges),
KclError::Io(e) => e.source_ranges.extend(source_ranges),
KclError::Unexpected(e) => e.source_ranges.extend(source_ranges),
KclError::ValueAlreadyDefined(e) => e.source_ranges.extend(source_ranges),
KclError::UndefinedValue(e) => e.source_ranges.extend(source_ranges),
KclError::InvalidExpression(e) => e.source_ranges.extend(source_ranges),
KclError::Engine(e) => e.source_ranges.extend(source_ranges),
KclError::Internal(e) => e.source_ranges.extend(source_ranges),
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;
}
}
}
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 {
@ -551,6 +647,7 @@ impl From<pyo3::PyErr> for KclError {
fn from(error: pyo3::PyErr) -> Self {
KclError::Internal(KclErrorDetails {
source_ranges: vec![],
backtrace: Default::default(),
message: error.to_string(),
})
}
@ -629,8 +726,13 @@ impl CompilationError {
impl From<CompilationError> for KclErrorDetails {
fn from(err: CompilationError) -> Self {
let backtrace = vec![BacktraceItem {
source_range: err.source_range,
fn_name: None,
}];
KclErrorDetails {
source_ranges: vec![err.source_range],
backtrace,
message: err.message,
}
}

View File

@ -70,10 +70,10 @@ pub(super) fn expect_properties<'a>(
) -> Result<&'a [Node<ObjectProperty>], KclError> {
assert_eq!(annotation.name().unwrap(), for_key);
Ok(&**annotation.properties.as_ref().ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Empty `{for_key}` annotation"),
source_ranges: vec![annotation.as_source_range()],
})
KclError::Semantic(KclErrorDetails::new(
format!("Empty `{for_key}` annotation"),
vec![annotation.as_source_range()],
))
})?)
}
@ -84,10 +84,10 @@ pub(super) fn expect_ident(expr: &Expr) -> Result<&str, KclError> {
}
}
Err(KclError::Semantic(KclErrorDetails {
message: "Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
source_ranges: vec![expr.into()],
}))
Err(KclError::Semantic(KclErrorDetails::new(
"Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
vec![expr.into()],
)))
}
// Returns the unparsed number literal.
@ -98,10 +98,10 @@ pub(super) fn expect_number(expr: &Expr) -> Result<String, KclError> {
}
}
Err(KclError::Semantic(KclErrorDetails {
message: "Unexpected settings value, expected a number, e.g., `1.0`".to_owned(),
source_ranges: vec![expr.into()],
}))
Err(KclError::Semantic(KclErrorDetails::new(
"Unexpected settings value, expected a number, e.g., `1.0`".to_owned(),
vec![expr.into()],
)))
}
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 let Some(s) = p.value.ident_name() {
return Impl::from_str(s).map(Some).map_err(|_| {
KclError::Semantic(KclErrorDetails {
message: format!(
KclError::Semantic(KclErrorDetails::new(
format!(
"Invalid value for {} attribute, expected one of: {}",
IMPL,
IMPL_VALUES.join(", ")
),
source_ranges: vec![source_range],
})
vec![source_range],
))
});
}
}
@ -139,12 +139,12 @@ impl UnitLen {
"inch" | "in" => Ok(UnitLen::Inches),
"ft" => Ok(UnitLen::Feet),
"yd" => Ok(UnitLen::Yards),
value => Err(KclError::Semantic(KclErrorDetails {
message: format!(
value => Err(KclError::Semantic(KclErrorDetails::new(
format!(
"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 {
"deg" => Ok(UnitAngle::Degrees),
"rad" => Ok(UnitAngle::Radians),
value => Err(KclError::Semantic(KclErrorDetails {
message: format!("Unexpected value for angle units: `{value}`; expected one of `deg`, `rad`"),
source_ranges: vec![source_range],
})),
value => Err(KclError::Semantic(KclErrorDetails::new(
format!("Unexpected value for angle units: `{value}`; expected one of `deg`, `rad`"),
vec![source_range],
))),
}
}
}

View File

@ -2,18 +2,17 @@ use fnv::FnvHashMap;
use indexmap::IndexMap;
use kittycad_modeling_cmds::{
self as kcmc,
id::ModelingCmdId,
ok_response::OkModelingCmdResponse,
shared::ExtrusionFaceCapType,
websocket::{BatchResponse, OkWebSocketResponseData, WebSocketResponse},
EnableSketchMode, ModelingCmd,
};
use schemars::JsonSchema;
use serde::{ser::SerializeSeq, Serialize};
use uuid::Uuid;
use crate::{
errors::KclErrorDetails,
execution::ArtifactId,
parsing::ast::types::{Node, Program},
KclError, NodePath, SourceRange,
};
@ -58,52 +57,6 @@ impl PartialOrd for ArtifactCommand {
}
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS, JsonSchema)]
#[ts(export_to = "Artifact.ts")]
pub struct ArtifactId(Uuid);
impl ArtifactId {
pub fn new(uuid: Uuid) -> Self {
Self(uuid)
}
}
impl From<Uuid> for ArtifactId {
fn from(uuid: Uuid) -> Self {
Self::new(uuid)
}
}
impl From<&Uuid> for ArtifactId {
fn from(uuid: &Uuid) -> Self {
Self::new(*uuid)
}
}
impl From<ArtifactId> for Uuid {
fn from(id: ArtifactId) -> Self {
id.0
}
}
impl From<&ArtifactId> for Uuid {
fn from(id: &ArtifactId) -> Self {
id.0
}
}
impl From<ModelingCmdId> for ArtifactId {
fn from(id: ModelingCmdId) -> Self {
Self::new(*id.as_ref())
}
}
impl From<&ModelingCmdId> for ArtifactId {
fn from(id: &ModelingCmdId) -> Self {
Self::new(*id.as_ref())
}
}
pub type DummyPathToNode = Vec<()>;
fn serialize_dummy_path_to_node<S>(_path_to_node: &DummyPathToNode, serializer: S) -> Result<S::Ok, S::Error>
@ -750,6 +703,7 @@ pub(super) fn build_artifact_graph(
artifact_commands: &[ArtifactCommand],
responses: &IndexMap<Uuid, WebSocketResponse>,
ast: &Node<Program>,
cached_body_items: usize,
exec_artifacts: &mut IndexMap<ArtifactId, Artifact>,
initial_graph: ArtifactGraph,
) -> Result<ArtifactGraph, KclError> {
@ -763,7 +717,7 @@ pub(super) fn build_artifact_graph(
for exec_artifact in exec_artifacts.values_mut() {
// Note: We only have access to the new AST. So if these artifacts
// somehow came from cached AST, this won't fill in anything.
fill_in_node_paths(exec_artifact, ast);
fill_in_node_paths(exec_artifact, ast, cached_body_items);
}
for artifact_command in artifact_commands {
@ -790,6 +744,7 @@ pub(super) fn build_artifact_graph(
&flattened_responses,
&path_to_plane_id_map,
ast,
cached_body_items,
exec_artifacts,
)?;
for artifact in artifact_updates {
@ -807,16 +762,18 @@ pub(super) fn build_artifact_graph(
/// These may have been created with placeholder `CodeRef`s because we didn't
/// have the entire AST available. Now we fill them in.
fn fill_in_node_paths(artifact: &mut Artifact, program: &Node<Program>) {
fn fill_in_node_paths(artifact: &mut Artifact, program: &Node<Program>, cached_body_items: usize) {
match artifact {
Artifact::StartSketchOnFace(face) => {
if face.code_ref.node_path.is_empty() {
face.code_ref.node_path = NodePath::from_range(program, face.code_ref.range).unwrap_or_default();
face.code_ref.node_path =
NodePath::from_range(program, cached_body_items, face.code_ref.range).unwrap_or_default();
}
}
Artifact::StartSketchOnPlane(plane) => {
if plane.code_ref.node_path.is_empty() {
plane.code_ref.node_path = NodePath::from_range(program, plane.code_ref.range).unwrap_or_default();
plane.code_ref.node_path =
NodePath::from_range(program, cached_body_items, plane.code_ref.range).unwrap_or_default();
}
}
_ => {}
@ -905,6 +862,7 @@ fn artifacts_to_update(
responses: &FnvHashMap<Uuid, OkModelingCmdResponse>,
path_to_plane_id_map: &FnvHashMap<Uuid, Uuid>,
ast: &Node<Program>,
cached_body_items: usize,
exec_artifacts: &IndexMap<ArtifactId, Artifact>,
) -> Result<Vec<Artifact>, KclError> {
let uuid = artifact_command.cmd_id;
@ -918,7 +876,7 @@ fn artifacts_to_update(
// correct value based on NodePath.
let path_to_node = Vec::new();
let range = artifact_command.range;
let node_path = NodePath::from_range(ast, range).unwrap_or_default();
let node_path = NodePath::from_range(ast, cached_body_items, range).unwrap_or_default();
let code_ref = CodeRef {
range,
node_path,
@ -983,12 +941,10 @@ fn artifacts_to_update(
ModelingCmd::StartPath(_) => {
let mut return_arr = Vec::new();
let current_plane_id = path_to_plane_id_map.get(&artifact_command.cmd_id).ok_or_else(|| {
KclError::Internal(KclErrorDetails {
message: format!(
"Expected a current plane ID when processing StartPath command, but we have none: {id:?}"
),
source_ranges: vec![range],
})
KclError::Internal(KclErrorDetails::new(
format!("Expected a current plane ID when processing StartPath command, but we have none: {id:?}"),
vec![range],
))
})?;
return_arr.push(Artifact::Path(Path {
id,
@ -1107,10 +1063,10 @@ fn artifacts_to_update(
// TODO: Using the first one. Make sure to revisit this
// choice, don't think it matters for now.
path_id: ArtifactId::new(*loft_cmd.section_ids.first().ok_or_else(|| {
KclError::Internal(KclErrorDetails {
message: format!("Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"),
source_ranges: vec![range],
})
KclError::Internal(KclErrorDetails::new(
format!("Expected at least one section ID in Loft command: {id:?}; cmd={cmd:?}"),
vec![range],
))
})?),
surface_ids: Vec::new(),
edge_ids: Vec::new(),
@ -1150,12 +1106,12 @@ fn artifacts_to_update(
};
last_path = Some(path);
let path_sweep_id = path.sweep_id.ok_or_else(|| {
KclError::Internal(KclErrorDetails {
message:format!(
KclError::Internal(KclErrorDetails::new(
format!(
"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| {
if let Artifact::StartSketchOnFace(s) = a {
@ -1204,12 +1160,12 @@ fn artifacts_to_update(
continue;
};
let path_sweep_id = path.sweep_id.ok_or_else(|| {
KclError::Internal(KclErrorDetails {
message:format!(
KclError::Internal(KclErrorDetails::new(
format!(
"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| {
if let Artifact::StartSketchOnFace(s) = a {

View File

@ -308,6 +308,11 @@ impl ArtifactGraph {
// a child of the line above it.
let label = label.unwrap_or("");
if code_ref.node_path.is_empty() {
if !code_ref.range.module_id().is_top_level() {
// This is pointing to another module. We don't care about
// these. It's okay that it's missing, for now.
return Ok(());
}
return writeln!(output, "{prefix} %% {label}Missing NodePath");
}
writeln!(output, "{prefix} %% {label}{:?}", code_ref.node_path.steps)

View File

@ -79,6 +79,9 @@ pub(super) enum CacheResult {
reapply_settings: bool,
/// The program that needs to be executed.
program: Node<Program>,
/// The number of body items that were cached and omitted from the
/// program that needs to be executed. Used to compute [`crate::NodePath`].
cached_body_items: usize,
},
/// Check only the imports, and not the main program.
/// Before sending this we already checked the main program and it is the same.
@ -191,6 +194,7 @@ pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInf
clear_scene: true,
reapply_settings: true,
program: new.ast.clone(),
cached_body_items: 0,
};
}
@ -219,6 +223,7 @@ fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>,
clear_scene: true,
reapply_settings,
program: new_ast,
cached_body_items: 0,
};
}
@ -239,6 +244,7 @@ fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>,
clear_scene: true,
reapply_settings,
program: new_ast,
cached_body_items: 0,
}
}
std::cmp::Ordering::Greater => {
@ -255,6 +261,7 @@ fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>,
clear_scene: false,
reapply_settings,
program: new_ast,
cached_body_items: old_ast.body.len(),
}
}
std::cmp::Ordering::Equal => {
@ -592,7 +599,8 @@ startSketchOn(XY)
CacheResult::ReExecute {
clear_scene: true,
reapply_settings: true,
program: new_program.ast
program: new_program.ast,
cached_body_items: 0,
}
);
}
@ -630,7 +638,8 @@ startSketchOn(XY)
CacheResult::ReExecute {
clear_scene: true,
reapply_settings: true,
program: new_program.ast
program: new_program.ast,
cached_body_items: 0,
}
);
}

View File

@ -3,7 +3,7 @@ use schemars::JsonSchema;
use serde::Serialize;
use super::{types::NumericType, ArtifactId, KclValue};
use crate::{docs::StdLibFn, ModuleId, SourceRange};
use crate::{ModuleId, SourceRange};
/// A CAD modeling operation for display in the feature tree, AKA operations
/// timeline.
@ -13,21 +13,6 @@ use crate::{docs::StdLibFn, ModuleId, SourceRange};
pub enum Operation {
#[serde(rename_all = "camelCase")]
StdLibCall {
/// The standard library function being called.
#[serde(flatten)]
std_lib_fn: StdLibFnRef,
/// The unlabeled argument to the function.
unlabeled_arg: Option<OpArg>,
/// The labeled keyword arguments to the function.
labeled_args: IndexMap<String, OpArg>,
/// The source range of the operation in the source code.
source_range: SourceRange,
/// True if the operation resulted in an error.
#[serde(default, skip_serializing_if = "is_false")]
is_error: bool,
},
#[serde(rename_all = "camelCase")]
KclStdLibCall {
name: String,
/// The unlabeled argument to the function.
unlabeled_arg: Option<OpArg>,
@ -57,19 +42,12 @@ impl PartialOrd for Operation {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(match (self, other) {
(Self::StdLibCall { source_range: a, .. }, Self::StdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::StdLibCall { source_range: a, .. }, Self::KclStdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::StdLibCall { source_range: a, .. }, Self::GroupBegin { source_range: b, .. }) => a.cmp(b),
(Self::StdLibCall { .. }, Self::GroupEnd) => std::cmp::Ordering::Less,
(Self::KclStdLibCall { source_range: a, .. }, Self::KclStdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::KclStdLibCall { source_range: a, .. }, Self::StdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::KclStdLibCall { source_range: a, .. }, Self::GroupBegin { source_range: b, .. }) => a.cmp(b),
(Self::KclStdLibCall { .. }, Self::GroupEnd) => std::cmp::Ordering::Less,
(Self::GroupBegin { source_range: a, .. }, Self::GroupBegin { source_range: b, .. }) => a.cmp(b),
(Self::GroupBegin { source_range: a, .. }, Self::StdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::GroupBegin { source_range: a, .. }, Self::KclStdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::GroupBegin { .. }, Self::GroupEnd) => std::cmp::Ordering::Less,
(Self::GroupEnd, Self::StdLibCall { .. }) => std::cmp::Ordering::Greater,
(Self::GroupEnd, Self::KclStdLibCall { .. }) => std::cmp::Ordering::Greater,
(Self::GroupEnd, Self::GroupBegin { .. }) => std::cmp::Ordering::Greater,
(Self::GroupEnd, Self::GroupEnd) => std::cmp::Ordering::Equal,
})
@ -81,7 +59,6 @@ impl Operation {
pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) {
match self {
Self::StdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::KclStdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::GroupBegin { .. } | Self::GroupEnd => {}
}
}
@ -107,6 +84,7 @@ pub enum Group {
labeled_args: IndexMap<String, OpArg>,
},
/// A whole-module import use.
#[allow(dead_code)]
#[serde(rename_all = "camelCase")]
ModuleInstance {
/// The name of the module being used.
@ -135,54 +113,6 @@ impl OpArg {
}
}
/// A reference to a standard library function. This exists to implement
/// `PartialEq` and `Eq` for `Operation`.
#[derive(Debug, Clone, Serialize, ts_rs::TS, JsonSchema)]
#[ts(export_to = "Operation.ts")]
#[serde(rename_all = "camelCase")]
pub struct StdLibFnRef {
// The following doc comment gets inlined into Operation, overriding what's
// there, in the generated TS. We serialize to its name. Renaming the
// field to "name" allows it to match the other variant.
/// The standard library function being called.
#[serde(
rename = "name",
serialize_with = "std_lib_fn_name",
deserialize_with = "std_lib_fn_from_name"
)]
#[ts(type = "string", rename = "name")]
pub std_lib_fn: Box<dyn StdLibFn>,
}
impl StdLibFnRef {
pub(crate) fn new(std_lib_fn: Box<dyn StdLibFn>) -> Self {
Self { std_lib_fn }
}
}
impl From<&Box<dyn StdLibFn>> for StdLibFnRef {
fn from(std_lib_fn: &Box<dyn StdLibFn>) -> Self {
Self::new(std_lib_fn.clone())
}
}
impl PartialEq for StdLibFnRef {
fn eq(&self, other: &Self) -> bool {
self.std_lib_fn.name() == other.std_lib_fn.name()
}
}
impl Eq for StdLibFnRef {}
#[expect(clippy::borrowed_box, reason = "Explicit Box is needed for serde")]
fn std_lib_fn_name<S>(std_lib_fn: &Box<dyn StdLibFn>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let name = std_lib_fn.name();
serializer.serialize_str(&name)
}
fn is_false(b: &bool) -> bool {
!*b
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,988 @@
use async_recursion::async_recursion;
use indexmap::IndexMap;
use super::{types::ArrayLen, EnvironmentRef};
use crate::{
docs::StdLibFn,
errors::{KclError, KclErrorDetails},
execution::{
cad_op::{Group, OpArg, OpKclValue, Operation},
kcl_value::FunctionSource,
memory,
types::RuntimeType,
BodyType, ExecState, ExecutorContext, KclValue, Metadata, StatementKind, TagEngineInfo, TagIdentifier,
},
parsing::ast::types::{CallExpressionKw, DefaultParamVal, FunctionExpression, Node, Program, Type},
source_range::SourceRange,
std::StdFn,
CompilationError,
};
#[derive(Debug, Clone)]
pub struct Args {
/// Positional args.
pub args: Vec<Arg>,
/// Keyword arguments
pub kw_args: KwArgs,
pub source_range: SourceRange,
pub ctx: ExecutorContext,
/// If this call happens inside a pipe (|>) expression, this holds the LHS of that |>.
/// Otherwise it's None.
pub pipe_value: Option<Arg>,
}
impl Args {
pub fn new(args: Vec<Arg>, source_range: SourceRange, ctx: ExecutorContext, pipe_value: Option<Arg>) -> Self {
Self {
args,
kw_args: Default::default(),
source_range,
ctx,
pipe_value,
}
}
/// Collect the given keyword arguments.
pub fn new_kw(kw_args: KwArgs, source_range: SourceRange, ctx: ExecutorContext, pipe_value: Option<Arg>) -> Self {
Self {
args: Default::default(),
kw_args,
source_range,
ctx,
pipe_value,
}
}
/// Get the unlabeled keyword argument. If not set, returns None.
pub(crate) fn unlabeled_kw_arg_unconverted(&self) -> Option<&Arg> {
self.kw_args
.unlabeled
.as_ref()
.map(|(_, a)| a)
.or(self.args.first())
.or(self.pipe_value.as_ref())
}
}
#[derive(Debug, Clone)]
pub struct Arg {
/// The evaluated argument.
pub value: KclValue,
/// The source range of the unevaluated argument.
pub source_range: SourceRange,
}
impl Arg {
pub fn new(value: KclValue, source_range: SourceRange) -> Self {
Self { value, source_range }
}
pub fn synthetic(value: KclValue) -> Self {
Self {
value,
source_range: SourceRange::synthetic(),
}
}
pub fn source_ranges(&self) -> Vec<SourceRange> {
vec![self.source_range]
}
}
#[derive(Debug, Clone, Default)]
pub struct KwArgs {
/// Unlabeled keyword args. Currently only the first arg can be unlabeled.
/// If the argument was a local variable, then the first element of the tuple is its name
/// which may be used to treat this arg as a labelled arg.
pub unlabeled: Option<(Option<String>, Arg)>,
/// Labeled args.
pub labeled: IndexMap<String, Arg>,
pub errors: Vec<Arg>,
}
impl KwArgs {
/// How many arguments are there?
pub fn len(&self) -> usize {
self.labeled.len() + if self.unlabeled.is_some() { 1 } else { 0 }
}
/// Are there no arguments?
pub fn is_empty(&self) -> bool {
self.labeled.len() == 0 && self.unlabeled.is_none()
}
}
struct FunctionDefinition<'a> {
input_arg: Option<(String, Option<Type>)>,
named_args: IndexMap<String, (Option<DefaultParamVal>, Option<Type>)>,
return_type: Option<Node<Type>>,
deprecated: bool,
include_in_feature_tree: bool,
is_std: bool,
body: FunctionBody<'a>,
}
#[derive(Debug)]
enum FunctionBody<'a> {
Rust(StdFn),
Kcl(&'a Node<Program>, EnvironmentRef),
}
impl<'a> From<&'a FunctionSource> for FunctionDefinition<'a> {
fn from(value: &'a FunctionSource) -> Self {
#[allow(clippy::type_complexity)]
fn args_from_ast(
ast: &FunctionExpression,
) -> (
Option<(String, Option<Type>)>,
IndexMap<String, (Option<DefaultParamVal>, Option<Type>)>,
) {
let mut input_arg = None;
let mut named_args = IndexMap::new();
for p in &ast.params {
if !p.labeled {
input_arg = Some((p.identifier.name.clone(), p.type_.as_ref().map(|t| t.inner.clone())));
continue;
}
named_args.insert(
p.identifier.name.clone(),
(p.default_value.clone(), p.type_.as_ref().map(|t| t.inner.clone())),
);
}
(input_arg, named_args)
}
match value {
FunctionSource::Std { func, ast, props } => {
let (input_arg, named_args) = args_from_ast(ast);
FunctionDefinition {
input_arg,
named_args,
return_type: ast.return_type.clone(),
deprecated: props.deprecated,
include_in_feature_tree: props.include_in_feature_tree,
is_std: true,
body: FunctionBody::Rust(*func),
}
}
FunctionSource::User { ast, memory, .. } => {
let (input_arg, named_args) = args_from_ast(ast);
FunctionDefinition {
input_arg,
named_args,
return_type: ast.return_type.clone(),
deprecated: false,
include_in_feature_tree: true,
// TODO I think this might be wrong for pure Rust std functions
is_std: false,
body: FunctionBody::Kcl(&ast.body, *memory),
}
}
FunctionSource::None => unreachable!(),
}
}
}
impl From<&dyn StdLibFn> for FunctionDefinition<'static> {
fn from(value: &dyn StdLibFn) -> Self {
let mut input_arg = None;
let mut named_args = IndexMap::new();
for a in value.args(false) {
if !a.label_required {
input_arg = Some((a.name.clone(), None));
continue;
}
named_args.insert(
a.name.clone(),
(
if a.required {
None
} else {
Some(DefaultParamVal::none())
},
None,
),
);
}
FunctionDefinition {
input_arg,
named_args,
return_type: None,
deprecated: value.deprecated(),
include_in_feature_tree: value.feature_tree_operation(),
is_std: true,
body: FunctionBody::Rust(value.std_lib_fn()),
}
}
}
impl Node<CallExpressionKw> {
#[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let fn_name = &self.callee;
let callsite: SourceRange = self.into();
// Build a hashmap from argument labels to the final evaluated values.
let mut fn_args = IndexMap::with_capacity(self.arguments.len());
let mut errors = Vec::new();
for arg_expr in &self.arguments {
let source_range = SourceRange::from(arg_expr.arg.clone());
let metadata = Metadata { source_range };
let value = ctx
.execute_expr(&arg_expr.arg, exec_state, &metadata, &[], StatementKind::Expression)
.await?;
let arg = Arg::new(value, source_range);
match &arg_expr.label {
Some(l) => {
fn_args.insert(l.name.clone(), arg);
}
None => {
if let Some(id) = arg_expr.arg.ident_name() {
fn_args.insert(id.to_owned(), arg);
} else {
errors.push(arg);
}
}
}
}
// Evaluate the unlabeled first param, if any exists.
let unlabeled = if let Some(ref arg_expr) = self.unlabeled {
let source_range = SourceRange::from(arg_expr.clone());
let metadata = Metadata { source_range };
let value = ctx
.execute_expr(arg_expr, exec_state, &metadata, &[], StatementKind::Expression)
.await?;
let label = arg_expr.ident_name().map(str::to_owned);
Some((label, Arg::new(value, source_range)))
} else {
None
};
let args = Args::new_kw(
KwArgs {
unlabeled,
labeled: fn_args,
errors,
},
self.into(),
ctx.clone(),
exec_state.pipe_value().map(|v| Arg::new(v.clone(), callsite)),
);
match ctx.stdlib.get_rust_function(fn_name) {
Some(func) => {
let def: FunctionDefinition = (&*func).into();
// All std lib functions return a value, so the unwrap is safe.
def.call_kw(Some(func.name()), exec_state, ctx, args, callsite)
.await
.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 => {
// Clone the function so that we can use a mutable reference to
// exec_state.
let func = fn_name.get_result(exec_state, ctx).await?.clone();
let Some(fn_src) = func.as_fn() else {
return Err(KclError::Semantic(KclErrorDetails::new(
"cannot call this because it isn't a function".to_string(),
vec![callsite],
)));
};
let return_value = fn_src
.call_kw(Some(fn_name.to_string()), exec_state, ctx, args, callsite)
.await
.map_err(|e| {
// Add the call expression to the source ranges.
//
// 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 mut source_ranges: Vec<SourceRange> = vec![callsite];
// We want to send the source range of the original function.
if let KclValue::Function { meta, .. } = func {
source_ranges = meta.iter().map(|m| m.source_range).collect();
};
KclError::UndefinedValue(KclErrorDetails::new(
format!("Result of user-defined function {} is undefined", fn_name),
source_ranges,
))
})?;
Ok(result)
}
}
}
}
impl FunctionDefinition<'_> {
pub async fn call_kw(
&self,
fn_name: Option<String>,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
mut args: Args,
callsite: SourceRange,
) -> Result<Option<KclValue>, KclError> {
if self.deprecated {
exec_state.warn(CompilationError::err(
callsite,
format!(
"{} is deprecated, see the docs for a recommended replacement",
match &fn_name {
Some(n) => format!("`{n}`"),
None => "This function".to_owned(),
}
),
));
}
type_check_params_kw(fn_name.as_deref(), self, &mut args.kw_args, exec_state)?;
// Don't early return until the stack frame is popped!
self.body.prep_mem(exec_state);
let op = if self.include_in_feature_tree {
let op_labeled_args = args
.kw_args
.labeled
.iter()
.map(|(k, arg)| (k.clone(), OpArg::new(OpKclValue::from(&arg.value), arg.source_range)))
.collect();
if self.is_std {
Some(Operation::StdLibCall {
name: fn_name.clone().unwrap_or_else(|| "unknown function".to_owned()),
unlabeled_arg: args
.unlabeled_kw_arg_unconverted()
.map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
labeled_args: op_labeled_args,
source_range: callsite,
is_error: false,
})
} else {
exec_state.push_op(Operation::GroupBegin {
group: Group::FunctionCall {
name: fn_name.clone(),
function_source_range: self.as_source_range().unwrap(),
unlabeled_arg: args
.kw_args
.unlabeled
.as_ref()
.map(|arg| OpArg::new(OpKclValue::from(&arg.1.value), arg.1.source_range)),
labeled_args: op_labeled_args,
},
source_range: callsite,
});
None
}
} else {
None
};
let mut result = match &self.body {
FunctionBody::Rust(f) => f(exec_state, args).await.map(Some),
FunctionBody::Kcl(f, _) => {
if let Err(e) = assign_args_to_params_kw(self, args, exec_state) {
exec_state.mut_stack().pop_env();
return Err(e);
}
ctx.exec_block(f, exec_state, BodyType::Block).await.map(|_| {
exec_state
.stack()
.get(memory::RETURN_NAME, f.as_source_range())
.ok()
.cloned()
})
}
};
exec_state.mut_stack().pop_env();
if let Some(mut op) = op {
op.set_std_lib_call_is_error(result.is_err());
// Track call operation. We do this after the call
// since things like patternTransform may call user code
// before running, and we will likely want to use the
// return value. The call takes ownership of the args,
// so we need to build the op before the call.
exec_state.push_op(op);
} else if !self.is_std {
exec_state.push_op(Operation::GroupEnd);
}
if self.is_std {
if let Ok(Some(result)) = &mut result {
update_memory_for_tags_of_geometry(result, exec_state)?;
}
}
coerce_result_type(result, self, exec_state)
}
// Postcondition: result.is_some() if function is not in the standard library.
fn as_source_range(&self) -> Option<SourceRange> {
match &self.body {
FunctionBody::Rust(_) => None,
FunctionBody::Kcl(p, _) => Some(p.as_source_range()),
}
}
}
impl FunctionBody<'_> {
fn prep_mem(&self, exec_state: &mut ExecState) {
match self {
FunctionBody::Rust(_) => exec_state.mut_stack().push_new_env_for_rust_call(),
FunctionBody::Kcl(_, memory) => exec_state.mut_stack().push_new_env_for_call(*memory),
}
}
}
impl FunctionSource {
pub async fn call_kw(
&self,
fn_name: Option<String>,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
args: Args,
callsite: SourceRange,
) -> Result<Option<KclValue>, KclError> {
let def: FunctionDefinition = self.into();
def.call_kw(fn_name, exec_state, ctx, args, callsite).await
}
}
fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut ExecState) -> Result<(), KclError> {
// If the return result is a sketch or solid, we want to update the
// memory for the tags of the group.
// TODO: This could probably be done in a better way, but as of now this was my only idea
// and it works.
match result {
KclValue::Sketch { value } => {
for (name, tag) in value.tags.iter() {
if exec_state.stack().cur_frame_contains(name) {
exec_state.mut_stack().update(name, |v, _| {
v.as_mut_tag().unwrap().merge_info(tag);
});
} else {
exec_state
.mut_stack()
.add(
name.to_owned(),
KclValue::TagIdentifier(Box::new(tag.clone())),
SourceRange::default(),
)
.unwrap();
}
}
}
KclValue::Solid { ref mut value } => {
for v in &value.value {
if let Some(tag) = v.get_tag() {
// Get the past tag and update it.
let tag_id = if let Some(t) = value.sketch.tags.get(&tag.name) {
let mut t = t.clone();
let Some(info) = t.get_cur_info() else {
return Err(KclError::Internal(KclErrorDetails::new(
format!("Tag {} does not have path info", tag.name),
vec![tag.into()],
)));
};
let mut info = info.clone();
info.surface = Some(v.clone());
info.sketch = value.id;
t.info.push((exec_state.stack().current_epoch(), info));
t
} else {
// It's probably a fillet or a chamfer.
// Initialize it.
TagIdentifier {
value: tag.name.clone(),
info: vec![(
exec_state.stack().current_epoch(),
TagEngineInfo {
id: v.get_id(),
surface: Some(v.clone()),
path: None,
sketch: value.id,
},
)],
meta: vec![Metadata {
source_range: tag.clone().into(),
}],
}
};
// update the sketch tags.
value.sketch.merge_tags(Some(&tag_id).into_iter());
if exec_state.stack().cur_frame_contains(&tag.name) {
exec_state.mut_stack().update(&tag.name, |v, _| {
v.as_mut_tag().unwrap().merge_info(&tag_id);
});
} else {
exec_state
.mut_stack()
.add(
tag.name.clone(),
KclValue::TagIdentifier(Box::new(tag_id)),
SourceRange::default(),
)
.unwrap();
}
}
}
// Find the stale sketch in memory and update it.
if !value.sketch.tags.is_empty() {
let sketches_to_update: Vec<_> = exec_state
.stack()
.find_keys_in_current_env(|v| match v {
KclValue::Sketch { value: sk } => sk.original_id == value.sketch.original_id,
_ => false,
})
.cloned()
.collect();
for k in sketches_to_update {
exec_state.mut_stack().update(&k, |v, _| {
let sketch = v.as_mut_sketch().unwrap();
sketch.merge_tags(value.sketch.tags.values());
});
}
}
}
KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
for v in value {
update_memory_for_tags_of_geometry(v, exec_state)?;
}
}
_ => {}
}
Ok(())
}
fn type_check_params_kw(
fn_name: Option<&str>,
fn_def: &FunctionDefinition<'_>,
args: &mut KwArgs,
exec_state: &mut ExecState,
) -> Result<(), KclError> {
// If it's possible the input arg was meant to be labelled and we probably don't want to use
// it as the input arg, then treat it as labelled.
if let Some((Some(label), _)) = &args.unlabeled {
if (fn_def.input_arg.is_none() || exec_state.pipe_value().is_some())
&& fn_def.named_args.iter().any(|p| p.0 == label)
&& !args.labeled.contains_key(label)
{
let (label, arg) = args.unlabeled.take().unwrap();
args.labeled.insert(label.unwrap(), arg);
}
}
for (label, arg) in &mut args.labeled {
match fn_def.named_args.get(label) {
Some((_, ty)) => {
if let Some(ty) = ty {
arg.value = arg
.value
.coerce(
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range).map_err(|e| KclError::Semantic(e.into()))?,
exec_state,
)
.map_err(|e| {
let mut message = format!(
"{label} requires a value with type `{}`, but found {}",
ty,
arg.value.human_friendly_type(),
);
if let Some(ty) = e.explicit_coercion {
// 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})`");
}
KclError::Semantic(KclErrorDetails::new(
message,
vec![arg.source_range],
))
})?;
}
}
None => {
exec_state.err(CompilationError::err(
arg.source_range,
format!(
"`{label}` is not an argument of {}",
fn_name
.map(|n| format!("`{}`", n))
.unwrap_or_else(|| "this function".to_owned()),
),
));
}
}
}
if !args.errors.is_empty() {
let actuals = args.labeled.keys();
let formals: Vec<_> = fn_def
.named_args
.keys()
.filter_map(|name| {
if actuals.clone().any(|a| a == name) {
return None;
}
Some(format!("`{name}`"))
})
.collect();
let suggestion = if formals.is_empty() {
String::new()
} else {
format!("; suggested labels: {}", formals.join(", "))
};
let mut errors = args.errors.iter().map(|e| {
CompilationError::err(
e.source_range,
format!("This argument needs a label, but it doesn't have one{suggestion}"),
)
});
let first = errors.next().unwrap();
errors.for_each(|e| exec_state.err(e));
return Err(KclError::Semantic(first.into()));
}
if let Some(arg) = &mut args.unlabeled {
if let Some((_, Some(ty))) = &fn_def.input_arg {
arg.1.value = arg
.1
.value
.coerce(
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
.map_err(|e| KclError::Semantic(e.into()))?,
exec_state,
)
.map_err(|_| {
KclError::Semantic(KclErrorDetails::new(
format!(
"The input argument of {} requires a value with type `{}`, but found {}",
fn_name
.map(|n| format!("`{}`", n))
.unwrap_or_else(|| "this function".to_owned()),
ty,
arg.1.value.human_friendly_type()
),
vec![arg.1.source_range],
))
})?;
}
} else if let Some((name, _)) = &fn_def.input_arg {
if let Some(arg) = args.labeled.get(name) {
exec_state.err(CompilationError::err(
arg.source_range,
format!(
"{} expects an unlabeled first parameter (`@{name}`), but it is labelled in the call",
fn_name
.map(|n| format!("The function `{}`", n))
.unwrap_or_else(|| "This function".to_owned()),
),
));
}
}
Ok(())
}
fn assign_args_to_params_kw(
fn_def: &FunctionDefinition<'_>,
args: Args,
exec_state: &mut ExecState,
) -> Result<(), KclError> {
// Add the arguments to the memory. A new call frame should have already
// been created.
let source_ranges = fn_def.as_source_range().into_iter().collect();
for (name, (default, _)) in fn_def.named_args.iter() {
let arg = args.kw_args.labeled.get(name);
match arg {
Some(arg) => {
exec_state.mut_stack().add(
name.clone(),
arg.value.clone(),
arg.source_ranges().pop().unwrap_or(SourceRange::synthetic()),
)?;
}
None => match default {
Some(ref default_val) => {
let value = KclValue::from_default_param(default_val.clone(), exec_state);
exec_state
.mut_stack()
.add(name.clone(), value, default_val.source_range())?;
}
None => {
return Err(KclError::Semantic(KclErrorDetails::new(
format!(
"This function requires a parameter {}, but you haven't passed it one.",
name
),
source_ranges,
)));
}
},
}
}
if let Some((param_name, _)) = &fn_def.input_arg {
let unlabelled = args.unlabeled_kw_arg_unconverted();
let Some(unlabeled) = unlabelled else {
return Err(if args.kw_args.labeled.contains_key(param_name) {
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,
))
} else {
KclError::Semantic(KclErrorDetails::new(
"This function expects an unlabeled first parameter, but you haven't passed it one.".to_owned(),
source_ranges,
))
});
};
exec_state.mut_stack().add(
param_name.clone(),
unlabeled.value.clone(),
unlabeled.source_ranges().pop().unwrap_or(SourceRange::synthetic()),
)?;
}
Ok(())
}
fn coerce_result_type(
result: Result<Option<KclValue>, KclError>,
fn_def: &FunctionDefinition<'_>,
exec_state: &mut ExecState,
) -> Result<Option<KclValue>, KclError> {
if let Ok(Some(val)) = result {
if let Some(ret_ty) = &fn_def.return_type {
let mut ty = RuntimeType::from_parsed(ret_ty.inner.clone(), exec_state, ret_ty.as_source_range())
.map_err(|e| KclError::Semantic(e.into()))?;
// Treat `[T; 1+]` as `T | [T; 1+]` (which can't yet be expressed in our syntax of types).
// This is a very specific hack which exists because some std functions can produce arrays
// but usually only make a singleton and the frontend expects the singleton.
// If we can make the frontend work on arrays (or at least arrays of length 1), then this
// can be removed.
// I believe this is safe, since anywhere which requires an array should coerce the singleton
// to an array and we only do this hack for return values.
if let RuntimeType::Array(inner, ArrayLen::NonEmpty) = &ty {
ty = RuntimeType::Union(vec![(**inner).clone(), ty]);
}
let val = val.coerce(&ty, exec_state).map_err(|_| {
KclError::Semantic(KclErrorDetails::new(
format!(
"This function requires its result to be of type `{}`, but found {}",
ty.human_friendly_type(),
val.human_friendly_type(),
),
ret_ty.as_source_ranges(),
))
})?;
Ok(Some(val))
} else {
Ok(Some(val))
}
} else {
result
}
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use super::*;
use crate::{
execution::{memory::Stack, parse_execute, types::NumericType, ContextType},
parsing::ast::types::{DefaultParamVal, Identifier, Parameter},
};
#[tokio::test(flavor = "multi_thread")]
async fn test_assign_args_to_params() {
// Set up a little framework for this test.
fn mem(number: usize) -> KclValue {
KclValue::Number {
value: number as f64,
ty: NumericType::count(),
meta: Default::default(),
}
}
fn ident(s: &'static str) -> Node<Identifier> {
Node::no_src(Identifier {
name: s.to_owned(),
digest: None,
})
}
fn opt_param(s: &'static str) -> Parameter {
Parameter {
identifier: ident(s),
type_: None,
default_value: Some(DefaultParamVal::none()),
labeled: true,
digest: None,
}
}
fn req_param(s: &'static str) -> Parameter {
Parameter {
identifier: ident(s),
type_: None,
default_value: None,
labeled: true,
digest: None,
}
}
fn additional_program_memory(items: &[(String, KclValue)]) -> Stack {
let mut program_memory = Stack::new_for_tests();
for (name, item) in items {
program_memory
.add(name.clone(), item.clone(), SourceRange::default())
.unwrap();
}
program_memory
}
// Declare the test cases.
for (test_name, params, args, expected) in [
("empty", Vec::new(), Vec::new(), Ok(additional_program_memory(&[]))),
(
"all params required, and all given, should be OK",
vec![req_param("x")],
vec![("x", mem(1))],
Ok(additional_program_memory(&[("x".to_owned(), mem(1))])),
),
(
"all params required, none given, should error",
vec![req_param("x")],
vec![],
Err(KclError::Semantic(KclErrorDetails::new(
"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",
vec![opt_param("x")],
vec![],
Ok(additional_program_memory(&[("x".to_owned(), KclValue::none())])),
),
(
"mixed params, too few given",
vec![req_param("x"), opt_param("y")],
vec![],
Err(KclError::Semantic(KclErrorDetails::new(
"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",
vec![req_param("x"), opt_param("y")],
vec![("x", mem(1))],
Ok(additional_program_memory(&[
("x".to_owned(), mem(1)),
("y".to_owned(), KclValue::none()),
])),
),
(
"mixed params, maximum given, should be OK",
vec![req_param("x"), opt_param("y")],
vec![("x", mem(1)), ("y", mem(2))],
Ok(additional_program_memory(&[
("x".to_owned(), mem(1)),
("y".to_owned(), mem(2)),
])),
),
] {
// Run each test.
let func_expr = Node::no_src(FunctionExpression {
params,
body: Program::empty(),
return_type: None,
digest: None,
});
let func_src = FunctionSource::User {
ast: Box::new(func_expr),
settings: Default::default(),
memory: EnvironmentRef::dummy(),
};
let labeled = args
.iter()
.map(|(name, value)| {
let arg = Arg::new(value.clone(), SourceRange::default());
((*name).to_owned(), arg)
})
.collect::<IndexMap<_, _>>();
let exec_ctxt = ExecutorContext {
engine: Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
)),
fs: Arc::new(crate::fs::FileManager::new()),
stdlib: Arc::new(crate::std::StdLib::new()),
settings: Default::default(),
context_type: ContextType::Mock,
};
let mut exec_state = ExecState::new(&exec_ctxt);
exec_state.mod_local.stack = Stack::new_for_tests();
let args = Args::new_kw(
KwArgs {
unlabeled: None,
labeled,
errors: Vec::new(),
},
SourceRange::default(),
exec_ctxt,
None,
);
let actual = assign_args_to_params_kw(&(&func_src).into(), args, &mut exec_state)
.map(|_| exec_state.mod_local.stack);
assert_eq!(
actual, expected,
"failed test '{test_name}':\ngot {actual:?}\nbut expected\n{expected:?}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn type_check_user_args() {
let program = r#"fn makeMessage(prefix: string, suffix: string) {
return prefix + suffix
}
msg1 = makeMessage(prefix = "world", suffix = " hello")
msg2 = makeMessage(prefix = 1, suffix = 3)"#;
let err = parse_execute(program).await.unwrap_err();
assert_eq!(
err.message(),
"prefix requires a value with type `string`, but found number(default units)"
)
}
}

View File

@ -8,12 +8,12 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[cfg(feature = "artifact-graph")]
use crate::execution::ArtifactId;
use crate::{
engine::{PlaneName, DEFAULT_PLANE_INFO},
errors::{KclError, KclErrorDetails},
execution::{types::NumericType, ExecState, ExecutorContext, Metadata, TagEngineInfo, TagIdentifier, UnitLen},
execution::{
types::NumericType, ArtifactId, ExecState, ExecutorContext, Metadata, TagEngineInfo, TagIdentifier, UnitLen,
},
parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode},
std::{args::TyF64, sketch::PlaneData},
};
@ -256,7 +256,6 @@ pub struct Helix {
/// The id of the helix.
pub value: uuid::Uuid,
/// The artifact ID.
#[cfg(feature = "artifact-graph")]
pub artifact_id: ArtifactId,
/// Number of revolutions.
pub revolutions: f64,
@ -278,7 +277,6 @@ pub struct Plane {
/// The id of the plane.
pub id: uuid::Uuid,
/// The artifact ID.
#[cfg(feature = "artifact-graph")]
pub artifact_id: ArtifactId,
// The code for the plane either a string or custom.
pub value: PlaneType,
@ -471,18 +469,18 @@ impl TryFrom<PlaneData> for PlaneInfo {
PlaneData::NegYZ => PlaneName::NegYz,
PlaneData::Plane(_) => {
// We will never get here since we already checked for PlaneData::Plane.
return Err(KclError::Internal(KclErrorDetails {
message: format!("PlaneData {:?} not found", value),
source_ranges: Default::default(),
}));
return Err(KclError::Internal(KclErrorDetails::new(
format!("PlaneData {:?} not found", value),
Default::default(),
)));
}
};
let info = DEFAULT_PLANE_INFO.get(&name).ok_or_else(|| {
KclError::Internal(KclErrorDetails {
message: format!("Plane {} not found", name),
source_ranges: Default::default(),
})
KclError::Internal(KclErrorDetails::new(
format!("Plane {} not found", name),
Default::default(),
))
})?;
Ok(info.clone())
@ -508,7 +506,6 @@ impl Plane {
let id = exec_state.next_uuid();
Ok(Plane {
id,
#[cfg(feature = "artifact-graph")]
artifact_id: id.into(),
info: PlaneInfo::try_from(value.clone())?,
value: value.into(),
@ -530,7 +527,6 @@ pub struct Face {
/// The id of the face.
pub id: uuid::Uuid,
/// The artifact ID.
#[cfg(feature = "artifact-graph")]
pub artifact_id: ArtifactId,
/// The tag of the face.
pub value: String,
@ -584,7 +580,6 @@ pub struct Sketch {
pub tags: IndexMap<String, TagIdentifier>,
/// The original id of the sketch. This stays the same even if the sketch is
/// is sketched on face etc.
#[cfg(feature = "artifact-graph")]
pub artifact_id: ArtifactId,
#[ts(skip)]
pub original_id: uuid::Uuid,
@ -748,7 +743,6 @@ pub struct Solid {
/// The id of the solid.
pub id: uuid::Uuid,
/// The artifact ID of the solid. Unlike `id`, this doesn't change.
#[cfg(feature = "artifact-graph")]
pub artifact_id: ArtifactId,
/// The extrude surfaces.
pub value: Vec<ExtrudeSurface>,

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