Compare commits
2 Commits
andrewvarg
...
kurt-web-a
Author | SHA1 | Date | |
---|---|---|---|
c42903d2e8 | |||
d98669fb8a |
38
.github/ISSUE_TEMPLATE/release.md
vendored
@ -1,38 +0,0 @@
|
||||
---
|
||||
name: Release
|
||||
about: Create a new release for the Zoo Design Studio
|
||||
title: "Cut release v1.?.?"
|
||||
labels: [release]
|
||||
---
|
||||
|
||||
> Instructions: https://github.com/KittyCAD/modeling-app/blob/main/CONTRIBUTING.md#shipping-releases
|
||||
|
||||
---
|
||||
|
||||
# Manual Checklist
|
||||
|
||||
Release builds URL: ???
|
||||
|
||||
## Windows via ???
|
||||
|
||||
* [ ] Download the release build for this platform
|
||||
* [ ] Confirm the application opens (dismiss the updater)
|
||||
* [ ] Create a project with a basic Text-to-CAD prompt
|
||||
* [ ] Confirm the result is viewable in an engine stream
|
||||
* [ ] Open the application again and confirm the updater can downgrade
|
||||
|
||||
## macOS via ???
|
||||
|
||||
* [ ] Download the release build for this platform
|
||||
* [ ] Confirm the application opens (dismiss the updater)
|
||||
* [ ] Create a project with a basic Text-to-CAD prompt
|
||||
* [ ] Confirm the result is viewable in an engine stream
|
||||
* [ ] Open the application again and confirm the updater can downgrade
|
||||
|
||||
## Linux via ???
|
||||
|
||||
* [ ] Download the release build for this platform
|
||||
* [ ] Confirm the application opens (dismiss the updater)
|
||||
* [ ] Create a project with a basic Text-to-CAD prompt
|
||||
* [ ] Confirm the result is viewable in an engine stream
|
||||
* [ ] Open the application again and confirm the updater can downgrade
|
71
.github/workflows/build-apps.yml
vendored
@ -10,7 +10,7 @@ on:
|
||||
|
||||
env:
|
||||
IS_RELEASE: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }}
|
||||
IS_STAGING: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
IS_NIGHTLY: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@ -91,14 +91,14 @@ jobs:
|
||||
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
|
||||
run: "npm run build:wasm"
|
||||
|
||||
- name: Set staging version, product name, release notes, and icons
|
||||
if: ${{ env.IS_STAGING == 'true' }}
|
||||
- name: Set nightly version, product name, release notes, and icons
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
run: |
|
||||
COMMIT=$(git rev-parse --short HEAD)
|
||||
DATE=$(date +'%-y.%-m.%-d')
|
||||
export VERSION=$DATE-main.$COMMIT
|
||||
npm run files:set-version
|
||||
npm run files:flip-to-staging
|
||||
npm run files:flip-to-nightly
|
||||
|
||||
- name: Set release version
|
||||
if: ${{ env.IS_RELEASE == 'true' }}
|
||||
@ -130,14 +130,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: namespace-profile-macos-6-cores
|
||||
platform: macos
|
||||
- os: namespace-profile-windows-4-cores
|
||||
platform: windows
|
||||
- os: macos-14
|
||||
platform: mac
|
||||
- os: windows-2022
|
||||
platform: win
|
||||
- os: ubuntu-22.04
|
||||
platform: linux
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: build-apps (${{ matrix.platform }})
|
||||
env:
|
||||
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
|
||||
steps:
|
||||
@ -167,14 +166,14 @@ jobs:
|
||||
- run: npm install
|
||||
|
||||
- name: Prepare certificate and variables (Windows only)
|
||||
if: ${{ (env.IS_RELEASE == 'true' || env.IS_STAGING == 'true') && matrix.platform == 'windows' }}
|
||||
if: ${{ (env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true') && matrix.os == 'windows-2022' }}
|
||||
run: |
|
||||
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /c/Certificate_pkcs12.p12
|
||||
cat /c/Certificate_pkcs12.p12
|
||||
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
|
||||
cat /d/Certificate_pkcs12.p12
|
||||
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
|
||||
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
|
||||
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
|
||||
echo "SM_CLIENT_CERT_FILE=C:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
|
||||
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
|
||||
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
|
||||
echo "C:\Program Files (x86)\Windows Kits\10\App Certification Kit" >> $GITHUB_PATH
|
||||
echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
|
||||
@ -182,7 +181,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Setup certicate with SSM KSP (Windows only)
|
||||
if: ${{ (env.IS_RELEASE == 'true' || env.IS_STAGING == 'true') && matrix.platform == 'windows' }}
|
||||
if: ${{ (env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true') && matrix.os == 'windows-2022' }}
|
||||
run: |
|
||||
curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:%SM_API_KEY%" -o smtools-windows-x64.msi
|
||||
msiexec /i smtools-windows-x64.msi /quiet /qn
|
||||
@ -192,7 +191,7 @@ jobs:
|
||||
smksp_cert_sync.exe
|
||||
smctl windows certsync
|
||||
# This last line `smctl windows certsync` was added after windows codesign failures started happening
|
||||
# with staging-v25.4.10. It looks like `smksp_cert_sync.exe` used to do the sync to the local cert store,
|
||||
# with nightly-v25.4.10. It looks like `smksp_cert_sync.exe` used to do the sync to the local cert store,
|
||||
# but stopped doing it overnight. This extra call that I randomly got from this azure-related doc page
|
||||
# https://docs.digicert.com/en/digicert-keylocker/code-signing/sign-with-third-party-signing-tools/windows-applications/sign-azure-apps-with-signtool-using-ksp-library.html#sync-certificates--windows-only--618365
|
||||
# seems to be doing that extra sync that we need for scripts/sign-win.js to work.
|
||||
@ -200,13 +199,13 @@ jobs:
|
||||
shell: cmd
|
||||
|
||||
- name: Build the app (debug)
|
||||
if: ${{ env.IS_RELEASE == 'false' && env.IS_STAGING == 'false' }}
|
||||
if: ${{ env.IS_RELEASE == 'false' && env.IS_NIGHTLY == 'false' }}
|
||||
# electron-builder doesn't have a concept of release vs debug,
|
||||
# this is just not doing any codesign or release yml generation, and points to dev infra
|
||||
run: npm run tronb:package:dev
|
||||
|
||||
- name: Build the app (release)
|
||||
if: ${{ env.IS_RELEASE == 'true' || env.IS_STAGING == 'true' }}
|
||||
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
@ -216,7 +215,7 @@ jobs:
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||
run: npm run tronb:package:${{ env.IS_STAGING == 'true' && 'dev' || 'prod' }}
|
||||
run: npm run tronb:package:prod
|
||||
|
||||
- name: List artifacts in out/
|
||||
run: ls -R out
|
||||
@ -240,20 +239,20 @@ jobs:
|
||||
out/*-x86_64-linux.*
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ env.IS_RELEASE == 'true' || env.IS_STAGING == 'true' }}
|
||||
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
|
||||
with:
|
||||
name: out-yml-${{ matrix.platform }}
|
||||
path: |
|
||||
out/latest*.yml
|
||||
|
||||
# TODO: add the 'Build for Mac TestFlight' stage back
|
||||
# TODO: add the 'Build for Mac TestFlight (nightly)' stage back
|
||||
|
||||
|
||||
upload-apps-release:
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
# Equivalent to IS_RELEASE || IS_STAGING (but we can't access those env vars here)
|
||||
# Equivalent to IS_RELEASE || IS_NIGHTLY (but we can't access those env vars here)
|
||||
if: ${{ (github.ref_type == 'tag' && startsWith(github.ref_name, 'v')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
env:
|
||||
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
|
||||
@ -264,32 +263,32 @@ jobs:
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: out-arm64-windows
|
||||
name: out-arm64-win
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: out-x64-windows
|
||||
name: out-x64-win
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: out-yml-windows
|
||||
name: out-yml-win
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: out-arm64-macos
|
||||
name: out-arm64-mac
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: out-x64-macos
|
||||
name: out-x64-mac
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: out-yml-macos
|
||||
name: out-yml-mac
|
||||
path: out
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
@ -311,8 +310,8 @@ jobs:
|
||||
env:
|
||||
NOTES: ${{ needs.prepare-files.outputs.notes }}
|
||||
PUB_DATE: ${{ github.event.repository.updated_at }}
|
||||
WEBSITE_DIR: ${{ env.IS_STAGING == 'true' && 'dl.zoo.dev/releases/modeling-app/staging' || 'dl.zoo.dev/releases/modeling-app' }}
|
||||
URL_CODED_NAME: ${{ env.IS_STAGING == 'true' && 'Zoo%20Design%20Studio%20%28Staging%29' || 'Zoo%20Design%20Studio' }}
|
||||
WEBSITE_DIR: ${{ env.IS_NIGHTLY == 'true' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
|
||||
URL_CODED_NAME: ${{ env.IS_NIGHTLY == 'true' && 'Zoo%20Design%20Studio%20%28Nightly%29' || 'Zoo%20Design%20Studio' }}
|
||||
run: |
|
||||
RELEASE_DIR=https://${WEBSITE_DIR}
|
||||
jq --null-input \
|
||||
@ -361,26 +360,26 @@ jobs:
|
||||
run: "ls -R out"
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
if: ${{ env.IS_STAGING == 'true' }}
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
uses: 'google-github-actions/auth@v2.1.8'
|
||||
with:
|
||||
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||
|
||||
- name: Set up Google Cloud SDK
|
||||
if: ${{ env.IS_STAGING == 'true' }}
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
uses: google-github-actions/setup-gcloud@v2.1.4
|
||||
with:
|
||||
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
|
||||
|
||||
- name: Upload staging files to public bucket
|
||||
if: ${{ env.IS_STAGING == 'true' }}
|
||||
- name: Upload nightly files to public bucket
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.2
|
||||
with:
|
||||
path: out
|
||||
glob: '*'
|
||||
parent: false
|
||||
destination: 'dl.kittycad.io/releases/modeling-app/staging'
|
||||
destination: 'dl.kittycad.io/releases/modeling-app/nightly'
|
||||
|
||||
- name: Invalidate bucket cache on latest*.yml and last_download.json files
|
||||
if: ${{ env.IS_STAGING == 'true' }}
|
||||
run: npm run files:invalidate-bucket:staging
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
run: npm run files:invalidate-bucket:nightly
|
||||
|
20
.github/workflows/e2e-tests.yml
vendored
@ -177,16 +177,16 @@ jobs:
|
||||
TARGET: web
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: playwright-report-snapshot-${{ github.sha }}
|
||||
name: playwright-report-ubuntu-snapshot-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
- name: Check diff
|
||||
if: ${{ always() && github.ref != 'refs/heads/main' }}
|
||||
if: ${{ github.ref != 'refs/heads/main' }}
|
||||
shell: bash
|
||||
id: git-check
|
||||
run: |
|
||||
@ -197,7 +197,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Commit changes
|
||||
if: ${{ always() && steps.git-check.outputs.modified == 'true' }}
|
||||
if: ${{ steps.git-check.outputs.modified == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots
|
||||
@ -220,7 +220,7 @@ jobs:
|
||||
include:
|
||||
- os: "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64"
|
||||
- os: namespace-profile-macos-8-cores
|
||||
- os: namespace-profile-windows-8-cores
|
||||
- os: windows-latest-8-cores
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: e2e:web (${{ contains(matrix.os, 'ubuntu') && 'ubuntu' || (contains(matrix.os, 'windows') && 'windows' || 'macos') }})
|
||||
env:
|
||||
@ -293,7 +293,6 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: playwright-report-web-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
@ -305,6 +304,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# TODO: enable namespace-profile-windows-latest once available
|
||||
include:
|
||||
- os: "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64"
|
||||
shardIndex: 1
|
||||
@ -336,10 +336,10 @@ jobs:
|
||||
- os: namespace-profile-macos-8-cores
|
||||
shardIndex: 2
|
||||
shardTotal: 2
|
||||
- os: namespace-profile-windows-8-cores
|
||||
- os: windows-latest-8-cores
|
||||
shardIndex: 1
|
||||
shardTotal: 2
|
||||
- os: namespace-profile-windows-8-cores
|
||||
- os: windows-latest-8-cores
|
||||
shardIndex: 2
|
||||
shardTotal: 2
|
||||
runs-on: ${{ matrix.os }}
|
||||
@ -418,7 +418,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results-desktop-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: test-results-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
@ -427,7 +427,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-desktop-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
name: playwright-report-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
|
12
.github/workflows/publish-apps-release.yml
vendored
@ -31,42 +31,42 @@ jobs:
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: out-arm64-windows
|
||||
name: out-arm64-win
|
||||
path: out
|
||||
run-id: ${{ steps.tag_workflow_id.outputs.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: out-x64-windows
|
||||
name: out-x64-win
|
||||
path: out
|
||||
run-id: ${{ steps.tag_workflow_id.outputs.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: out-yml-windows
|
||||
name: out-yml-win
|
||||
path: out
|
||||
run-id: ${{ steps.tag_workflow_id.outputs.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: out-arm64-macos
|
||||
name: out-arm64-mac
|
||||
path: out
|
||||
run-id: ${{ steps.tag_workflow_id.outputs.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: out-x64-macos
|
||||
name: out-x64-mac
|
||||
path: out
|
||||
run-id: ${{ steps.tag_workflow_id.outputs.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: out-yml-macos
|
||||
name: out-yml-mac
|
||||
path: out
|
||||
run-id: ${{ steps.tag_workflow_id.outputs.id }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
@ -251,8 +251,7 @@ Before you submit a contribution PR to this repo, please ensure that:
|
||||
|
||||
#### 1. Create a 'Cut release $VERSION' issue
|
||||
|
||||
Use the **Release** issue template.
|
||||
This will be used to facilitate changelog discussions and release testing.
|
||||
It will be used to document changelog discussions and release testing.
|
||||
|
||||
https://github.com/KittyCAD/modeling-app/issues/new
|
||||
|
||||
@ -271,9 +270,27 @@ The workflow should be listed right away [in this list](https://github.com/Kitty
|
||||
|
||||
#### 3. Manually test artifacts
|
||||
|
||||
##### Release builds
|
||||
|
||||
The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in step 2).
|
||||
|
||||
Assign someone to each section of the manual checklist generated by the issue template.
|
||||
Manually test against [this list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue.
|
||||
|
||||
A prompt should show up asking for a downgrade to the last release version. Running through that at the end of testing
|
||||
and making sure the current release candidate has the ability to be updated to what electron-updater points to is critical,
|
||||
but what is actually being downloaded and installed isn't.
|
||||
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing.
|
||||
|
||||
```
|
||||
# Windows (PowerShell)
|
||||
& 'C:\Program Files\Zoo Design Studio\Zoo Design Studio.exe'
|
||||
|
||||
# macOS
|
||||
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
|
||||
|
||||
# Linux
|
||||
./Zoo Design Studio-{version}-{arch}-linux.AppImage
|
||||
```
|
||||
|
||||
#### 4. Bump the KCL version
|
||||
|
||||
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 259 KiB |
@ -46,7 +46,3 @@ KCL has no support for area, volume, or other higher dimension units. When inter
|
||||
## Explicit conversions
|
||||
|
||||
You might sometimes need to convert from one unit to another for some calculation. You can do this implicitly when calling a function (see above), but if you can't or don't want to, then you can use the explicit conversion functions in the [`std::units`](/docs/kcl-std/modules/std-units) module.
|
||||
|
||||
KCL cannot know about changes to units caused by arithmetic. For example, you may intend for `10in * 25.4` to be the value `254mm` (i.e., `10in` in mm), however, the result of that computation in KCL is `254in`. It is always better to rely on automatic conversion or to use the explicit conversion functions, where possible.
|
||||
|
||||
Converting between degrees and radians using π ([`PI`](/docs/kcl-std/consts/std-math-PI) in KCL) is especially prone to this error and so the `PI` constant always requires specifying units of any computation it is used with. E.g., `radius = (circumference / (2 * PI)): number(mm)`.
|
||||
|
@ -4,6 +4,8 @@ excerpt: "Project specific settings for the app. These live in `project.toml` in
|
||||
layout: manual
|
||||
---
|
||||
|
||||
# Project Settings
|
||||
|
||||
Project specific settings for the app. These live in `project.toml` in the base of the project directory. Updating the settings for the project in the app will update this file automatically. Do not edit this file manually, as it may be overwritten by the app. Manual edits can cause corruption of the settings file.
|
||||
|
||||
## Project Configuration Structure
|
||||
@ -182,4 +184,4 @@ color = 240.0
|
||||
# Use inches as the default measurement unit
|
||||
base_unit = "in"
|
||||
|
||||
```
|
||||
```
|
@ -4,6 +4,8 @@ excerpt: "User specific settings for the app. These live in `user.toml` in the a
|
||||
layout: manual
|
||||
---
|
||||
|
||||
# User Settings
|
||||
|
||||
User specific settings for the app. These live in `user.toml` in the app's configuration directory. Updating the settings in the app will update this file automatically. Do not edit this file manually, as it may be overwritten by the app. Manual edits can cause corruption of the settings file.
|
||||
|
||||
## User Configuration Structure
|
||||
@ -232,4 +234,4 @@ base_unit = "mm"
|
||||
# Disable text wrapping in the editor
|
||||
text_wrapping = false
|
||||
|
||||
```
|
||||
```
|
70
docs/kcl-std/bezierCurve.md
Normal file
33
docs/kcl-std/consts.md
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
title: "KCL Constants"
|
||||
excerpt: "Documentation for the KCL constants."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
|
||||
### `std`
|
||||
|
||||
- [`END`](/docs/kcl/consts/std-END)
|
||||
- [`START`](/docs/kcl/consts/std-START)
|
||||
- [`X`](/docs/kcl/consts/std-X)
|
||||
- [`XY`](/docs/kcl/consts/std-XY)
|
||||
- [`XZ`](/docs/kcl/consts/std-XZ)
|
||||
- [`Y`](/docs/kcl/consts/std-Y)
|
||||
- [`YZ`](/docs/kcl/consts/std-YZ)
|
||||
- [`Z`](/docs/kcl/consts/std-Z)
|
||||
|
||||
### `std::math`
|
||||
|
||||
- [`E`](/docs/kcl/consts/std-math-E)
|
||||
- [`PI`](/docs/kcl/consts/std-math-PI)
|
||||
- [`TAU`](/docs/kcl/consts/std-math-TAU)
|
||||
|
||||
### `std::turns`
|
||||
|
||||
- [`HALF_TURN`](/docs/kcl/consts/std-turns-HALF_TURN)
|
||||
- [`QUARTER_TURN`](/docs/kcl/consts/std-turns-QUARTER_TURN)
|
||||
- [`THREE_QUARTER_TURN`](/docs/kcl/consts/std-turns-THREE_QUARTER_TURN)
|
||||
- [`ZERO`](/docs/kcl/consts/std-turns-ZERO)
|
||||
|
@ -18,7 +18,7 @@ E: number = 2.71828182845904523536028747135266250_
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 30deg, length = 2 * E ^ 2)
|
||||
|> angledLine(angle = 30, length = 2 * E ^ 2)
|
||||
|> yLine(endAbsolute = 0)
|
||||
|> close()
|
||||
|
||||
|
@ -16,8 +16,8 @@ However, `PI` is nearly always used for converting between different units - usu
|
||||
from radians. Therefore, `PI` is treated a bit specially by KCL and always has unknown units. This
|
||||
means that if you use `PI`, you will need to give KCL some extra information about the units of numbers.
|
||||
Usually you should use type ascription on the result of calculations, e.g., `(2 * PI): number(rad)`.
|
||||
It is better to use `units::toRadians` or `units::toDegrees` to convert between angles with
|
||||
different units where possible.
|
||||
You might prefer to use `units::toRadians` or `units::toDegrees` to convert between angles with
|
||||
different units.
|
||||
|
||||
### Examples
|
||||
|
||||
|
@ -18,7 +18,7 @@ TAU: number = 6.28318530717958647692528676655900577_
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 50deg, length = 10 * TAU)
|
||||
|> angledLine(angle = 50, length = 10 * TAU)
|
||||
|> yLine(endAbsolute = 0)
|
||||
|> close()
|
||||
|
||||
|
@ -8,7 +8,7 @@ layout: manual
|
||||
No turn, zero degrees/radians.
|
||||
|
||||
```kcl
|
||||
turns::ZERO
|
||||
turns::ZERO: number = 0
|
||||
```
|
||||
|
||||
|
||||
|
@ -27,7 +27,7 @@ abs(@input: number): number
|
||||
### Examples
|
||||
|
||||
```kcl
|
||||
myAngle = -120deg
|
||||
myAngle = -120
|
||||
|
||||
sketch001 = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|
@ -8,7 +8,7 @@ layout: manual
|
||||
Compute the cosine of a number.
|
||||
|
||||
```kcl
|
||||
cos(@num: number(Angle)): number
|
||||
cos(@num: number(Angle)): number(_)
|
||||
```
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ cos(@num: number(Angle)): number
|
||||
|
||||
### Returns
|
||||
|
||||
[`number`](/docs/kcl-std/types/std-types-number) - A number.
|
||||
[`number(_)`](/docs/kcl-std/types/std-types-number) - A number.
|
||||
|
||||
|
||||
### Examples
|
||||
@ -29,7 +29,7 @@ cos(@num: number(Angle)): number
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 30deg, length = 3 / cos(30deg))
|
||||
|> angledLine(angle = 30, length = 3 / cos(30deg))
|
||||
|> yLine(endAbsolute = 0)
|
||||
|> close()
|
||||
|
||||
|
@ -29,7 +29,7 @@ max(@input: [number; 1+]): number
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 70deg, length = max([15, 31, 4, 13, 22]))
|
||||
|> angledLine(angle = 70, length = max([15, 31, 4, 13, 22]))
|
||||
|> line(end = [20, 0])
|
||||
|> close()
|
||||
|
||||
|
@ -29,7 +29,7 @@ min(@input: [number; 1+]): number
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 70deg, length = min([15, 31, 4, 13, 22]))
|
||||
|> angledLine(angle = 70, length = min([15, 31, 4, 13, 22]))
|
||||
|> line(end = [20, 0])
|
||||
|> close()
|
||||
|
||||
|
@ -33,7 +33,7 @@ polar(
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> line(end = polar(angle = 30deg, length = 5), tag = $thing)
|
||||
|> line(end = polar(angle = 30, length = 5), tag = $thing)
|
||||
|> line(end = [0, 5])
|
||||
|> line(end = [segEndX(thing), 0])
|
||||
|> line(end = [-20, 10])
|
||||
|
@ -33,7 +33,7 @@ pow(
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 50deg, length = pow(5, exp = 2))
|
||||
|> angledLine(angle = 50, length = pow(5, exp = 2))
|
||||
|> yLine(endAbsolute = 0)
|
||||
|> close()
|
||||
|
||||
|
@ -8,7 +8,7 @@ layout: manual
|
||||
Compute the sine of a number.
|
||||
|
||||
```kcl
|
||||
sin(@num: number(Angle)): number
|
||||
sin(@num: number(Angle)): number(_)
|
||||
```
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ sin(@num: number(Angle)): number
|
||||
|
||||
### Returns
|
||||
|
||||
[`number`](/docs/kcl-std/types/std-types-number) - A number.
|
||||
[`number(_)`](/docs/kcl-std/types/std-types-number) - A number.
|
||||
|
||||
|
||||
### Examples
|
||||
@ -29,7 +29,7 @@ sin(@num: number(Angle)): number
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 50deg, length = 15 / sin(135deg))
|
||||
|> angledLine(angle = 50, length = 15 / sin(135deg))
|
||||
|> yLine(endAbsolute = 0)
|
||||
|> close()
|
||||
|
||||
|
@ -29,7 +29,7 @@ sqrt(@input: number): number
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 50deg, length = sqrt(2500))
|
||||
|> angledLine(angle = 50, length = sqrt(2500))
|
||||
|> yLine(endAbsolute = 0)
|
||||
|> close()
|
||||
|
||||
|
@ -8,7 +8,7 @@ layout: manual
|
||||
Compute the tangent of a number.
|
||||
|
||||
```kcl
|
||||
tan(@num: number(Angle)): number
|
||||
tan(@num: number(Angle)): number(_)
|
||||
```
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ tan(@num: number(Angle)): number
|
||||
|
||||
### Returns
|
||||
|
||||
[`number`](/docs/kcl-std/types/std-types-number) - A number.
|
||||
[`number(_)`](/docs/kcl-std/types/std-types-number) - A number.
|
||||
|
||||
|
||||
### Examples
|
||||
@ -29,7 +29,7 @@ tan(@num: number(Angle)): number
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 50deg, length = 50 * tan((1 / 2): number(rad)))
|
||||
|> angledLine(angle = 50, length = 50 * tan((1 / 2): number(rad)))
|
||||
|> yLine(endAbsolute = 0)
|
||||
|> close()
|
||||
|
||||
|
@ -46,7 +46,7 @@ angledLine(
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> yLine(endAbsolute = 15)
|
||||
|> angledLine(angle = 30deg, length = 15)
|
||||
|> angledLine(angle = 30, length = 15)
|
||||
|> line(end = [8, -10])
|
||||
|> yLine(endAbsolute = 0)
|
||||
|> close()
|
||||
|
@ -42,7 +42,7 @@ exampleSketch = startSketchOn(XZ)
|
||||
|> line(endAbsolute = [5, 10])
|
||||
|> line(endAbsolute = [-10, 10], tag = $lineToIntersect)
|
||||
|> line(endAbsolute = [0, 20])
|
||||
|> angledLineThatIntersects(angle = 80deg, intersectTag = lineToIntersect, offset = 10)
|
||||
|> angledLineThatIntersects(angle = 80, intersectTag = lineToIntersect, offset = 10)
|
||||
|> close()
|
||||
|
||||
example = extrude(exampleSketch, length = 10)
|
||||
|
@ -53,7 +53,7 @@ for to construct your shape, you're likely looking for tangentialArc.
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> line(end = [10, 0])
|
||||
|> arc(angleStart = 0, angleEnd = 280deg, radius = 16)
|
||||
|> arc(angleStart = 0, angleEnd = 280, radius = 16)
|
||||
|> close()
|
||||
example = extrude(exampleSketch, length = 10)
|
||||
|
||||
|
@ -43,7 +43,7 @@ extruded in the same direction.
|
||||
example = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> line(end = [10, 0])
|
||||
|> arc(angleStart = 120deg, angleEnd = 0, radius = 5)
|
||||
|> arc(angleStart = 120, angleEnd = 0, radius = 5)
|
||||
|> line(end = [5, 0])
|
||||
|> line(end = [0, 10])
|
||||
|> bezierCurve(control1 = [-10, 0], control2 = [2, 10], end = [-5, 10])
|
||||
@ -58,7 +58,7 @@ example = startSketchOn(XZ)
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [-10, 0])
|
||||
|> arc(angleStart = 120deg, angleEnd = -60deg, radius = 5)
|
||||
|> arc(angleStart = 120, angleEnd = -60, radius = 5)
|
||||
|> line(end = [10, 0])
|
||||
|> line(end = [5, 0])
|
||||
|> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10])
|
||||
@ -75,7 +75,7 @@ example = extrude(exampleSketch, length = 10)
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [-10, 0])
|
||||
|> arc(angleStart = 120deg, angleEnd = -60deg, radius = 5)
|
||||
|> arc(angleStart = 120, angleEnd = -60, radius = 5)
|
||||
|> line(end = [10, 0])
|
||||
|> line(end = [5, 0])
|
||||
|> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10])
|
||||
@ -92,7 +92,7 @@ example = extrude(exampleSketch, length = 20, symmetric = true)
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [-10, 0])
|
||||
|> arc(angleStart = 120deg, angleEnd = -60deg, radius = 5)
|
||||
|> arc(angleStart = 120, angleEnd = -60, radius = 5)
|
||||
|> line(end = [10, 0])
|
||||
|> line(end = [5, 0])
|
||||
|> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10])
|
||||
|
@ -30,10 +30,10 @@ getNextAdjacentEdge(@edge: tag): Edge
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> line(end = [10, 0])
|
||||
|> angledLine(angle = 60deg, length = 10)
|
||||
|> angledLine(angle = 120deg, length = 10)
|
||||
|> angledLine(angle = 60, length = 10)
|
||||
|> angledLine(angle = 120, length = 10)
|
||||
|> line(end = [-10, 0])
|
||||
|> angledLine(angle = 240deg, length = 10, tag = $referenceEdge)
|
||||
|> angledLine(angle = 240, length = 10, tag = $referenceEdge)
|
||||
|> close()
|
||||
|
||||
example = extrude(exampleSketch, length = 5)
|
||||
|
@ -30,10 +30,10 @@ getOppositeEdge(@edge: tag): Edge
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> line(end = [10, 0])
|
||||
|> angledLine(angle = 60deg, length = 10)
|
||||
|> angledLine(angle = 120deg, length = 10)
|
||||
|> angledLine(angle = 60, length = 10)
|
||||
|> angledLine(angle = 120, length = 10)
|
||||
|> line(end = [-10, 0])
|
||||
|> angledLine(angle = 240deg, length = 10, tag = $referenceEdge)
|
||||
|> angledLine(angle = 240, length = 10, tag = $referenceEdge)
|
||||
|> close()
|
||||
|
||||
example = extrude(exampleSketch, length = 5)
|
||||
|
@ -30,10 +30,10 @@ getPreviousAdjacentEdge(@edge: tag): Edge
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> line(end = [10, 0])
|
||||
|> angledLine(angle = 60deg, length = 10)
|
||||
|> angledLine(angle = 120deg, length = 10)
|
||||
|> angledLine(angle = 60, length = 10)
|
||||
|> angledLine(angle = 120, length = 10)
|
||||
|> line(end = [-10, 0])
|
||||
|> angledLine(angle = 240deg, length = 10, tag = $referenceEdge)
|
||||
|> angledLine(angle = 240, length = 10, tag = $referenceEdge)
|
||||
|> close()
|
||||
|
||||
example = extrude(exampleSketch, length = 5)
|
||||
|
@ -43,11 +43,11 @@ a = 10
|
||||
b = 14
|
||||
startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> involuteCircular(startRadius = a, endRadius = b, angle = 60deg)
|
||||
|> involuteCircular(startRadius = a, endRadius = b, angle = 60)
|
||||
|> involuteCircular(
|
||||
startRadius = a,
|
||||
endRadius = b,
|
||||
angle = 60deg,
|
||||
angle = 60,
|
||||
reverse = true,
|
||||
)
|
||||
|
||||
|
@ -30,7 +30,7 @@ profileStart(@profile: Sketch): Point2d
|
||||
sketch001 = startSketchOn(XY)
|
||||
|> startProfile(at = [5, 2])
|
||||
|> angledLine(angle = 120, length = 50, tag = $seg01)
|
||||
|> angledLine(angle = segAng(seg01) + 120deg, length = 50)
|
||||
|> angledLine(angle = segAng(seg01) + 120, length = 50)
|
||||
|> line(end = profileStart(%))
|
||||
|> close()
|
||||
|> extrude(length = 20)
|
||||
|
@ -30,8 +30,8 @@ profileStartX(@profile: Sketch): number(Length)
|
||||
sketch001 = startSketchOn(XY)
|
||||
|> startProfile(at = [5, 2])
|
||||
|> angledLine(angle = -26.6, length = 50)
|
||||
|> angledLine(angle = 90deg, length = 50)
|
||||
|> angledLine(angle = 30deg, endAbsoluteX = profileStartX(%))
|
||||
|> angledLine(angle = 90, length = 50)
|
||||
|> angledLine(angle = 30, endAbsoluteX = profileStartX(%))
|
||||
|
||||
```
|
||||
|
||||
|
@ -29,8 +29,8 @@ profileStartY(@profile: Sketch): number(Length)
|
||||
```kcl
|
||||
sketch001 = startSketchOn(XY)
|
||||
|> startProfile(at = [5, 2])
|
||||
|> angledLine(angle = -60deg, length = 14)
|
||||
|> angledLine(angle = 30deg, endAbsoluteY = profileStartY(%))
|
||||
|> angledLine(angle = -60, length = 14)
|
||||
|> angledLine(angle = 30, endAbsoluteY = profileStartY(%))
|
||||
|
||||
```
|
||||
|
||||
|
@ -62,7 +62,7 @@ part001 = startSketchOn(XY)
|
||||
|> line(end = [0, -5.5])
|
||||
|> line(end = [-2, 0])
|
||||
|> close()
|
||||
|> revolve(axis = Y) // default angle is 360deg
|
||||
|> revolve(axis = Y) // default angle is 360
|
||||
|
||||
```
|
||||
|
||||
@ -72,7 +72,7 @@ part001 = startSketchOn(XY)
|
||||
// A donut shape.
|
||||
sketch001 = startSketchOn(XY)
|
||||
|> circle(center = [15, 0], radius = 5)
|
||||
|> revolve(angle = 360deg, axis = Y)
|
||||
|> revolve(angle = 360, axis = Y)
|
||||
|
||||
```
|
||||
|
||||
@ -89,7 +89,7 @@ part001 = startSketchOn(XY)
|
||||
|> line(end = [0, -5.5])
|
||||
|> line(end = [-2, 0])
|
||||
|> close()
|
||||
|> revolve(axis = Y, angle = 180deg)
|
||||
|> revolve(axis = Y, angle = 180)
|
||||
|
||||
```
|
||||
|
||||
@ -106,7 +106,7 @@ part001 = startSketchOn(XY)
|
||||
|> line(end = [0, -5.5])
|
||||
|> line(end = [-2, 0])
|
||||
|> close()
|
||||
|> revolve(axis = Y, angle = 180deg)
|
||||
|> revolve(axis = Y, angle = 180)
|
||||
|
||||
part002 = startSketchOn(part001, face = END)
|
||||
|> startProfile(at = [4.5, -5])
|
||||
@ -131,7 +131,7 @@ box = startSketchOn(XY)
|
||||
|
||||
sketch001 = startSketchOn(box, face = END)
|
||||
|> circle(center = [10, 10], radius = 4)
|
||||
|> revolve(angle = -90deg, axis = Y)
|
||||
|> revolve(angle = -90, axis = Y)
|
||||
|
||||
```
|
||||
|
||||
@ -148,7 +148,7 @@ box = startSketchOn(XY)
|
||||
|
||||
sketch001 = startSketchOn(box, face = END)
|
||||
|> circle(center = [10, 10], radius = 4)
|
||||
|> revolve(angle = 90deg, axis = getOppositeEdge(revolveAxis))
|
||||
|> revolve(angle = 90, axis = getOppositeEdge(revolveAxis))
|
||||
|
||||
```
|
||||
|
||||
@ -165,7 +165,7 @@ box = startSketchOn(XY)
|
||||
|
||||
sketch001 = startSketchOn(box, face = END)
|
||||
|> circle(center = [10, 10], radius = 4)
|
||||
|> revolve(angle = 90deg, axis = getOppositeEdge(revolveAxis), tolerance = 0.0001)
|
||||
|> revolve(angle = 90, axis = getOppositeEdge(revolveAxis), tolerance = 0.0001)
|
||||
|
||||
```
|
||||
|
||||
@ -229,7 +229,7 @@ profile001 = startSketchOn(XY)
|
||||
|
||||
sketch001 = startSketchOn(XY)
|
||||
|> circle(center = [-10, 10], radius = 4)
|
||||
|> revolve(angle = 90deg, axis = revolveAxis)
|
||||
|> revolve(angle = 90, axis = revolveAxis)
|
||||
|
||||
```
|
||||
|
||||
@ -246,7 +246,7 @@ profile001 = startSketchOn(XY)
|
||||
|
||||
sketch001 = startSketchOn(XY)
|
||||
|> circle(center = [-10, 10], radius = 4)
|
||||
|> revolve(angle = 90deg, axis = revolveAxis)
|
||||
|> revolve(angle = 90, axis = revolveAxis)
|
||||
|
||||
```
|
||||
|
||||
@ -263,7 +263,7 @@ profile001 = startSketchOn(XY)
|
||||
|
||||
sketch001 = startSketchOn(XY)
|
||||
|> circle(center = [-10, 10], radius = 4)
|
||||
|> revolve(angle = 90deg, axis = revolveAxis, symmetric = true)
|
||||
|> revolve(angle = 90, axis = revolveAxis, symmetric = true)
|
||||
|
||||
```
|
||||
|
||||
@ -280,7 +280,7 @@ profile001 = startSketchOn(XY)
|
||||
|
||||
sketch001 = startSketchOn(XY)
|
||||
|> circle(center = [-10, 10], radius = 4)
|
||||
|> revolve(angle = 90deg, axis = revolveAxis, bidirectionalAngle = 50)
|
||||
|> revolve(angle = 90, axis = revolveAxis, bidirectionalAngle = 50)
|
||||
|
||||
```
|
||||
|
||||
|
@ -30,8 +30,8 @@ segLen(@tag: tag): number(Length)
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 60, length = 10, tag = $thing)
|
||||
|> tangentialArc(angle = -120deg, radius = 5)
|
||||
|> angledLine(angle = -60deg, length = segLen(thing))
|
||||
|> tangentialArc(angle = -120, radius = 5)
|
||||
|> angledLine(angle = -60, length = segLen(thing))
|
||||
|> close()
|
||||
|
||||
example = extrude(exampleSketch, length = 5)
|
||||
|
@ -156,7 +156,7 @@ exampleSketch = startSketchOn(XY)
|
||||
|> line(end = [-2, 0])
|
||||
|> close()
|
||||
|
||||
example = revolve(exampleSketch, axis = Y, angle = 180deg)
|
||||
example = revolve(exampleSketch, axis = Y, angle = 180)
|
||||
|
||||
exampleSketch002 = startSketchOn(example, face = END)
|
||||
|> startProfile(at = [4.5, -5])
|
||||
@ -189,7 +189,7 @@ exampleSketch = startSketchOn(XY)
|
||||
example = revolve(
|
||||
exampleSketch,
|
||||
axis = Y,
|
||||
angle = 180deg,
|
||||
angle = 180,
|
||||
tagEnd = $end01,
|
||||
)
|
||||
|
||||
|
@ -54,9 +54,9 @@ swept along the same path.
|
||||
sweepPath = startSketchOn(XZ)
|
||||
|> startProfile(at = [0.05, 0.05])
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc(angle = 90deg, radius = 5)
|
||||
|> tangentialArc(angle = 90, radius = 5)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc(angle = -90deg, radius = 5)
|
||||
|> tangentialArc(angle = -90, radius = 5)
|
||||
|> line(end = [0, 7])
|
||||
|
||||
// Create a hole for the pipe.
|
||||
@ -101,7 +101,7 @@ springSketch = startSketchOn(XZ)
|
||||
sketch001 = startSketchOn(XY)
|
||||
rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
|
||||
|> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA001) - 90deg, length = 50.61)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 50.61)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
@ -111,7 +111,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
|
||||
sketch002 = startSketchOn(YZ)
|
||||
sweepPath = startProfile(sketch002, at = [0, 0])
|
||||
|> yLine(length = 231.81)
|
||||
|> tangentialArc(radius = 80, angle = -90deg)
|
||||
|> tangentialArc(radius = 80, angle = -90)
|
||||
|> xLine(length = 384.93)
|
||||
|
||||
sweep([rectangleSketch, circleSketch], path = sweepPath)
|
||||
@ -130,7 +130,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
|
||||
sketch002 = startSketchOn(YZ)
|
||||
sweepPath = startProfile(sketch002, at = [0, 0])
|
||||
|> yLine(length = 231.81)
|
||||
|> tangentialArc(radius = 80, angle = -90deg)
|
||||
|> tangentialArc(radius = 80, angle = -90)
|
||||
|> xLine(length = 384.93)
|
||||
|
||||
sweep(circleSketch, path = sweepPath, sectional = true)
|
||||
|
@ -47,7 +47,7 @@ for 'angle' degrees along the imaginary circle.
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 45deg, length = 10)
|
||||
|> angledLine(angle = 45, length = 10)
|
||||
|> tangentialArc(end = [0, -10])
|
||||
|> line(end = [-10, 0])
|
||||
|> close()
|
||||
@ -61,7 +61,7 @@ example = extrude(exampleSketch, length = 10)
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 60deg, length = 10)
|
||||
|> angledLine(angle = 60, length = 10)
|
||||
|> tangentialArc(endAbsolute = [15, 15])
|
||||
|> line(end = [10, -15])
|
||||
|> close()
|
||||
@ -75,9 +75,9 @@ example = extrude(exampleSketch, length = 10)
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 60deg, length = 10)
|
||||
|> tangentialArc(radius = 10, angle = -120deg)
|
||||
|> angledLine(angle = -60deg, length = 10)
|
||||
|> angledLine(angle = 60, length = 10)
|
||||
|> tangentialArc(radius = 10, angle = -120)
|
||||
|> angledLine(angle = -60, length = 10)
|
||||
|> close()
|
||||
|
||||
example = extrude(exampleSketch, length = 10)
|
||||
|
@ -38,10 +38,10 @@ xLine(
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> xLine(length = 15)
|
||||
|> angledLine(angle = 80deg, length = 15)
|
||||
|> angledLine(angle = 80, length = 15)
|
||||
|> line(end = [8, -10])
|
||||
|> xLine(length = 10)
|
||||
|> angledLine(angle = 120deg, length = 30)
|
||||
|> angledLine(angle = 120, length = 30)
|
||||
|> xLine(length = -15)
|
||||
|> close()
|
||||
|
||||
|
@ -38,7 +38,7 @@ yLine(
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> yLine(length = 15)
|
||||
|> angledLine(angle = 30deg, length = 15)
|
||||
|> angledLine(angle = 30, length = 15)
|
||||
|> line(end = [8, -10])
|
||||
|> yLine(length = -5)
|
||||
|> close()
|
||||
|
@ -55,7 +55,7 @@ example = extrude(exampleSketch, length = 5)
|
||||
// Add color to a revolved solid.
|
||||
sketch001 = startSketchOn(XY)
|
||||
|> circle(center = [15, 0], radius = 5)
|
||||
|> revolve(angle = 360deg, axis = Y)
|
||||
|> revolve(angle = 360, axis = Y)
|
||||
|> appearance(color = '#ff0000', metalness = 90, roughness = 90)
|
||||
|
||||
```
|
||||
@ -196,9 +196,9 @@ example = extrude(exampleSketch, length = 1)
|
||||
sweepPath = startSketchOn(XZ)
|
||||
|> startProfile(at = [0.05, 0.05])
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc(angle = 90deg, radius = 5)
|
||||
|> tangentialArc(angle = 90, radius = 5)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc(angle = -90deg, radius = 5)
|
||||
|> tangentialArc(angle = -90, radius = 5)
|
||||
|> line(end = [0, 7])
|
||||
|
||||
pipeHole = startSketchOn(XY)
|
||||
|
@ -50,7 +50,7 @@ Its properties are:
|
||||
|
||||
- `rotation.axis` (a 3D point, defaults to the Z axis)
|
||||
|
||||
- `rotation.angle`
|
||||
- `rotation.angle` (number of degrees)
|
||||
|
||||
- `rotation.origin` (either "local" i.e. rotate around its own center, "global" i.e. rotate around the scene's center, or a 3D point, defaults to "local")
|
||||
|
||||
@ -135,7 +135,7 @@ fn transform(@i) {
|
||||
pow(0.9, exp = i)
|
||||
],
|
||||
// Turn by 15 degrees each time.
|
||||
rotation = { angle = 15deg * i, origin = "local" }
|
||||
rotation = { angle = 15 * i, origin = "local" }
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,7 +171,7 @@ fn transform(@i) {
|
||||
return {
|
||||
translate = [0, 0, -i * width],
|
||||
rotation = {
|
||||
angle = 90deg * i,
|
||||
angle = 90 * i,
|
||||
// Rotate around the overall scene's origin.
|
||||
origin = "global"
|
||||
}
|
||||
@ -219,7 +219,7 @@ fn transform(@i) {
|
||||
// Transform functions can return multiple transforms. They'll be applied in order.
|
||||
return [
|
||||
{ translate = [30 * i, 0, 0] },
|
||||
{ rotation = { angle = 45deg * i } }
|
||||
{ rotation = { angle = 45 * i } }
|
||||
]
|
||||
}
|
||||
startSketchOn(XY)
|
||||
|
@ -72,9 +72,9 @@ rotation.
|
||||
sweepPath = startSketchOn(XZ)
|
||||
|> startProfile(at = [0.05, 0.05])
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc(angle = 90deg, radius = 5)
|
||||
|> tangentialArc(angle = 90, radius = 5)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc(angle = -90deg, radius = 5)
|
||||
|> tangentialArc(angle = -90, radius = 5)
|
||||
|> line(end = [0, 7])
|
||||
|
||||
// Create a hole for the pipe.
|
||||
@ -98,9 +98,9 @@ sweepSketch = startSketchOn(XY)
|
||||
sweepPath = startSketchOn(XZ)
|
||||
|> startProfile(at = [0.05, 0.05])
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc(angle = 90deg, radius = 5)
|
||||
|> tangentialArc(angle = 90, radius = 5)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc(angle = -90deg, radius = 5)
|
||||
|> tangentialArc(angle = -90, radius = 5)
|
||||
|> line(end = [0, 7])
|
||||
|
||||
// Create a hole for the pipe.
|
||||
@ -124,9 +124,9 @@ sweepSketch = startSketchOn(XY)
|
||||
sweepPath = startSketchOn(XZ)
|
||||
|> startProfile(at = [0.05, 0.05])
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc(angle = 90deg, radius = 5)
|
||||
|> tangentialArc(angle = 90, radius = 5)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc(angle = -90deg, radius = 5)
|
||||
|> tangentialArc(angle = -90, radius = 5)
|
||||
|> line(end = [0, 7])
|
||||
|
||||
// Create a hole for the pipe.
|
||||
@ -137,7 +137,7 @@ sweepSketch = startSketchOn(XY)
|
||||
|> circle(center = [0, 0], radius = 2)
|
||||
|> subtract2d(tool = pipeHole)
|
||||
|> sweep(path = sweepPath)
|
||||
|> rotate(axis = Z, angle = 90deg)
|
||||
|> rotate(axis = Z, angle = 90)
|
||||
|
||||
```
|
||||
|
||||
@ -150,7 +150,7 @@ sweepSketch = startSketchOn(XY)
|
||||
import "tests/inputs/cube.sldprt" as cube
|
||||
|
||||
cube
|
||||
|> rotate(axis = [0, 0, 1.0], angle = 9deg)
|
||||
|> rotate(axis = [0, 0, 1.0], angle = 9)
|
||||
|
||||
```
|
||||
|
||||
@ -163,9 +163,9 @@ cube
|
||||
sweepPath = startSketchOn(XZ)
|
||||
|> startProfile(at = [0.05, 0.05])
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc(angle = 90deg, radius = 5)
|
||||
|> tangentialArc(angle = 90, radius = 5)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc(angle = -90deg, radius = 5)
|
||||
|> tangentialArc(angle = -90, radius = 5)
|
||||
|> line(end = [0, 7])
|
||||
|
||||
// Create a hole for the pipe.
|
||||
@ -176,7 +176,7 @@ sweepSketch = startSketchOn(XY)
|
||||
|> circle(center = [0, 0], radius = 2)
|
||||
|> subtract2d(tool = pipeHole)
|
||||
|> sweep(path = sweepPath)
|
||||
|> rotate(axis = [0, 0, 1.0], angle = 90deg)
|
||||
|> rotate(axis = [0, 0, 1.0], angle = 90)
|
||||
|
||||
```
|
||||
|
||||
@ -199,13 +199,13 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
|
||||
sketch002 = startSketchOn(YZ)
|
||||
sweepPath = startProfile(sketch002, at = [0, 0])
|
||||
|> yLine(length = 231.81)
|
||||
|> tangentialArc(radius = 80, angle = -90deg)
|
||||
|> tangentialArc(radius = 80, angle = -90)
|
||||
|> xLine(length = 384.93)
|
||||
|
||||
parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
|
||||
|
||||
// Rotate the sweeps.
|
||||
rotate(parts, axis = [0, 0, 1.0], angle = 90deg)
|
||||
rotate(parts, axis = [0, 0, 1.0], angle = 90)
|
||||
|
||||
```
|
||||
|
||||
@ -228,7 +228,7 @@ profile001 = square()
|
||||
|
||||
profile002 = square()
|
||||
|> translate(x = 0, y = 0, z = 20)
|
||||
|> rotate(axis = [0, 0, 1.0], angle = 45deg)
|
||||
|> rotate(axis = [0, 0, 1.0], angle = 45)
|
||||
|
||||
loft([profile001, profile002])
|
||||
|
||||
|
@ -54,9 +54,9 @@ look like the model moves and gets bigger at the same time. Say you have a squar
|
||||
sweepPath = startSketchOn(XZ)
|
||||
|> startProfile(at = [0.05, 0.05])
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc(angle = 90deg, radius = 5)
|
||||
|> tangentialArc(angle = 90, radius = 5)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc(angle = -90deg, radius = 5)
|
||||
|> tangentialArc(angle = -90, radius = 5)
|
||||
|> line(end = [0, 7])
|
||||
|
||||
// Create a hole for the pipe.
|
||||
@ -93,7 +93,7 @@ cube
|
||||
sketch001 = startSketchOn(XY)
|
||||
rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
|
||||
|> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA001) - 90deg, length = 50.61)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 50.61)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
@ -103,7 +103,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
|
||||
sketch002 = startSketchOn(YZ)
|
||||
sweepPath = startProfile(sketch002, at = [0, 0])
|
||||
|> yLine(length = 231.81)
|
||||
|> tangentialArc(radius = 80, angle = -90deg)
|
||||
|> tangentialArc(radius = 80, angle = -90)
|
||||
|> xLine(length = 384.93)
|
||||
|
||||
parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
|
||||
|
@ -47,9 +47,9 @@ and then rotate it using the `rotate` function to create a loft.
|
||||
sweepPath = startSketchOn(XZ)
|
||||
|> startProfile(at = [0.05, 0.05])
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc(angle = 90deg, radius = 5)
|
||||
|> tangentialArc(angle = 90, radius = 5)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc(angle = -90deg, radius = 5)
|
||||
|> tangentialArc(angle = -90, radius = 5)
|
||||
|> line(end = [0, 7])
|
||||
|
||||
// Create a hole for the pipe.
|
||||
@ -91,7 +91,7 @@ cube
|
||||
sketch001 = startSketchOn(XY)
|
||||
rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
|
||||
|> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA001) - 90deg, length = 50.61)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 50.61)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
@ -101,7 +101,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
|
||||
sketch002 = startSketchOn(YZ)
|
||||
sweepPath = startProfile(sketch002, at = [0, 0])
|
||||
|> yLine(length = 231.81)
|
||||
|> tangentialArc(radius = 80, angle = -90deg)
|
||||
|> tangentialArc(radius = 80, angle = -90)
|
||||
|> xLine(length = 384.93)
|
||||
|
||||
parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
|
||||
@ -162,7 +162,7 @@ profile001 = square()
|
||||
|
||||
profile002 = square()
|
||||
|> translate(z = 20)
|
||||
|> rotate(axis = [0, 0, 1.0], angle = 45deg)
|
||||
|> rotate(axis = [0, 0, 1.0], angle = 45)
|
||||
|
||||
loft([profile001, profile002])
|
||||
|
||||
|
@ -29,7 +29,7 @@ units::toDegrees(@num: number(Angle)): number(deg)
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 50deg, length = 70 * cos(units::toDegrees((PI / 4): number(rad))))
|
||||
|> angledLine(angle = 50, length = 70 * cos(units::toDegrees((PI / 4): number(rad))))
|
||||
|> yLine(endAbsolute = 0)
|
||||
|> close()
|
||||
|
||||
|
@ -29,7 +29,7 @@ units::toRadians(@num: number(Angle)): number(rad)
|
||||
```kcl
|
||||
exampleSketch = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> angledLine(angle = 50deg, length = 70 * cos(units::toRadians(45deg)))
|
||||
|> angledLine(angle = 50, length = 70 * cos(units::toRadians(45)))
|
||||
|> yLine(endAbsolute = 0)
|
||||
|> close()
|
||||
|
||||
|
67
docs/kcl-std/subtract2d.md
Normal file
@ -17,7 +17,7 @@ startSketchOn(XZ)
|
||||
|> startProfile(at = origin)
|
||||
|> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001)
|
||||
|> angledLine(
|
||||
angle = segAng(rectangleSegmentA001) - 90deg,
|
||||
angle = segAng(rectangleSegmentA001) - 90,
|
||||
length = 196.99,
|
||||
tag = $rectangleSegmentB001,
|
||||
)
|
||||
@ -80,7 +80,7 @@ fn rect(origin) {
|
||||
|> startProfile(at = origin)
|
||||
|> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001)
|
||||
|> angledLine(
|
||||
angle = segAng(rectangleSegmentA001) - 90deg,
|
||||
angle = segAng(rectangleSegmentA001) - 90,
|
||||
length = 196.99
|
||||
tag = $rectangleSegmentB001,
|
||||
)
|
||||
|
@ -302,7 +302,7 @@ test.describe('Command bar tests', () => {
|
||||
|
||||
// Assert that the an alternative variable name is chosen,
|
||||
// since the default variable name is already in use (distance)
|
||||
await cmdBar.variableCheckbox.click()
|
||||
await page.getByRole('button', { name: 'Create new variable' }).click()
|
||||
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
|
||||
'length001'
|
||||
)
|
||||
|
@ -281,7 +281,10 @@ test.describe('Feature Tree pane', () => {
|
||||
|
||||
await test.step('Add a named constant for distance argument and submit', async () => {
|
||||
await expect(cmdBar.currentArgumentInput).toBeVisible()
|
||||
await cmdBar.variableCheckbox.click()
|
||||
const addVariableButton = page.getByRole('button', {
|
||||
name: 'Create new variable',
|
||||
})
|
||||
await addVariableButton.click()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
|
@ -172,10 +172,6 @@ export class CmdBarFixture {
|
||||
return this.page.getByTestId('cmd-bar-arg-value')
|
||||
}
|
||||
|
||||
get variableCheckbox() {
|
||||
return this.page.getByTestId('cmd-bar-variable-checkbox')
|
||||
}
|
||||
|
||||
get cmdOptions() {
|
||||
return this.page.getByTestId('cmd-bar-option')
|
||||
}
|
||||
@ -195,7 +191,7 @@ export class CmdBarFixture {
|
||||
* Clicks the Create new variable button for kcl input
|
||||
*/
|
||||
createNewVariable = async () => {
|
||||
await this.variableCheckbox.click()
|
||||
await this.page.getByRole('button', { name: 'Create new variable' }).click()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3807,7 +3807,7 @@ sketch002 = startSketchOn(extrude001, face = rectangleSegmentA001)
|
||||
stage: 'arguments',
|
||||
})
|
||||
await page.keyboard.insertText(newAngle)
|
||||
await cmdBar.variableCheckbox.click()
|
||||
await page.getByRole('button', { name: 'Create new variable' }).click()
|
||||
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
|
||||
'angle001'
|
||||
)
|
||||
|
@ -170,7 +170,7 @@ test(
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
const crypticErrorText =
|
||||
'tag requires a value with type `tag`, but found a value with type `string`.'
|
||||
'tag requires a value with type `tag`, but found string'
|
||||
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
|
||||
|
||||
// black pixel means the scene has been cleared.
|
||||
@ -369,7 +369,7 @@ test(
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
const crypticErrorText =
|
||||
'tag requires a value with type `tag`, but found a value with type `string`.'
|
||||
'tag requires a value with type `tag`, but found string'
|
||||
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
|
||||
|
||||
// black pixel means the scene has been cleared.
|
||||
@ -408,7 +408,7 @@ test(
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
const crypticErrorText =
|
||||
'tag requires a value with type `tag`, but found a value with type `string`.'
|
||||
'tag requires a value with type `tag`, but found string'
|
||||
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
|
||||
}
|
||||
)
|
||||
|
@ -1,54 +1,45 @@
|
||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
async function navigateAndClickOpenInDesktopApp(
|
||||
page: Page,
|
||||
codeLength: number
|
||||
) {
|
||||
const code = Array(codeLength).fill('0').join('')
|
||||
const targetURL = `?create-file=true&browser=test&code=${code}&ask-open-desktop=true`
|
||||
expect(targetURL.length).toEqual(codeLength + 58)
|
||||
await page.goto(page.url() + targetURL)
|
||||
expect(page.url()).toContain(targetURL)
|
||||
const button = page.getByRole('button', { name: 'Open in desktop app' })
|
||||
await button.click()
|
||||
}
|
||||
|
||||
function getToastError(page: Page) {
|
||||
return page.getByText('The URL is too long to open in the desktop app')
|
||||
}
|
||||
|
||||
const isWindows =
|
||||
navigator.platform === 'Windows' || navigator.platform === 'Win32'
|
||||
test.describe('Share link tests', () => {
|
||||
test(
|
||||
`Open in desktop app with 2000-long code works non-Windows`,
|
||||
{ tag: ['@web', '@macos', '@linux'] },
|
||||
async ({ page }) => {
|
||||
test.skip(process.platform === 'win32')
|
||||
const codeLength = 2000
|
||||
await navigateAndClickOpenInDesktopApp(page, codeLength)
|
||||
await expect(getToastError(page)).not.toBeVisible()
|
||||
}
|
||||
)
|
||||
;[
|
||||
{
|
||||
codeLength: 1000,
|
||||
showsErrorOnWindows: false,
|
||||
},
|
||||
{
|
||||
codeLength: 2000,
|
||||
showsErrorOnWindows: true,
|
||||
},
|
||||
].forEach(({ codeLength, showsErrorOnWindows }) => {
|
||||
test(
|
||||
`Open in desktop app with ${codeLength}-long code ${isWindows && showsErrorOnWindows ? 'shows error' : "doesn't show error"}`,
|
||||
{ tag: ['@web'] },
|
||||
async ({ page }) => {
|
||||
if (process.env.TARGET !== 'web') {
|
||||
// This test is web-only
|
||||
// TODO: re-enable on CI as part of a new @web test suite
|
||||
return
|
||||
}
|
||||
|
||||
test(
|
||||
`Open in desktop app with 1000-long code works on Windows`,
|
||||
{ tag: ['@web', '@windows'] },
|
||||
async ({ page }) => {
|
||||
test.skip(process.platform !== 'win32')
|
||||
const codeLength = 1000
|
||||
await navigateAndClickOpenInDesktopApp(page, codeLength)
|
||||
await expect(getToastError(page)).not.toBeVisible()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`Open in desktop app with 2000-long code doesn't work on Windows`,
|
||||
{ tag: ['@web', '@windows'] },
|
||||
async ({ page }) => {
|
||||
test.skip(process.platform !== 'win32')
|
||||
const codeLength = 2000
|
||||
await navigateAndClickOpenInDesktopApp(page, codeLength)
|
||||
await expect(getToastError(page)).toBeVisible()
|
||||
}
|
||||
)
|
||||
const code = Array(codeLength).fill('0').join('')
|
||||
const targetURL = `?create-file=true&browser=test&code=${code}&ask-open-desktop=true`
|
||||
expect(targetURL.length).toEqual(codeLength + 58)
|
||||
await page.goto(page.url() + targetURL)
|
||||
expect(page.url()).toContain(targetURL)
|
||||
const button = page.getByRole('button', { name: 'Open in desktop app' })
|
||||
await button.click()
|
||||
const toastError = page.getByText(
|
||||
'The URL is too long to open in the desktop app on Windows'
|
||||
)
|
||||
if (isWindows && showsErrorOnWindows) {
|
||||
await expect(toastError).toBeVisible()
|
||||
} else {
|
||||
await expect(toastError).not.toBeVisible()
|
||||
// TODO: check if we could verify the deep link dialog shows up
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -3433,71 +3433,6 @@ profile003 = startProfile(sketch002, at = [-201.08, 254.17])
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
test('Will exit out of sketch mode when all code is nuked', async ({
|
||||
page,
|
||||
context,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `myVar1 = 5
|
||||
myVar2 = 6
|
||||
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile001 = startProfile(sketch001, at = [106.68, 89.77])
|
||||
|> line(end = [132.34, 157.8])
|
||||
|> line(end = [67.65, -460.55], tag = $seg01)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(profile001, length = 500)
|
||||
sketch002 = startSketchOn(extrude001, face = seg01)
|
||||
profile002 = startProfile(sketch002, at = [83.39, 329.15])
|
||||
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|
||||
|> angledLine(length = 156.54, angle = -28)
|
||||
|> angledLine(
|
||||
angle = -151,
|
||||
length = 116.27,
|
||||
)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
profile003 = startProfile(sketch002, at = [-201.08, 254.17])
|
||||
|> line(end = [103.55, 33.32])
|
||||
|> line(end = [48.8, -153.54])`
|
||||
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await scene.connectionEstablished()
|
||||
await scene.settled(cmdBar)
|
||||
const expectSketchOriginToBeDrawn = async () => {
|
||||
await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 672, y: 193 }, 15)
|
||||
}
|
||||
|
||||
await test.step('Open feature tree and edit second sketch', async () => {
|
||||
await toolbar.openFeatureTreePane()
|
||||
const sketchButton = await toolbar.getFeatureTreeOperation('Sketch', 1)
|
||||
await sketchButton.dblclick()
|
||||
await page.waitForTimeout(700) // Wait for engine animation
|
||||
await expectSketchOriginToBeDrawn()
|
||||
})
|
||||
|
||||
await test.step('clear editor content while in sketch mode', async () => {
|
||||
await editor.replaceCode('', '')
|
||||
await page.waitForTimeout(100)
|
||||
await expect(
|
||||
page.getByText('Unable to maintain sketch mode')
|
||||
).toBeVisible()
|
||||
await scene.expectPixelColorNotToBe(
|
||||
TEST_COLORS.WHITE,
|
||||
{ x: 672, y: 193 },
|
||||
15
|
||||
)
|
||||
})
|
||||
})
|
||||
test('empty draft sketch is cleaned up properly', async ({
|
||||
scene,
|
||||
toolbar,
|
||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
@ -1198,174 +1198,3 @@ export async function enableConsoleLogEverything({
|
||||
console.log(`[Main] ${msg.type()}: ${msg.text()}`)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a pan touch gesture from the center of an element.
|
||||
*
|
||||
* Adapted from Playwright docs: https://playwright.dev/docs/touch-events
|
||||
*/
|
||||
export async function panFromCenter(
|
||||
locator: Locator,
|
||||
deltaX = 0,
|
||||
deltaY = 0,
|
||||
steps = 5
|
||||
) {
|
||||
const { centerX, centerY } = await locator.evaluate((target: HTMLElement) => {
|
||||
const bounds = target.getBoundingClientRect()
|
||||
const centerX = bounds.left + bounds.width / 2
|
||||
const centerY = bounds.top + bounds.height / 2
|
||||
return { centerX, centerY }
|
||||
})
|
||||
|
||||
// Providing only clientX and clientY as the app only cares about those.
|
||||
const touches = [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: centerX,
|
||||
clientY: centerY,
|
||||
},
|
||||
]
|
||||
await locator.dispatchEvent('touchstart', {
|
||||
touches,
|
||||
changedTouches: touches,
|
||||
targetTouches: touches,
|
||||
})
|
||||
|
||||
for (let j = 1; j <= steps; j++) {
|
||||
const touches = [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: centerX + (deltaX * j) / steps,
|
||||
clientY: centerY + (deltaY * j) / steps,
|
||||
},
|
||||
]
|
||||
await locator.dispatchEvent('touchmove', {
|
||||
touches,
|
||||
changedTouches: touches,
|
||||
targetTouches: touches,
|
||||
})
|
||||
}
|
||||
|
||||
await locator.dispatchEvent('touchend')
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a 2-finger pan touch gesture from the center of an element.
|
||||
* with {touchSpacing} pixels between.
|
||||
*
|
||||
* Adapted from Playwright docs: https://playwright.dev/docs/touch-events
|
||||
*/
|
||||
export async function panTwoFingerFromCenter(
|
||||
locator: Locator,
|
||||
deltaX = 0,
|
||||
deltaY = 0,
|
||||
steps = 5,
|
||||
spacingX = 20
|
||||
) {
|
||||
const { centerX, centerY } = await locator.evaluate((target: HTMLElement) => {
|
||||
const bounds = target.getBoundingClientRect()
|
||||
const centerX = bounds.left + bounds.width / 2
|
||||
const centerY = bounds.top + bounds.height / 2
|
||||
return { centerX, centerY }
|
||||
})
|
||||
|
||||
// Providing only clientX and clientY as the app only cares about those.
|
||||
const touches = [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: centerX,
|
||||
clientY: centerY,
|
||||
},
|
||||
{
|
||||
identifier: 1,
|
||||
clientX: centerX + spacingX,
|
||||
clientY: centerY,
|
||||
},
|
||||
]
|
||||
await locator.dispatchEvent('touchstart', {
|
||||
touches,
|
||||
changedTouches: touches,
|
||||
targetTouches: touches,
|
||||
})
|
||||
|
||||
for (let j = 1; j <= steps; j++) {
|
||||
const touches = [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: centerX + (deltaX * j) / steps,
|
||||
clientY: centerY + (deltaY * j) / steps,
|
||||
},
|
||||
{
|
||||
identifier: 1,
|
||||
clientX: centerX + spacingX + (deltaX * j) / steps,
|
||||
clientY: centerY + (deltaY * j) / steps,
|
||||
},
|
||||
]
|
||||
await locator.dispatchEvent('touchmove', {
|
||||
touches,
|
||||
changedTouches: touches,
|
||||
targetTouches: touches,
|
||||
})
|
||||
}
|
||||
|
||||
await locator.dispatchEvent('touchend')
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate a pinch touch gesture from the center of an element.
|
||||
* Touch points are set horizontally from each other, separated by {startDistance} pixels.
|
||||
*/
|
||||
export async function pinchFromCenter(
|
||||
locator: Locator,
|
||||
startDistance = 100,
|
||||
delta = 0,
|
||||
steps = 5
|
||||
) {
|
||||
const { centerX, centerY } = await locator.evaluate((target: HTMLElement) => {
|
||||
const bounds = target.getBoundingClientRect()
|
||||
const centerX = bounds.left + bounds.width / 2
|
||||
const centerY = bounds.top + bounds.height / 2
|
||||
return { centerX, centerY }
|
||||
})
|
||||
|
||||
// Providing only clientX and clientY as the app only cares about those.
|
||||
const touches = [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: centerX - startDistance / 2,
|
||||
clientY: centerY,
|
||||
},
|
||||
{
|
||||
identifier: 1,
|
||||
clientX: centerX + startDistance / 2,
|
||||
clientY: centerY,
|
||||
},
|
||||
]
|
||||
await locator.dispatchEvent('touchstart', {
|
||||
touches,
|
||||
changedTouches: touches,
|
||||
targetTouches: touches,
|
||||
})
|
||||
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
const touches = [
|
||||
{
|
||||
identifier: 0,
|
||||
clientX: centerX - startDistance / 2 + (delta * i) / steps,
|
||||
clientY: centerY,
|
||||
},
|
||||
{
|
||||
identifier: 1,
|
||||
clientX: centerX + startDistance / 2 + (delta * i) / steps,
|
||||
clientY: centerY,
|
||||
},
|
||||
]
|
||||
await locator.dispatchEvent('touchmove', {
|
||||
touches,
|
||||
changedTouches: touches,
|
||||
targetTouches: touches,
|
||||
})
|
||||
}
|
||||
|
||||
await locator.dispatchEvent('touchend')
|
||||
}
|
||||
|
@ -1,146 +0,0 @@
|
||||
import { getUtils } from '@e2e/playwright/test-utils'
|
||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||
import { type Page } from '@playwright/test'
|
||||
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
|
||||
|
||||
test.use({
|
||||
hasTouch: true,
|
||||
})
|
||||
test.describe('Testing Camera Movement (Touch Only)', () => {
|
||||
/**
|
||||
* DUPLICATED FROM `testing-camera-movement.spec.ts`, might need to become a util.
|
||||
*
|
||||
* hack that we're implemented our own retry instead of using retries built into playwright.
|
||||
* however each of these camera drags can be flaky, because of udp
|
||||
* and so putting them together means only one needs to fail to make this test extra flaky.
|
||||
* this way we can retry within the test
|
||||
* We could break them out into separate tests, but the longest past of the test is waiting
|
||||
* for the stream to start, so it can be good to bundle related things together.
|
||||
*/
|
||||
const _bakeInRetries = async ({
|
||||
mouseActions,
|
||||
afterPosition,
|
||||
beforePosition,
|
||||
retryCount = 0,
|
||||
page,
|
||||
scene,
|
||||
}: {
|
||||
mouseActions: () => Promise<void>
|
||||
beforePosition: [number, number, number]
|
||||
afterPosition: [number, number, number]
|
||||
retryCount?: number
|
||||
page: Page
|
||||
scene: SceneFixture
|
||||
}) => {
|
||||
const acceptableCamError = 5
|
||||
const u = await getUtils(page)
|
||||
|
||||
await test.step('Set up initial camera position', async () =>
|
||||
await scene.moveCameraTo({
|
||||
x: beforePosition[0],
|
||||
y: beforePosition[1],
|
||||
z: beforePosition[2],
|
||||
}))
|
||||
|
||||
await test.step('Do actions and watch for changes', async () =>
|
||||
u.doAndWaitForImageDiff(async () => {
|
||||
await mouseActions()
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.closeDebugPanel()
|
||||
await page.waitForTimeout(100)
|
||||
}, 300))
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
await expect(page.getByTestId('cam-x-position')).toBeAttached()
|
||||
|
||||
const vals = await Promise.all([
|
||||
page.getByTestId('cam-x-position').inputValue(),
|
||||
page.getByTestId('cam-y-position').inputValue(),
|
||||
page.getByTestId('cam-z-position').inputValue(),
|
||||
])
|
||||
const errors = vals.map((v, i) => Math.abs(Number(v) - afterPosition[i]))
|
||||
let shouldRetry = false
|
||||
|
||||
if (errors.some((e) => e > acceptableCamError)) {
|
||||
if (retryCount > 2) {
|
||||
console.log('xVal', vals[0], 'xError', errors[0])
|
||||
console.log('yVal', vals[1], 'yError', errors[1])
|
||||
console.log('zVal', vals[2], 'zError', errors[2])
|
||||
|
||||
throw new Error('Camera position not as expected', {
|
||||
cause: {
|
||||
vals,
|
||||
errors,
|
||||
},
|
||||
})
|
||||
}
|
||||
shouldRetry = true
|
||||
}
|
||||
if (shouldRetry) {
|
||||
await _bakeInRetries({
|
||||
mouseActions,
|
||||
afterPosition: afterPosition,
|
||||
beforePosition: beforePosition,
|
||||
retryCount: retryCount + 1,
|
||||
page,
|
||||
scene,
|
||||
})
|
||||
}
|
||||
}
|
||||
// test(
|
||||
// 'Touch camera controls',
|
||||
// {
|
||||
// tag: '@web',
|
||||
// },
|
||||
// async ({ page, homePage, scene, cmdBar }) => {
|
||||
// const u = await getUtils(page)
|
||||
// const camInitialPosition: [number, number, number] = [0, 85, 85]
|
||||
//
|
||||
// await homePage.goToModelingScene()
|
||||
// await scene.settled(cmdBar)
|
||||
// const stream = page.getByTestId('stream')
|
||||
//
|
||||
// await u.openAndClearDebugPanel()
|
||||
// await u.closeKclCodePanel()
|
||||
//
|
||||
// await test.step('Orbit', async () => {
|
||||
// await bakeInRetries({
|
||||
// mouseActions: async () => {
|
||||
// await panFromCenter(stream, 200, 200)
|
||||
// await page.waitForTimeout(200)
|
||||
// },
|
||||
// afterPosition: [19, 85, 85],
|
||||
// beforePosition: camInitialPosition,
|
||||
// page,
|
||||
// scene,
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// await test.step('Pan', async () => {
|
||||
// await bakeInRetries({
|
||||
// mouseActions: async () => {
|
||||
// await panTwoFingerFromCenter(stream, 200, 200)
|
||||
// await page.waitForTimeout(200)
|
||||
// },
|
||||
// afterPosition: [19, 85, 85],
|
||||
// beforePosition: camInitialPosition,
|
||||
// page,
|
||||
// scene,
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// await test.step('Zoom', async () => {
|
||||
// await bakeInRetries({
|
||||
// mouseActions: async () => {
|
||||
// await pinchFromCenter(stream, 300, -100, 5)
|
||||
// },
|
||||
// afterPosition: [0, 118, 118],
|
||||
// beforePosition: camInitialPosition,
|
||||
// page,
|
||||
// scene,
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
// )
|
||||
})
|
18
package-lock.json
generated
@ -47,7 +47,6 @@
|
||||
"diff": "^7.0.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
"fuse.js": "^7.1.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"html2canvas-pro": "^1.5.8",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"json-rpc-2.0": "^1.6.0",
|
||||
@ -94,7 +93,6 @@
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/electron": "^1.6.10",
|
||||
"@types/hammerjs": "^2.0.46",
|
||||
"@types/isomorphic-fetch": "^0.0.39",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/minimist": "^1.2.5",
|
||||
@ -7493,13 +7491,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hammerjs": {
|
||||
"version": "2.0.46",
|
||||
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz",
|
||||
"integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/http-cache-semantics": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
|
||||
@ -15183,15 +15174,6 @@
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hammerjs": {
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
|
||||
"integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/happy-dom": {
|
||||
"version": "17.4.4",
|
||||
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.4.4.tgz",
|
||||
|
@ -49,7 +49,6 @@
|
||||
"diff": "^7.0.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
"fuse.js": "^7.1.0",
|
||||
"hammerjs": "^2.0.8",
|
||||
"html2canvas-pro": "^1.5.8",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"json-rpc-2.0": "^1.6.0",
|
||||
@ -116,10 +115,10 @@
|
||||
"circular-deps:diff:nodejs": "npm run circular-deps:diff || node ./scripts/diff.js",
|
||||
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
|
||||
"files:set-notes": "./scripts/set-files-notes.sh",
|
||||
"files:flip-to-staging": "./scripts/flip-files-to-staging.sh",
|
||||
"files:flip-to-staging:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/flip-files-to-staging.ps1",
|
||||
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
|
||||
"files:flip-to-nightly:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/flip-files-to-nightly.ps1",
|
||||
"files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh",
|
||||
"files:invalidate-bucket:staging": "./scripts/invalidate-files-bucket.sh --staging",
|
||||
"files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly",
|
||||
"postinstall": "electron-rebuild",
|
||||
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
|
||||
"tron:start": "electron-forge start",
|
||||
@ -169,7 +168,6 @@
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@types/diff": "^7.0.2",
|
||||
"@types/electron": "^1.6.10",
|
||||
"@types/hammerjs": "^2.0.46",
|
||||
"@types/isomorphic-fetch": "^0.0.39",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/minimist": "^1.2.5",
|
||||
|
20
rust/Cargo.lock
generated
@ -1792,7 +1792,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-bumper"
|
||||
version = "0.1.81"
|
||||
version = "0.1.80"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@ -1803,7 +1803,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-derive-docs"
|
||||
version = "0.1.81"
|
||||
version = "0.1.80"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1812,7 +1812,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-directory-test-macro"
|
||||
version = "0.1.81"
|
||||
version = "0.1.80"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
@ -1822,7 +1822,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-language-server"
|
||||
version = "0.2.81"
|
||||
version = "0.2.80"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@ -1843,7 +1843,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-language-server-release"
|
||||
version = "0.1.81"
|
||||
version = "0.1.80"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@ -1863,7 +1863,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-lib"
|
||||
version = "0.2.81"
|
||||
version = "0.2.80"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"approx 0.5.1",
|
||||
@ -1940,7 +1940,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-python-bindings"
|
||||
version = "0.3.81"
|
||||
version = "0.3.80"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"kcl-lib",
|
||||
@ -1955,7 +1955,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-test-server"
|
||||
version = "0.1.81"
|
||||
version = "0.1.80"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hyper 0.14.32",
|
||||
@ -1968,7 +1968,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-to-core"
|
||||
version = "0.1.81"
|
||||
version = "0.1.80"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1982,7 +1982,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kcl-wasm-lib"
|
||||
version = "0.1.81"
|
||||
version = "0.1.80"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bson",
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
[package]
|
||||
name = "kcl-bumper"
|
||||
version = "0.1.81"
|
||||
version = "0.1.80"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/KittyCAD/modeling-api"
|
||||
rust-version = "1.76"
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-derive-docs"
|
||||
description = "A tool for generating documentation from Rust derive macros"
|
||||
version = "0.1.81"
|
||||
version = "0.1.80"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -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.81"
|
||||
version = "0.1.80"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "kcl-language-server-release"
|
||||
version = "0.1.81"
|
||||
version = "0.1.80"
|
||||
edition = "2021"
|
||||
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
||||
publish = false
|
||||
|
@ -2,7 +2,7 @@
|
||||
name = "kcl-language-server"
|
||||
description = "A language server for KCL."
|
||||
authors = ["KittyCAD Inc <kcl@kittycad.io>"]
|
||||
version = "0.2.81"
|
||||
version = "0.2.80"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "kcl-lib"
|
||||
description = "KittyCAD Language implementation and tools"
|
||||
version = "0.2.81"
|
||||
version = "0.2.80"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/KittyCAD/modeling-app"
|
||||
|
@ -300,6 +300,12 @@ extrude001 = extrude(profile001, length = 4)
|
||||
let first = &result.first().unwrap().2;
|
||||
let second = &result.last().unwrap().2;
|
||||
|
||||
assert!(
|
||||
first.artifact_commands.len() < second.artifact_commands.len(),
|
||||
"Second should have all the artifact commands of the first, plus more. first={:?}, second={:?}",
|
||||
first.artifact_commands.len(),
|
||||
second.artifact_commands.len()
|
||||
);
|
||||
assert!(
|
||||
first.artifact_graph.len() < second.artifact_graph.len(),
|
||||
"Second should have all the artifacts of the first, plus more. first={:?}, second={:?}",
|
||||
|
@ -1230,10 +1230,7 @@ secondSketch = startSketchOn(part001, face = '')
|
||||
let result = execute_and_snapshot(code, None).await;
|
||||
let err = result.unwrap_err();
|
||||
let err = err.as_kcl_error().unwrap();
|
||||
assert_eq!(
|
||||
err.message(),
|
||||
"face requires a value with type `tag`, but found a value with type `string`."
|
||||
);
|
||||
assert_eq!(err.message(), "face requires a value with type `tag`, but found string");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@ -1965,7 +1962,7 @@ someFunction('INVALID')
|
||||
let err = err.as_kcl_error().unwrap();
|
||||
assert_eq!(
|
||||
err.message(),
|
||||
"The input argument of `startSketchOn` requires a value with type `Solid` or a value with type `Plane` (`Solid | Plane`), but found a value with type `string`."
|
||||
"The input argument of `startSketchOn` requires a value with type `Solid | Plane`, but found string"
|
||||
);
|
||||
assert_eq!(
|
||||
err.source_ranges(),
|
||||
@ -2090,7 +2087,7 @@ async fn kcl_test_better_type_names() {
|
||||
},
|
||||
None => todo!(),
|
||||
};
|
||||
assert_eq!(err, "This function expected the input argument to be one or more Solids or ImportedGeometry but it's actually of type Sketch. You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`");
|
||||
assert_eq!(err, "This function expected the input argument to be one or more Solids or imported geometry but it's actually of type Sketch. You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
|
@ -508,9 +508,8 @@ impl EngineManager for EngineConnection {
|
||||
.await?;
|
||||
|
||||
// Wait for the response.
|
||||
let response_timeout = 300;
|
||||
let current_time = std::time::Instant::now();
|
||||
while current_time.elapsed().as_secs() < response_timeout {
|
||||
while current_time.elapsed().as_secs() < 60 {
|
||||
let guard = self.socket_health.read().await;
|
||||
if *guard == SocketHealth::Inactive {
|
||||
// Check if we have any pending errors.
|
||||
|
@ -135,10 +135,8 @@ pub struct KclErrorWithOutputs {
|
||||
pub non_fatal: Vec<CompilationError>,
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
pub operations: Vec<Operation>,
|
||||
// TODO: Remove this field. Doing so breaks the ts-rs output for some
|
||||
// reason.
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
pub _artifact_commands: Vec<ArtifactCommand>,
|
||||
pub artifact_commands: Vec<ArtifactCommand>,
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
pub artifact_graph: ArtifactGraph,
|
||||
pub filenames: IndexMap<ModuleId, ModulePath>,
|
||||
@ -164,7 +162,7 @@ impl KclErrorWithOutputs {
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
operations,
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
_artifact_commands: artifact_commands,
|
||||
artifact_commands,
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
artifact_graph,
|
||||
filenames,
|
||||
@ -179,7 +177,7 @@ impl KclErrorWithOutputs {
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
operations: Default::default(),
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
_artifact_commands: Default::default(),
|
||||
artifact_commands: Default::default(),
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
artifact_graph: Default::default(),
|
||||
filenames: Default::default(),
|
||||
@ -783,7 +781,6 @@ impl Severity {
|
||||
pub enum Tag {
|
||||
Deprecated,
|
||||
Unnecessary,
|
||||
UnknownNumericUnits,
|
||||
None,
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,26 @@ pub struct ArtifactCommand {
|
||||
pub command: ModelingCmd,
|
||||
}
|
||||
|
||||
impl PartialOrd for ArtifactCommand {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
// Order by the source range.
|
||||
let range = self.range.cmp(&other.range);
|
||||
if range != std::cmp::Ordering::Equal {
|
||||
return Some(range);
|
||||
}
|
||||
#[cfg(test)]
|
||||
{
|
||||
// If the ranges are equal, order by the serde variant.
|
||||
Some(
|
||||
crate::variant_name::variant_name(&self.command)
|
||||
.cmp(&crate::variant_name::variant_name(&other.command)),
|
||||
)
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
self.cmd_id.partial_cmp(&other.cmd_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub type DummyPathToNode = Vec<()>;
|
||||
|
||||
fn serialize_dummy_path_to_node<S>(_path_to_node: &DummyPathToNode, serializer: S) -> Result<S::Ok, S::Error>
|
||||
|
@ -111,6 +111,8 @@ impl GlobalState {
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
operations: self.exec_state.artifacts.operations,
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
artifact_commands: self.exec_state.artifacts.commands,
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
artifact_graph: self.exec_state.artifacts.graph,
|
||||
errors: self.exec_state.errors,
|
||||
default_planes: ctx.engine.get_default_planes().read().await.clone(),
|
||||
|
@ -2,9 +2,7 @@ use indexmap::IndexMap;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{types::NumericType, ArtifactId, KclValue};
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
use crate::parsing::ast::types::{Node, Program};
|
||||
use crate::{parsing::ast::types::ItemVisibility, ModuleId, NodePath, SourceRange};
|
||||
use crate::{ModuleId, NodePath, SourceRange};
|
||||
|
||||
/// A CAD modeling operation for display in the feature tree, AKA operations
|
||||
/// timeline.
|
||||
@ -28,20 +26,6 @@ pub enum Operation {
|
||||
is_error: bool,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
VariableDeclaration {
|
||||
/// The variable name.
|
||||
name: String,
|
||||
/// The value of the variable.
|
||||
value: OpKclValue,
|
||||
/// The visibility modifier of the variable, e.g. `export`. `Default`
|
||||
/// means there is no visibility modifier.
|
||||
visibility: ItemVisibility,
|
||||
/// The node path of the operation in the source code.
|
||||
node_path: NodePath,
|
||||
/// The source range of the operation in the source code.
|
||||
source_range: SourceRange,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
GroupBegin {
|
||||
/// The details of the group.
|
||||
group: Group,
|
||||
@ -53,36 +37,32 @@ pub enum Operation {
|
||||
GroupEnd,
|
||||
}
|
||||
|
||||
/// A way for sorting the operations in the timeline. This is used to sort
|
||||
/// operations in the timeline and to determine the order of operations.
|
||||
/// We use this for the multi-threaded snapshotting, so that we can have deterministic
|
||||
/// output.
|
||||
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::GroupBegin { source_range: b, .. }) => a.cmp(b),
|
||||
(Self::StdLibCall { .. }, 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 { .. }, Self::GroupEnd) => std::cmp::Ordering::Less,
|
||||
(Self::GroupEnd, Self::StdLibCall { .. }) => std::cmp::Ordering::Greater,
|
||||
(Self::GroupEnd, Self::GroupBegin { .. }) => std::cmp::Ordering::Greater,
|
||||
(Self::GroupEnd, Self::GroupEnd) => std::cmp::Ordering::Equal,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
/// If the variant is `StdLibCall`, set the `is_error` field.
|
||||
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::VariableDeclaration { .. } | Self::GroupBegin { .. } | Self::GroupEnd => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
pub(crate) fn fill_node_paths(&mut self, program: &Node<Program>, cached_body_items: usize) {
|
||||
match self {
|
||||
Operation::StdLibCall {
|
||||
node_path,
|
||||
source_range,
|
||||
..
|
||||
}
|
||||
| Operation::VariableDeclaration {
|
||||
node_path,
|
||||
source_range,
|
||||
..
|
||||
}
|
||||
| Operation::GroupBegin {
|
||||
node_path,
|
||||
source_range,
|
||||
..
|
||||
} => {
|
||||
node_path.fill_placeholder(program, cached_body_items, *source_range);
|
||||
}
|
||||
Operation::GroupEnd => {}
|
||||
Self::GroupBegin { .. } | Self::GroupEnd => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,14 +6,13 @@ use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
execution::{
|
||||
annotations,
|
||||
cad_op::OpKclValue,
|
||||
fn_call::Args,
|
||||
kcl_value::{FunctionSource, TypeDef},
|
||||
memory,
|
||||
state::ModuleState,
|
||||
types::{NumericType, PrimitiveType, RuntimeType},
|
||||
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, ModelingCmdMeta, ModuleArtifactState,
|
||||
Operation, PlaneType, StatementKind, TagIdentifier,
|
||||
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, PlaneType, StatementKind,
|
||||
TagIdentifier,
|
||||
},
|
||||
fmt,
|
||||
modules::{ModuleId, ModulePath, ModuleRepr},
|
||||
@ -25,7 +24,7 @@ use crate::{
|
||||
},
|
||||
source_range::SourceRange,
|
||||
std::args::TyF64,
|
||||
CompilationError, NodePath,
|
||||
CompilationError,
|
||||
};
|
||||
|
||||
impl<'a> StatementKind<'a> {
|
||||
@ -84,7 +83,7 @@ impl ExecutorContext {
|
||||
preserve_mem: bool,
|
||||
module_id: ModuleId,
|
||||
path: &ModulePath,
|
||||
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>, ModuleArtifactState), KclError> {
|
||||
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError> {
|
||||
crate::log::log(format!("enter module {path} {}", exec_state.stack()));
|
||||
|
||||
let mut local_state = ModuleState::new(path.clone(), exec_state.stack().memory.clone(), Some(module_id));
|
||||
@ -109,16 +108,13 @@ impl ExecutorContext {
|
||||
} else {
|
||||
exec_state.mut_stack().pop_env()
|
||||
};
|
||||
let module_artifacts = if !preserve_mem {
|
||||
if !preserve_mem {
|
||||
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
|
||||
local_state.artifacts
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
crate::log::log(format!("leave {path}"));
|
||||
|
||||
result.map(|result| (result, env_ref, local_state.module_exports, module_artifacts))
|
||||
result.map(|result| (result, env_ref, local_state.module_exports))
|
||||
}
|
||||
|
||||
/// Execute an AST's program.
|
||||
@ -330,16 +326,6 @@ impl ExecutorContext {
|
||||
.mut_stack()
|
||||
.add(var_name.clone(), rhs.clone(), source_range)?;
|
||||
|
||||
if rhs.show_variable_in_feature_tree() {
|
||||
exec_state.push_op(Operation::VariableDeclaration {
|
||||
name: var_name.clone(),
|
||||
value: OpKclValue::from(&rhs),
|
||||
visibility: variable_declaration.visibility,
|
||||
node_path: NodePath::placeholder(),
|
||||
source_range,
|
||||
});
|
||||
}
|
||||
|
||||
// Track exports.
|
||||
if let ItemVisibility::Export = variable_declaration.visibility {
|
||||
if matches!(body_type, BodyType::Root) {
|
||||
@ -464,12 +450,12 @@ impl ExecutorContext {
|
||||
|
||||
if matches!(body_type, BodyType::Root) {
|
||||
// Flush the batch queue.
|
||||
exec_state
|
||||
self.engine
|
||||
.flush_batch(
|
||||
ModelingCmdMeta::new(self, SourceRange::new(program.end, program.end, program.module_id)),
|
||||
// True here tells the engine to flush all the end commands as well like fillets
|
||||
// and chamfers where the engine would otherwise eat the ID of the segments.
|
||||
true,
|
||||
SourceRange::new(program.end, program.end, program.module_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@ -549,12 +535,12 @@ impl ExecutorContext {
|
||||
|
||||
let result = match &mut repr {
|
||||
ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
|
||||
ModuleRepr::Kcl(_, Some((_, env_ref, items, _))) => Ok((*env_ref, items.clone())),
|
||||
ModuleRepr::Kcl(_, Some((_, env_ref, items))) => Ok((*env_ref, items.clone())),
|
||||
ModuleRepr::Kcl(program, cache) => self
|
||||
.exec_module_from_ast(program, module_id, &path, exec_state, source_range, false)
|
||||
.await
|
||||
.map(|(val, er, items, module_artifacts)| {
|
||||
*cache = Some((val, er, items.clone(), module_artifacts.clone()));
|
||||
.map(|(val, er, items)| {
|
||||
*cache = Some((val, er, items.clone()));
|
||||
(er, items)
|
||||
}),
|
||||
ModuleRepr::Foreign(geom, _) => Err(KclError::new_semantic(KclErrorDetails::new(
|
||||
@ -580,28 +566,28 @@ impl ExecutorContext {
|
||||
|
||||
let result = match &mut repr {
|
||||
ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
|
||||
ModuleRepr::Kcl(_, Some((val, _, _, _))) => Ok(val.clone()),
|
||||
ModuleRepr::Kcl(_, Some((val, _, _))) => Ok(val.clone()),
|
||||
ModuleRepr::Kcl(program, cached_items) => {
|
||||
let result = self
|
||||
.exec_module_from_ast(program, module_id, &path, exec_state, source_range, false)
|
||||
.await;
|
||||
match result {
|
||||
Ok((val, env, items, module_artifacts)) => {
|
||||
*cached_items = Some((val.clone(), env, items, module_artifacts));
|
||||
Ok((val, env, items)) => {
|
||||
*cached_items = Some((val.clone(), env, items));
|
||||
Ok(val)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
ModuleRepr::Foreign(_, Some((imported, _))) => Ok(imported.clone()),
|
||||
ModuleRepr::Foreign(_, Some(imported)) => Ok(Some(imported.clone())),
|
||||
ModuleRepr::Foreign(geom, cached) => {
|
||||
let result = super::import::send_to_engine(geom.clone(), exec_state, self)
|
||||
let result = super::import::send_to_engine(geom.clone(), self)
|
||||
.await
|
||||
.map(|geom| Some(KclValue::ImportedGeometry(geom)));
|
||||
|
||||
match result {
|
||||
Ok(val) => {
|
||||
*cached = Some((val.clone(), exec_state.mod_local.artifacts.clone()));
|
||||
*cached = val.clone();
|
||||
Ok(val)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
@ -623,7 +609,7 @@ impl ExecutorContext {
|
||||
exec_state: &mut ExecState,
|
||||
source_range: SourceRange,
|
||||
preserve_mem: bool,
|
||||
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>, ModuleArtifactState), KclError> {
|
||||
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError> {
|
||||
exec_state.global.mod_loader.enter_module(path);
|
||||
let result = self
|
||||
.exec_module_body(program, exec_state, preserve_mem, module_id, path)
|
||||
@ -812,10 +798,6 @@ fn apply_ascription(
|
||||
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into())
|
||||
.map_err(|e| KclError::new_semantic(e.into()))?;
|
||||
|
||||
if matches!(&ty, &RuntimeType::Primitive(PrimitiveType::Number(..))) {
|
||||
exec_state.clear_units_warnings(&source_range);
|
||||
}
|
||||
|
||||
value.coerce(&ty, false, exec_state).map_err(|_| {
|
||||
let suggestion = if ty == RuntimeType::length() {
|
||||
", you might try coercing to a fully specified numeric type such as `number(mm)`"
|
||||
@ -824,14 +806,9 @@ fn apply_ascription(
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let ty_str = if let Some(ty) = value.principal_type() {
|
||||
format!("(with type `{ty}`) ")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
KclError::new_semantic(KclErrorDetails::new(
|
||||
format!(
|
||||
"could not coerce {} {ty_str}to type `{ty}`{suggestion}",
|
||||
"could not coerce value of type {} to type {ty}{suggestion}",
|
||||
value.human_friendly_type()
|
||||
),
|
||||
vec![source_range],
|
||||
@ -1041,13 +1018,14 @@ impl Node<MemberExpression> {
|
||||
.map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
|
||||
.collect(),
|
||||
}),
|
||||
(being_indexed, _, _) => Err(KclError::new_semantic(KclErrorDetails::new(
|
||||
format!(
|
||||
"Only arrays can be indexed, but you're trying to index {}",
|
||||
being_indexed.human_friendly_type()
|
||||
),
|
||||
vec![self.clone().into()],
|
||||
))),
|
||||
(being_indexed, _, _) => {
|
||||
let t = being_indexed.human_friendly_type();
|
||||
let article = article_for(&t);
|
||||
Err(KclError::new_semantic(KclErrorDetails::new(
|
||||
format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
|
||||
vec![self.clone().into()],
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1175,7 +1153,7 @@ impl Node<BinaryExpression> {
|
||||
KclValue::Number { value: l / r, meta, ty }
|
||||
}
|
||||
BinaryOperator::Mod => {
|
||||
let (l, r, ty) = NumericType::combine_mod(left, right);
|
||||
let (l, r, ty) = NumericType::combine_div(left, right);
|
||||
self.warn_on_unknown(&ty, "Modulo of", exec_state);
|
||||
KclValue::Number { value: l % r, meta, ty }
|
||||
}
|
||||
@ -1222,14 +1200,11 @@ impl Node<BinaryExpression> {
|
||||
|
||||
fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) {
|
||||
if ty == &NumericType::Unknown {
|
||||
let sr = self.as_source_range();
|
||||
exec_state.clear_units_warnings(&sr);
|
||||
let mut err = CompilationError::err(
|
||||
sr,
|
||||
format!("{} numbers which have unknown or incompatible units.\nYou can probably fix this error by specifying the units using type ascription, e.g., `len: number(mm)` or `(a * b): number(deg)`.", verb),
|
||||
);
|
||||
err.tag = crate::errors::Tag::UnknownNumericUnits;
|
||||
exec_state.warn(err);
|
||||
// TODO suggest how to fix this
|
||||
exec_state.warn(CompilationError::err(
|
||||
self.as_source_range(),
|
||||
format!("{} numbers which have unknown or incompatible units.", verb),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1778,7 +1753,7 @@ a = 42: string
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("could not coerce a number (with type `number`) to type `string`"),
|
||||
.contains("could not coerce value of type number(default units) to type string"),
|
||||
"Expected error but found {err:?}"
|
||||
);
|
||||
|
||||
@ -1789,7 +1764,7 @@ a = 42: Plane
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("could not coerce a number (with type `number`) to type `Plane`"),
|
||||
.contains("could not coerce value of type number(default units) to type Plane"),
|
||||
"Expected error but found {err:?}"
|
||||
);
|
||||
|
||||
@ -1800,7 +1775,7 @@ arr = [0]: [string]
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains(
|
||||
"could not coerce an array of `number` with 1 value (with type `[any; 1]`) to type `[string]`"
|
||||
"could not coerce value of type array of number(default units) with 1 value to type [string]"
|
||||
),
|
||||
"Expected error but found {err:?}"
|
||||
);
|
||||
@ -1811,9 +1786,8 @@ mixedArr = [0, "a"]: [number(mm)]
|
||||
let result = parse_execute(program).await;
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains(
|
||||
"could not coerce an array of `number`, `string` (with type `[any; 2]`) to type `[number(mm)]`"
|
||||
),
|
||||
err.to_string()
|
||||
.contains("could not coerce value of type array of number(default units), string with 2 values to type [number(mm)]"),
|
||||
"Expected error but found {err:?}"
|
||||
);
|
||||
}
|
||||
@ -2118,19 +2092,4 @@ y = x: number(Length)"#;
|
||||
assert_eq!(num.n, 2.0);
|
||||
assert_eq!(num.ty, NumericType::mm());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn one_warning_unknown() {
|
||||
let ast = r#"
|
||||
// Should warn once
|
||||
a = PI * 2
|
||||
// Should warn once
|
||||
b = (PI * 2) / 3
|
||||
// Should not warn
|
||||
c = ((PI * 2) / 3): number(deg)
|
||||
"#;
|
||||
|
||||
let result = parse_execute(ast).await.unwrap();
|
||||
assert_eq!(result.exec_state.errors().len(), 2);
|
||||
}
|
||||
}
|
||||
|
@ -532,44 +532,6 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn type_err_str(expected: &Type, found: &KclValue, source_range: &SourceRange, exec_state: &mut ExecState) -> String {
|
||||
fn strip_backticks(s: &str) -> &str {
|
||||
let mut result = s;
|
||||
if s.starts_with('`') {
|
||||
result = &result[1..]
|
||||
}
|
||||
if s.ends_with('`') {
|
||||
result = &result[..result.len() - 1]
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
let expected_human = expected.human_friendly_type();
|
||||
let expected_ty = expected.to_string();
|
||||
let expected_str =
|
||||
if expected_human == expected_ty || expected_human == format!("a value with type `{expected_ty}`") {
|
||||
format!("a value with type `{expected_ty}`")
|
||||
} else {
|
||||
format!("{expected_human} (`{expected_ty}`)")
|
||||
};
|
||||
let found_human = found.human_friendly_type();
|
||||
let found_ty = found.principal_type_string();
|
||||
let found_str = if found_human == found_ty || found_human == format!("a {}", strip_backticks(&found_ty)) {
|
||||
format!("a value with type {}", found_ty)
|
||||
} else {
|
||||
format!("{found_human} (with type {})", found_ty)
|
||||
};
|
||||
|
||||
let mut result = format!("{expected_str}, but found {found_str}.");
|
||||
|
||||
if found.is_unknown_number() {
|
||||
exec_state.clear_units_warnings(source_range);
|
||||
result.push_str("\nThe found value is a number but has incomplete units information. You can probably fix this error by specifying the units using type ascription, e.g., `len: number(mm)` or `(a * b): number(deg)`.");
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn type_check_params_kw(
|
||||
fn_name: Option<&str>,
|
||||
fn_def: &FunctionDefinition<'_>,
|
||||
@ -594,19 +556,18 @@ fn type_check_params_kw(
|
||||
// For optional args, passing None should be the same as not passing an arg.
|
||||
if !(def.is_some() && matches!(arg.value, KclValue::KclNone { .. })) {
|
||||
if let Some(ty) = ty {
|
||||
let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range)
|
||||
.map_err(|e| KclError::new_semantic(e.into()))?;
|
||||
arg.value = arg
|
||||
.value
|
||||
.coerce(
|
||||
&rty,
|
||||
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range).map_err(|e| KclError::new_semantic(e.into()))?,
|
||||
true,
|
||||
exec_state,
|
||||
)
|
||||
.map_err(|e| {
|
||||
let mut message = format!(
|
||||
"{label} requires {}",
|
||||
type_err_str(ty, &arg.value, &arg.source_range, exec_state),
|
||||
"{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.
|
||||
@ -669,20 +630,28 @@ fn type_check_params_kw(
|
||||
|
||||
if let Some(arg) = &mut args.unlabeled {
|
||||
if let Some((_, Some(ty))) = &fn_def.input_arg {
|
||||
let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
|
||||
.map_err(|e| KclError::new_semantic(e.into()))?;
|
||||
arg.1.value = arg.1.value.coerce(&rty, true, exec_state).map_err(|_| {
|
||||
KclError::new_semantic(KclErrorDetails::new(
|
||||
format!(
|
||||
"The input argument of {} requires {}",
|
||||
fn_name
|
||||
.map(|n| format!("`{}`", n))
|
||||
.unwrap_or_else(|| "this function".to_owned()),
|
||||
type_err_str(ty, &arg.1.value, &arg.1.source_range, exec_state),
|
||||
),
|
||||
vec![arg.1.source_range],
|
||||
))
|
||||
})?;
|
||||
arg.1.value = arg
|
||||
.1
|
||||
.value
|
||||
.coerce(
|
||||
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
|
||||
.map_err(|e| KclError::new_semantic(e.into()))?,
|
||||
true,
|
||||
exec_state,
|
||||
)
|
||||
.map_err(|_| {
|
||||
KclError::new_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) {
|
||||
@ -778,8 +747,9 @@ fn coerce_result_type(
|
||||
let val = val.coerce(&ty, true, exec_state).map_err(|_| {
|
||||
KclError::new_semantic(KclErrorDetails::new(
|
||||
format!(
|
||||
"This function requires its result to be {}",
|
||||
type_err_str(ret_ty, &val, &(&val).into(), exec_state)
|
||||
"This function requires its result to be of type `{}`, but found {}",
|
||||
ty.human_friendly_type(),
|
||||
val.human_friendly_type(),
|
||||
),
|
||||
ret_ty.as_source_ranges(),
|
||||
))
|
||||
@ -958,7 +928,7 @@ 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 a value with type `number`.\nThe found value is a number but has incomplete units information. You can probably fix this error by specifying the units using type ascription, e.g., `len: number(mm)` or `(a * b): number(deg)`."
|
||||
"prefix requires a value with type `string`, but found number(default units)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -15,10 +15,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
execution::{
|
||||
annotations, typed_path::TypedPath, types::UnitLen, ExecState, ExecutorContext, ImportedGeometry,
|
||||
ModelingCmdMeta,
|
||||
},
|
||||
execution::{annotations, typed_path::TypedPath, types::UnitLen, ExecState, ExecutorContext, ImportedGeometry},
|
||||
fs::FileSystem,
|
||||
parsing::ast::types::{Annotation, Node},
|
||||
source_range::SourceRange,
|
||||
@ -260,22 +257,15 @@ pub struct PreImportedGeometry {
|
||||
pub source_range: SourceRange,
|
||||
}
|
||||
|
||||
pub async fn send_to_engine(
|
||||
pre: PreImportedGeometry,
|
||||
exec_state: &mut ExecState,
|
||||
ctxt: &ExecutorContext,
|
||||
) -> Result<ImportedGeometry, KclError> {
|
||||
pub async fn send_to_engine(pre: PreImportedGeometry, ctxt: &ExecutorContext) -> Result<ImportedGeometry, KclError> {
|
||||
let imported_geometry = ImportedGeometry::new(
|
||||
pre.id,
|
||||
pre.command.files.iter().map(|f| f.path.to_string()).collect(),
|
||||
vec![pre.source_range.into()],
|
||||
);
|
||||
|
||||
exec_state
|
||||
.async_modeling_cmd(
|
||||
ModelingCmdMeta::with_id(ctxt, pre.source_range, pre.id),
|
||||
&ModelingCmd::from(pre.command.clone()),
|
||||
)
|
||||
ctxt.engine
|
||||
.async_modeling_cmd(pre.id, pre.source_range, &ModelingCmd::from(pre.command.clone()))
|
||||
.await?;
|
||||
|
||||
Ok(imported_geometry)
|
||||
|
@ -4,6 +4,7 @@ use anyhow::Result;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::types::UnitType;
|
||||
use crate::{
|
||||
errors::KclErrorDetails,
|
||||
execution::{
|
||||
@ -277,85 +278,72 @@ impl KclValue {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if we should generate an [`crate::execution::Operation`] to
|
||||
/// display in the Feature Tree for variable declarations initialized with
|
||||
/// this value.
|
||||
pub(crate) fn show_variable_in_feature_tree(&self) -> bool {
|
||||
match self {
|
||||
KclValue::Uuid { .. } => false,
|
||||
KclValue::Bool { .. } | KclValue::Number { .. } | KclValue::String { .. } => true,
|
||||
KclValue::Tuple { .. }
|
||||
| KclValue::HomArray { .. }
|
||||
| KclValue::Object { .. }
|
||||
| KclValue::TagIdentifier(_)
|
||||
| KclValue::TagDeclarator(_)
|
||||
| KclValue::Plane { .. }
|
||||
| KclValue::Face { .. }
|
||||
| KclValue::Sketch { .. }
|
||||
| KclValue::Solid { .. }
|
||||
| KclValue::Helix { .. }
|
||||
| KclValue::ImportedGeometry(_)
|
||||
| KclValue::Function { .. }
|
||||
| KclValue::Module { .. }
|
||||
| KclValue::Type { .. }
|
||||
| KclValue::KclNone { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Human readable type name used in error messages. Should not be relied
|
||||
/// on for program logic.
|
||||
pub(crate) fn human_friendly_type(&self) -> String {
|
||||
self.inner_human_friendly_type(1)
|
||||
}
|
||||
|
||||
fn inner_human_friendly_type(&self, max_depth: usize) -> String {
|
||||
if let Some(pt) = self.principal_type() {
|
||||
if max_depth > 0 {
|
||||
// The principal type of an array uses the array's element type,
|
||||
// which is oftentimes `any`, and that's not a helpful message. So
|
||||
// we show the actual elements.
|
||||
if let KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } = self {
|
||||
// If it's empty, we want to show the type of the array.
|
||||
if !value.is_empty() {
|
||||
// A max of 3 is good because it's common to use 3D points.
|
||||
let max = 3;
|
||||
let len = value.len();
|
||||
let ellipsis = if len > max { ", ..." } else { "" };
|
||||
let element_label = if len == 1 { "value" } else { "values" };
|
||||
let element_tys = value
|
||||
.iter()
|
||||
.take(max)
|
||||
.map(|elem| elem.inner_human_friendly_type(max_depth - 1))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
return format!("array of {element_tys}{ellipsis} with {len} {element_label}");
|
||||
}
|
||||
}
|
||||
}
|
||||
return pt.to_string();
|
||||
}
|
||||
match self {
|
||||
KclValue::Uuid { .. } => "a unique ID (uuid)".to_owned(),
|
||||
KclValue::TagDeclarator(_) => "a tag declarator".to_owned(),
|
||||
KclValue::TagIdentifier(_) => "a tag identifier".to_owned(),
|
||||
KclValue::Solid { .. } => "a solid".to_owned(),
|
||||
KclValue::Sketch { .. } => "a sketch".to_owned(),
|
||||
KclValue::Helix { .. } => "a helix".to_owned(),
|
||||
KclValue::ImportedGeometry(_) => "an imported geometry".to_owned(),
|
||||
KclValue::Function { .. } => "a function".to_owned(),
|
||||
KclValue::Plane { .. } => "a plane".to_owned(),
|
||||
KclValue::Face { .. } => "a face".to_owned(),
|
||||
KclValue::Bool { .. } => "a boolean (`true` or `false`)".to_owned(),
|
||||
KclValue::Uuid { .. } => "Unique ID (uuid)",
|
||||
KclValue::TagDeclarator(_) => "TagDeclarator",
|
||||
KclValue::TagIdentifier(_) => "TagIdentifier",
|
||||
KclValue::Solid { .. } => "Solid",
|
||||
KclValue::Sketch { .. } => "Sketch",
|
||||
KclValue::Helix { .. } => "Helix",
|
||||
KclValue::ImportedGeometry(_) => "ImportedGeometry",
|
||||
KclValue::Function { .. } => "Function",
|
||||
KclValue::Plane { .. } => "Plane",
|
||||
KclValue::Face { .. } => "Face",
|
||||
KclValue::Bool { .. } => "boolean (true/false value)",
|
||||
KclValue::Number {
|
||||
ty: NumericType::Unknown,
|
||||
..
|
||||
} => "a number with unknown units".to_owned(),
|
||||
} => "number(unknown units)",
|
||||
KclValue::Number {
|
||||
ty: NumericType::Known(units),
|
||||
ty: NumericType::Known(UnitType::Length(_)),
|
||||
..
|
||||
} => format!("a number ({units})"),
|
||||
KclValue::Number { .. } => "a number".to_owned(),
|
||||
KclValue::String { .. } => "a string".to_owned(),
|
||||
KclValue::Object { .. } => "an object".to_owned(),
|
||||
KclValue::Module { .. } => "a module".to_owned(),
|
||||
KclValue::Type { .. } => "a type".to_owned(),
|
||||
KclValue::KclNone { .. } => "none".to_owned(),
|
||||
KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
|
||||
if value.is_empty() {
|
||||
"an empty array".to_owned()
|
||||
} else {
|
||||
// A max of 3 is good because it's common to use 3D points.
|
||||
const MAX: usize = 3;
|
||||
|
||||
let len = value.len();
|
||||
let element_tys = value
|
||||
.iter()
|
||||
.take(MAX)
|
||||
.map(|elem| elem.principal_type_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let mut result = format!("an array of {element_tys}");
|
||||
if len > MAX {
|
||||
result.push_str(&format!(", ... with {len} values"));
|
||||
}
|
||||
if len == 1 {
|
||||
result.push_str(" with 1 value");
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
} => "number(Length)",
|
||||
KclValue::Number {
|
||||
ty: NumericType::Known(UnitType::Angle(_)),
|
||||
..
|
||||
} => "number(Angle)",
|
||||
KclValue::Number { .. } => "number",
|
||||
KclValue::String { .. } => "string (text)",
|
||||
KclValue::Tuple { .. } => "tuple (list)",
|
||||
KclValue::HomArray { .. } => "array (list)",
|
||||
KclValue::Object { .. } => "object",
|
||||
KclValue::Module { .. } => "module",
|
||||
KclValue::Type { .. } => "type",
|
||||
KclValue::KclNone { .. } => "None",
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub(crate) fn from_literal(literal: Node<Literal>, exec_state: &mut ExecState) -> Self {
|
||||
@ -614,13 +602,6 @@ impl KclValue {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_unknown_number(&self) -> bool {
|
||||
match self {
|
||||
KclValue::Number { ty, .. } => !ty.is_fully_specified(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value_str(&self) -> Option<String> {
|
||||
match self {
|
||||
KclValue::Bool { value, .. } => Some(format!("{value}")),
|
||||
@ -668,8 +649,6 @@ impl From<GeometryWithImportedGeometry> for KclValue {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::exec::UnitType;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@ -679,21 +658,21 @@ mod tests {
|
||||
ty: NumericType::Known(UnitType::Length(UnitLen::Unknown)),
|
||||
meta: vec![],
|
||||
};
|
||||
assert_eq!(len.human_friendly_type(), "a number (Length)".to_string());
|
||||
assert_eq!(len.human_friendly_type(), "number(Length)".to_string());
|
||||
|
||||
let unknown = KclValue::Number {
|
||||
value: 1.0,
|
||||
ty: NumericType::Unknown,
|
||||
meta: vec![],
|
||||
};
|
||||
assert_eq!(unknown.human_friendly_type(), "a number with unknown units".to_string());
|
||||
assert_eq!(unknown.human_friendly_type(), "number(unknown units)".to_string());
|
||||
|
||||
let mm = KclValue::Number {
|
||||
value: 1.0,
|
||||
ty: NumericType::Known(UnitType::Length(UnitLen::Mm)),
|
||||
meta: vec![],
|
||||
};
|
||||
assert_eq!(mm.human_friendly_type(), "a number (mm)".to_string());
|
||||
assert_eq!(mm.human_friendly_type(), "number(mm)".to_string());
|
||||
|
||||
let array1_mm = KclValue::HomArray {
|
||||
value: vec![mm.clone()],
|
||||
@ -701,7 +680,7 @@ mod tests {
|
||||
};
|
||||
assert_eq!(
|
||||
array1_mm.human_friendly_type(),
|
||||
"an array of `number(mm)` with 1 value".to_string()
|
||||
"array of number(mm) with 1 value".to_string()
|
||||
);
|
||||
|
||||
let array2_mm = KclValue::HomArray {
|
||||
@ -710,7 +689,7 @@ mod tests {
|
||||
};
|
||||
assert_eq!(
|
||||
array2_mm.human_friendly_type(),
|
||||
"an array of `number(mm)`, `number(mm)`".to_string()
|
||||
"array of number(mm), number(mm) with 2 values".to_string()
|
||||
);
|
||||
|
||||
let array3_mm = KclValue::HomArray {
|
||||
@ -719,7 +698,7 @@ mod tests {
|
||||
};
|
||||
assert_eq!(
|
||||
array3_mm.human_friendly_type(),
|
||||
"an array of `number(mm)`, `number(mm)`, `number(mm)`".to_string()
|
||||
"array of number(mm), number(mm), number(mm) with 3 values".to_string()
|
||||
);
|
||||
|
||||
let inches = KclValue::Number {
|
||||
@ -733,14 +712,14 @@ mod tests {
|
||||
};
|
||||
assert_eq!(
|
||||
array4.human_friendly_type(),
|
||||
"an array of `number(mm)`, `number(mm)`, `number(in)`, ... with 4 values".to_string()
|
||||
"array of number(mm), number(mm), number(in), ... with 4 values".to_string()
|
||||
);
|
||||
|
||||
let empty_array = KclValue::HomArray {
|
||||
value: vec![],
|
||||
ty: RuntimeType::any(),
|
||||
};
|
||||
assert_eq!(empty_array.human_friendly_type(), "an empty array".to_string());
|
||||
assert_eq!(empty_array.human_friendly_type(), "[any; 0]".to_string());
|
||||
|
||||
let array_nested = KclValue::HomArray {
|
||||
value: vec![array2_mm.clone()],
|
||||
@ -748,7 +727,7 @@ mod tests {
|
||||
};
|
||||
assert_eq!(
|
||||
array_nested.human_friendly_type(),
|
||||
"an array of `[any; 2]` with 1 value".to_string()
|
||||
"array of [any; 2] with 1 value".to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,7 @@ pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketc
|
||||
use cache::GlobalState;
|
||||
pub use cache::{bust_cache, clear_mem_cache};
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
pub use cad_op::Group;
|
||||
pub use cad_op::Operation;
|
||||
pub use cad_op::{Group, Operation};
|
||||
pub use geometry::*;
|
||||
pub use id_generator::IdGenerator;
|
||||
pub(crate) use import::PreImportedGeometry;
|
||||
@ -23,10 +22,8 @@ use kcmc::{
|
||||
};
|
||||
use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId};
|
||||
pub use memory::EnvironmentRef;
|
||||
pub(crate) use modeling::ModelingCmdMeta;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub(crate) use state::ModuleArtifactState;
|
||||
pub use state::{ExecState, MetaSettings};
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -59,7 +56,6 @@ mod import;
|
||||
mod import_graph;
|
||||
pub(crate) mod kcl_value;
|
||||
mod memory;
|
||||
mod modeling;
|
||||
mod state;
|
||||
pub mod typed_path;
|
||||
pub(crate) mod types;
|
||||
@ -80,6 +76,9 @@ pub struct ExecOutcome {
|
||||
/// the Feature Tree.
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
pub operations: Vec<Operation>,
|
||||
/// Output commands to allow building the artifact graph by the caller.
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
pub artifact_commands: Vec<ArtifactCommand>,
|
||||
/// Output artifact graph.
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
pub artifact_graph: ArtifactGraph,
|
||||
@ -576,7 +575,7 @@ impl ExecutorContext {
|
||||
|
||||
let mut mem = exec_state.stack().clone();
|
||||
let module_infos = exec_state.global.module_infos.clone();
|
||||
let outcome = exec_state.into_mock_exec_outcome(result.0, self).await;
|
||||
let outcome = exec_state.to_mock_exec_outcome(result.0, self).await;
|
||||
|
||||
mem.squash_env(result.0);
|
||||
cache::write_old_memory((mem, module_infos)).await;
|
||||
@ -774,12 +773,15 @@ impl ExecutorContext {
|
||||
))
|
||||
.await;
|
||||
|
||||
let outcome = exec_state.into_exec_outcome(result.0, self).await;
|
||||
let outcome = exec_state.to_exec_outcome(result.0, self).await;
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
/// Perform the execution of a program.
|
||||
///
|
||||
/// You can optionally pass in some initialization memory for partial
|
||||
/// execution.
|
||||
///
|
||||
/// To access non-fatal errors and warnings, extract them from the `ExecState`.
|
||||
pub async fn run(
|
||||
&self,
|
||||
@ -792,6 +794,9 @@ impl ExecutorContext {
|
||||
/// Perform the execution of a program using a concurrent
|
||||
/// execution model.
|
||||
///
|
||||
/// You can optionally pass in some initialization memory for partial
|
||||
/// execution.
|
||||
///
|
||||
/// To access non-fatal errors and warnings, extract them from the `ExecState`.
|
||||
pub async fn run_concurrent(
|
||||
&self,
|
||||
@ -837,8 +842,6 @@ impl ExecutorContext {
|
||||
let module_id = *module_id;
|
||||
let module_path = module_path.clone();
|
||||
let source_range = SourceRange::from(import_stmt);
|
||||
// Clone before mutating.
|
||||
let module_exec_state = exec_state.clone();
|
||||
|
||||
self.add_import_module_ops(
|
||||
exec_state,
|
||||
@ -850,6 +853,7 @@ impl ExecutorContext {
|
||||
);
|
||||
|
||||
let repr = repr.clone();
|
||||
let exec_state = exec_state.clone();
|
||||
let exec_ctxt = self.clone();
|
||||
let results_tx = results_tx.clone();
|
||||
|
||||
@ -869,13 +873,11 @@ impl ExecutorContext {
|
||||
result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
|
||||
}
|
||||
ModuleRepr::Foreign(geom, _) => {
|
||||
let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
|
||||
let result = crate::execution::import::send_to_engine(geom.clone(), exec_ctxt)
|
||||
.await
|
||||
.map(|geom| Some(KclValue::ImportedGeometry(geom)));
|
||||
|
||||
result.map(|val| {
|
||||
ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
|
||||
})
|
||||
result.map(|val| ModuleRepr::Foreign(geom.clone(), val))
|
||||
}
|
||||
ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
|
||||
format!("Module {module_path} not found in universe"),
|
||||
@ -887,7 +889,7 @@ impl ExecutorContext {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let mut exec_state = module_exec_state;
|
||||
let mut exec_state = exec_state;
|
||||
let exec_ctxt = exec_ctxt;
|
||||
|
||||
let result = exec_module(
|
||||
@ -909,7 +911,7 @@ impl ExecutorContext {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
set.spawn(async move {
|
||||
let mut exec_state = module_exec_state;
|
||||
let mut exec_state = exec_state;
|
||||
let exec_ctxt = exec_ctxt;
|
||||
|
||||
let result = exec_module(
|
||||
@ -962,15 +964,6 @@ impl ExecutorContext {
|
||||
}
|
||||
}
|
||||
|
||||
// Since we haven't technically started executing the root module yet,
|
||||
// the operations corresponding to the imports will be missing unless we
|
||||
// track them here.
|
||||
#[cfg(all(test, feature = "artifact-graph"))]
|
||||
exec_state
|
||||
.global
|
||||
.root_module_artifacts
|
||||
.extend(exec_state.mod_local.artifacts.clone());
|
||||
|
||||
self.inner_run(program, exec_state, preserve_mem).await
|
||||
}
|
||||
|
||||
@ -1000,18 +993,6 @@ impl ExecutorContext {
|
||||
Ok((universe, root_imports))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "artifact-graph"))]
|
||||
fn add_import_module_ops(
|
||||
&self,
|
||||
_exec_state: &mut ExecState,
|
||||
_program: &crate::Program,
|
||||
_module_id: ModuleId,
|
||||
_module_path: &ModulePath,
|
||||
_source_range: SourceRange,
|
||||
_universe_map: &UniverseMap,
|
||||
) {
|
||||
}
|
||||
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
fn add_import_module_ops(
|
||||
&self,
|
||||
@ -1061,6 +1042,18 @@ impl ExecutorContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "artifact-graph"))]
|
||||
fn add_import_module_ops(
|
||||
&self,
|
||||
_exec_state: &mut ExecState,
|
||||
_program: &crate::Program,
|
||||
_module_id: ModuleId,
|
||||
_module_path: &ModulePath,
|
||||
_source_range: SourceRange,
|
||||
_universe_map: &UniverseMap,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Perform the execution of a program. Accept all possible parameters and
|
||||
/// output everything.
|
||||
async fn inner_run(
|
||||
@ -1128,32 +1121,26 @@ impl ExecutorContext {
|
||||
&ModulePath::Main,
|
||||
)
|
||||
.await;
|
||||
#[cfg(all(test, feature = "artifact-graph"))]
|
||||
let exec_result = exec_result.map(|(_, env_ref, _, module_artifacts)| {
|
||||
exec_state.global.root_module_artifacts.extend(module_artifacts);
|
||||
env_ref
|
||||
});
|
||||
#[cfg(not(all(test, feature = "artifact-graph")))]
|
||||
let exec_result = exec_result.map(|(_, env_ref, _, _)| env_ref);
|
||||
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
{
|
||||
// Fill in NodePath for operations.
|
||||
let cached_body_items = exec_state.global.artifacts.cached_body_items();
|
||||
for op in exec_state.global.artifacts.operations.iter_mut().skip(start_op) {
|
||||
op.fill_node_paths(program, cached_body_items);
|
||||
}
|
||||
#[cfg(test)]
|
||||
{
|
||||
for op in exec_state.global.root_module_artifacts.operations.iter_mut() {
|
||||
op.fill_node_paths(program, cached_body_items);
|
||||
}
|
||||
for module in exec_state.global.module_infos.values_mut() {
|
||||
if let ModuleRepr::Kcl(_, Some((_, _, _, module_artifacts))) = &mut module.repr {
|
||||
for op in &mut module_artifacts.operations {
|
||||
op.fill_node_paths(program, cached_body_items);
|
||||
}
|
||||
match op {
|
||||
Operation::StdLibCall {
|
||||
node_path,
|
||||
source_range,
|
||||
..
|
||||
}
|
||||
| Operation::GroupBegin {
|
||||
node_path,
|
||||
source_range,
|
||||
..
|
||||
} => {
|
||||
node_path.fill_placeholder(program, cached_body_items, *source_range);
|
||||
}
|
||||
Operation::GroupEnd => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1166,7 +1153,7 @@ impl ExecutorContext {
|
||||
self.engine.clear_queues().await;
|
||||
|
||||
match exec_state.build_artifact_graph(&self.engine, program).await {
|
||||
Ok(_) => exec_result,
|
||||
Ok(_) => exec_result.map(|(_, env_ref, _)| env_ref),
|
||||
Err(err) => exec_result.and(Err(err)),
|
||||
}
|
||||
}
|
||||
@ -1176,9 +1163,6 @@ impl ExecutorContext {
|
||||
/// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
|
||||
async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
|
||||
if exec_state.stack().memory.requires_std() {
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
let initial_ops = exec_state.global.artifacts.operations.len();
|
||||
|
||||
let path = vec!["std".to_owned(), "prelude".to_owned()];
|
||||
let resolved_path = ModulePath::from_std_import_path(&path)?;
|
||||
let id = self
|
||||
@ -1187,14 +1171,6 @@ impl ExecutorContext {
|
||||
let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
|
||||
|
||||
exec_state.mut_stack().memory.set_std(module_memory);
|
||||
|
||||
// Operations generated by the prelude are not useful, so clear them
|
||||
// out.
|
||||
//
|
||||
// TODO: Should we also clear them out of each module so that they
|
||||
// don't appear in test output?
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
exec_state.global.artifacts.operations.truncate(initial_ops);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -1943,13 +1919,13 @@ notNull = !myNull
|
||||
"#;
|
||||
assert_eq!(
|
||||
parse_execute(code1).await.unwrap_err().message(),
|
||||
"Cannot apply unary operator ! to non-boolean value: a number",
|
||||
"Cannot apply unary operator ! to non-boolean value: number(default units)",
|
||||
);
|
||||
|
||||
let code2 = "notZero = !0";
|
||||
assert_eq!(
|
||||
parse_execute(code2).await.unwrap_err().message(),
|
||||
"Cannot apply unary operator ! to non-boolean value: a number",
|
||||
"Cannot apply unary operator ! to non-boolean value: number(default units)",
|
||||
);
|
||||
|
||||
let code3 = r#"
|
||||
@ -1957,7 +1933,7 @@ notEmptyString = !""
|
||||
"#;
|
||||
assert_eq!(
|
||||
parse_execute(code3).await.unwrap_err().message(),
|
||||
"Cannot apply unary operator ! to non-boolean value: a string",
|
||||
"Cannot apply unary operator ! to non-boolean value: string",
|
||||
);
|
||||
|
||||
let code4 = r#"
|
||||
@ -1966,7 +1942,7 @@ notMember = !obj.a
|
||||
"#;
|
||||
assert_eq!(
|
||||
parse_execute(code4).await.unwrap_err().message(),
|
||||
"Cannot apply unary operator ! to non-boolean value: a number",
|
||||
"Cannot apply unary operator ! to non-boolean value: number(default units)",
|
||||
);
|
||||
|
||||
let code5 = "
|
||||
@ -1974,7 +1950,7 @@ a = []
|
||||
notArray = !a";
|
||||
assert_eq!(
|
||||
parse_execute(code5).await.unwrap_err().message(),
|
||||
"Cannot apply unary operator ! to non-boolean value: an empty array",
|
||||
"Cannot apply unary operator ! to non-boolean value: [any; 0]",
|
||||
);
|
||||
|
||||
let code6 = "
|
||||
@ -1982,7 +1958,7 @@ x = {}
|
||||
notObject = !x";
|
||||
assert_eq!(
|
||||
parse_execute(code6).await.unwrap_err().message(),
|
||||
"Cannot apply unary operator ! to non-boolean value: an object",
|
||||
"Cannot apply unary operator ! to non-boolean value: { }",
|
||||
);
|
||||
|
||||
let code7 = "
|
||||
@ -2008,7 +1984,7 @@ notTagDeclarator = !myTagDeclarator";
|
||||
assert!(
|
||||
tag_declarator_err
|
||||
.message()
|
||||
.starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
|
||||
.starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
|
||||
"Actual error: {:?}",
|
||||
tag_declarator_err
|
||||
);
|
||||
@ -2022,7 +1998,7 @@ notTagIdentifier = !myTag";
|
||||
assert!(
|
||||
tag_identifier_err
|
||||
.message()
|
||||
.starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
|
||||
.starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
|
||||
"Actual error: {:?}",
|
||||
tag_identifier_err
|
||||
);
|
||||
|
@ -1,224 +0,0 @@
|
||||
use kcmc::ModelingCmd;
|
||||
use kittycad_modeling_cmds::{
|
||||
self as kcmc,
|
||||
websocket::{ModelingCmdReq, OkWebSocketResponseData},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
use crate::exec::ArtifactCommand;
|
||||
use crate::{
|
||||
exec::{IdGenerator, KclValue},
|
||||
execution::Solid,
|
||||
std::Args,
|
||||
ExecState, ExecutorContext, KclError, SourceRange,
|
||||
};
|
||||
|
||||
/// Context and metadata needed to send a single modeling command.
|
||||
///
|
||||
/// Many functions consume Self so that the command ID isn't accidentally reused
|
||||
/// among multiple modeling commands.
|
||||
pub(crate) struct ModelingCmdMeta<'a> {
|
||||
/// The executor context, which contains the engine.
|
||||
pub ctx: &'a ExecutorContext,
|
||||
/// The source range of the command, used for error reporting.
|
||||
pub source_range: SourceRange,
|
||||
/// The id of the command, if it has been set by the caller or generated.
|
||||
id: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl<'a> ModelingCmdMeta<'a> {
|
||||
pub fn new(ctx: &'a ExecutorContext, source_range: SourceRange) -> Self {
|
||||
ModelingCmdMeta {
|
||||
ctx,
|
||||
source_range,
|
||||
id: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_id(ctx: &'a ExecutorContext, source_range: SourceRange, id: Uuid) -> Self {
|
||||
ModelingCmdMeta {
|
||||
ctx,
|
||||
source_range,
|
||||
id: Some(id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_args_id(args: &'a Args, id: Uuid) -> Self {
|
||||
ModelingCmdMeta {
|
||||
ctx: &args.ctx,
|
||||
source_range: args.source_range,
|
||||
id: Some(id),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&mut self, id_generator: &mut IdGenerator) -> Uuid {
|
||||
if let Some(id) = self.id {
|
||||
return id;
|
||||
}
|
||||
let id = id_generator.next_uuid();
|
||||
self.id = Some(id);
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Args> for ModelingCmdMeta<'a> {
|
||||
fn from(args: &'a Args) -> Self {
|
||||
ModelingCmdMeta::new(&args.ctx, args.source_range)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecState {
|
||||
/// Add a modeling command to the batch but don't fire it right away.
|
||||
pub(crate) async fn batch_modeling_cmd(
|
||||
&mut self,
|
||||
mut meta: ModelingCmdMeta<'_>,
|
||||
cmd: ModelingCmd,
|
||||
) -> Result<(), crate::errors::KclError> {
|
||||
let id = meta.id(self.id_generator());
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
self.push_command(ArtifactCommand {
|
||||
cmd_id: id,
|
||||
range: meta.source_range,
|
||||
command: cmd.clone(),
|
||||
});
|
||||
meta.ctx.engine.batch_modeling_cmd(id, meta.source_range, &cmd).await
|
||||
}
|
||||
|
||||
/// Add multiple modeling commands to the batch but don't fire them right
|
||||
/// away.
|
||||
pub(crate) async fn batch_modeling_cmds(
|
||||
&mut self,
|
||||
meta: ModelingCmdMeta<'_>,
|
||||
cmds: &[ModelingCmdReq],
|
||||
) -> Result<(), crate::errors::KclError> {
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
for cmd_req in cmds {
|
||||
self.push_command(ArtifactCommand {
|
||||
cmd_id: *cmd_req.cmd_id.as_ref(),
|
||||
range: meta.source_range,
|
||||
command: cmd_req.cmd.clone(),
|
||||
});
|
||||
}
|
||||
meta.ctx.engine.batch_modeling_cmds(meta.source_range, cmds).await
|
||||
}
|
||||
|
||||
/// Add a modeling command to the batch that gets executed at the end of the
|
||||
/// file. This is good for something like fillet or chamfer where the engine
|
||||
/// would eat the path id if we executed it right away.
|
||||
pub(crate) async fn batch_end_cmd(
|
||||
&mut self,
|
||||
mut meta: ModelingCmdMeta<'_>,
|
||||
cmd: ModelingCmd,
|
||||
) -> Result<(), crate::errors::KclError> {
|
||||
let id = meta.id(self.id_generator());
|
||||
// TODO: The order of the tracking of these doesn't match the order that
|
||||
// they're sent to the engine.
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
self.push_command(ArtifactCommand {
|
||||
cmd_id: id,
|
||||
range: meta.source_range,
|
||||
command: cmd.clone(),
|
||||
});
|
||||
meta.ctx.engine.batch_end_cmd(id, meta.source_range, &cmd).await
|
||||
}
|
||||
|
||||
/// Send the modeling cmd and wait for the response.
|
||||
pub(crate) async fn send_modeling_cmd(
|
||||
&mut self,
|
||||
mut meta: ModelingCmdMeta<'_>,
|
||||
cmd: ModelingCmd,
|
||||
) -> Result<OkWebSocketResponseData, KclError> {
|
||||
let id = meta.id(self.id_generator());
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
self.push_command(ArtifactCommand {
|
||||
cmd_id: id,
|
||||
range: meta.source_range,
|
||||
command: cmd.clone(),
|
||||
});
|
||||
meta.ctx.engine.send_modeling_cmd(id, meta.source_range, &cmd).await
|
||||
}
|
||||
|
||||
/// Send the modeling cmd async and don't wait for the response.
|
||||
/// Add it to our list of async commands.
|
||||
pub(crate) async fn async_modeling_cmd(
|
||||
&mut self,
|
||||
mut meta: ModelingCmdMeta<'_>,
|
||||
cmd: &ModelingCmd,
|
||||
) -> Result<(), crate::errors::KclError> {
|
||||
let id = meta.id(self.id_generator());
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
self.push_command(ArtifactCommand {
|
||||
cmd_id: id,
|
||||
range: meta.source_range,
|
||||
command: cmd.clone(),
|
||||
});
|
||||
meta.ctx.engine.async_modeling_cmd(id, meta.source_range, cmd).await
|
||||
}
|
||||
|
||||
/// Force flush the batch queue.
|
||||
pub(crate) async fn flush_batch(
|
||||
&mut self,
|
||||
meta: ModelingCmdMeta<'_>,
|
||||
// Whether or not to flush the end commands as well.
|
||||
// We only do this at the very end of the file.
|
||||
batch_end: bool,
|
||||
) -> Result<OkWebSocketResponseData, KclError> {
|
||||
meta.ctx.engine.flush_batch(batch_end, meta.source_range).await
|
||||
}
|
||||
|
||||
/// Flush just the fillets and chamfers for this specific SolidSet.
|
||||
pub(crate) async fn flush_batch_for_solids(
|
||||
&mut self,
|
||||
meta: ModelingCmdMeta<'_>,
|
||||
solids: &[Solid],
|
||||
) -> Result<(), KclError> {
|
||||
// Make sure we don't traverse sketches more than once.
|
||||
let mut traversed_sketches = Vec::new();
|
||||
|
||||
// Collect all the fillet/chamfer ids for the solids.
|
||||
let mut ids = Vec::new();
|
||||
for solid in solids {
|
||||
// We need to traverse the solids that share the same sketch.
|
||||
let sketch_id = solid.sketch.id;
|
||||
if !traversed_sketches.contains(&sketch_id) {
|
||||
// Find all the solids on the same shared sketch.
|
||||
ids.extend(
|
||||
self.stack()
|
||||
.walk_call_stack()
|
||||
.filter(|v| matches!(v, KclValue::Solid { value } if value.sketch.id == sketch_id))
|
||||
.flat_map(|v| match v {
|
||||
KclValue::Solid { value } => value.get_all_edge_cut_ids(),
|
||||
_ => unreachable!(),
|
||||
}),
|
||||
);
|
||||
traversed_sketches.push(sketch_id);
|
||||
}
|
||||
|
||||
ids.extend(solid.get_all_edge_cut_ids());
|
||||
}
|
||||
|
||||
// We can return early if there are no fillets or chamfers.
|
||||
if ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// We want to move these fillets and chamfers from batch_end to batch so they get executed
|
||||
// before what ever we call next.
|
||||
for id in ids {
|
||||
// Pop it off the batch_end and add it to the batch.
|
||||
let Some(item) = meta.ctx.engine.batch_end().write().await.shift_remove(&id) else {
|
||||
// It might be in the batch already.
|
||||
continue;
|
||||
};
|
||||
// Add it to the batch.
|
||||
meta.ctx.engine.batch().write().await.push(item);
|
||||
}
|
||||
|
||||
// Run flush.
|
||||
// Yes, we do need to actually flush the batch here, or references will fail later.
|
||||
self.flush_batch(meta, false).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -3,9 +3,7 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use indexmap::IndexMap;
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
use kcmc::websocket::WebSocketResponse;
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
use kittycad_modeling_cmds as kcmc;
|
||||
use kittycad_modeling_cmds::websocket::WebSocketResponse;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
@ -52,8 +50,6 @@ pub(super) struct GlobalState {
|
||||
pub errors: Vec<CompilationError>,
|
||||
#[cfg_attr(not(feature = "artifact-graph"), allow(dead_code))]
|
||||
pub artifacts: ArtifactState,
|
||||
#[cfg_attr(not(all(test, feature = "artifact-graph")), expect(dead_code))]
|
||||
pub root_module_artifacts: ModuleArtifactState,
|
||||
}
|
||||
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
@ -81,20 +77,6 @@ pub(super) struct ArtifactState {
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(super) struct ArtifactState {}
|
||||
|
||||
/// Artifact state for a single module.
|
||||
#[cfg(all(test, feature = "artifact-graph"))]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
|
||||
pub struct ModuleArtifactState {
|
||||
/// Outgoing engine commands.
|
||||
pub commands: Vec<ArtifactCommand>,
|
||||
/// Operations that have been performed in execution order.
|
||||
pub operations: Vec<Operation>,
|
||||
}
|
||||
|
||||
#[cfg(not(all(test, feature = "artifact-graph")))]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
|
||||
pub struct ModuleArtifactState {}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct ModuleState {
|
||||
/// The id generator for this module.
|
||||
@ -114,7 +96,6 @@ pub(super) struct ModuleState {
|
||||
pub settings: MetaSettings,
|
||||
pub(super) explicit_length_units: bool,
|
||||
pub(super) path: ModulePath,
|
||||
pub artifacts: ModuleArtifactState,
|
||||
}
|
||||
|
||||
impl ExecState {
|
||||
@ -145,17 +126,6 @@ impl ExecState {
|
||||
self.global.errors.push(e);
|
||||
}
|
||||
|
||||
pub fn clear_units_warnings(&mut self, source_range: &SourceRange) {
|
||||
self.global.errors = std::mem::take(&mut self.global.errors)
|
||||
.into_iter()
|
||||
.filter(|e| {
|
||||
e.severity != Severity::Warning
|
||||
|| !source_range.contains_range(&e.source_range)
|
||||
|| e.tag != crate::errors::Tag::UnknownNumericUnits
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
pub fn errors(&self) -> &[CompilationError] {
|
||||
&self.global.errors
|
||||
}
|
||||
@ -163,7 +133,7 @@ impl ExecState {
|
||||
/// Convert to execution outcome when running in WebAssembly. We want to
|
||||
/// reduce the amount of data that crosses the WASM boundary as much as
|
||||
/// possible.
|
||||
pub async fn into_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
|
||||
pub async fn to_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
|
||||
// Fields are opt-in so that we don't accidentally leak private internal
|
||||
// state when we add more to ExecState.
|
||||
ExecOutcome {
|
||||
@ -172,18 +142,22 @@ impl ExecState {
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
operations: self.global.artifacts.operations,
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
artifact_commands: self.global.artifacts.commands,
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
artifact_graph: self.global.artifacts.graph,
|
||||
errors: self.global.errors,
|
||||
default_planes: ctx.engine.get_default_planes().read().await.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn into_mock_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
|
||||
pub async fn to_mock_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
|
||||
ExecOutcome {
|
||||
variables: self.mod_local.variables(main_ref),
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
operations: Default::default(),
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
artifact_commands: Default::default(),
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
artifact_graph: Default::default(),
|
||||
errors: self.global.errors,
|
||||
filenames: Default::default(),
|
||||
@ -214,22 +188,12 @@ impl ExecState {
|
||||
}
|
||||
|
||||
pub(crate) fn push_op(&mut self, op: Operation) {
|
||||
#[cfg(all(test, feature = "artifact-graph"))]
|
||||
self.mod_local.artifacts.operations.push(op.clone());
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
self.global.artifacts.operations.push(op);
|
||||
#[cfg(not(feature = "artifact-graph"))]
|
||||
drop(op);
|
||||
}
|
||||
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
pub(crate) fn push_command(&mut self, command: ArtifactCommand) {
|
||||
#[cfg(all(test, feature = "artifact-graph"))]
|
||||
self.mod_local.artifacts.commands.push(command);
|
||||
#[cfg(not(all(test, feature = "artifact-graph")))]
|
||||
drop(command);
|
||||
}
|
||||
|
||||
pub(super) fn next_module_id(&self) -> ModuleId {
|
||||
ModuleId::from_usize(self.global.path_to_source_id.len())
|
||||
}
|
||||
@ -277,21 +241,6 @@ impl ExecState {
|
||||
self.global.module_infos.get(&id)
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "artifact-graph"))]
|
||||
pub(crate) fn modules(&self) -> &ModuleInfoMap {
|
||||
&self.global.module_infos
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "artifact-graph"))]
|
||||
pub(crate) fn operations(&self) -> &[Operation] {
|
||||
&self.global.artifacts.operations
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "artifact-graph"))]
|
||||
pub(crate) fn root_module_artifact_state(&self) -> &ModuleArtifactState {
|
||||
&self.global.root_module_artifacts
|
||||
}
|
||||
|
||||
pub fn current_default_units(&self) -> NumericType {
|
||||
NumericType::Default {
|
||||
len: self.length_unit(),
|
||||
@ -400,7 +349,6 @@ impl GlobalState {
|
||||
path_to_source_id: Default::default(),
|
||||
module_infos: Default::default(),
|
||||
artifacts: Default::default(),
|
||||
root_module_artifacts: Default::default(),
|
||||
mod_loader: Default::default(),
|
||||
errors: Default::default(),
|
||||
id_to_source: Default::default(),
|
||||
@ -440,15 +388,6 @@ impl ArtifactState {
|
||||
}
|
||||
}
|
||||
|
||||
impl ModuleArtifactState {
|
||||
/// When self is a cached state, extend it with new state.
|
||||
#[cfg(all(test, feature = "artifact-graph"))]
|
||||
pub(crate) fn extend(&mut self, other: ModuleArtifactState) {
|
||||
self.commands.extend(other.commands);
|
||||
self.operations.extend(other.operations);
|
||||
}
|
||||
}
|
||||
|
||||
impl ModuleState {
|
||||
pub(super) fn new(path: ModulePath, memory: Arc<ProgramMemory>, module_id: Option<ModuleId>) -> Self {
|
||||
ModuleState {
|
||||
@ -464,7 +403,6 @@ impl ModuleState {
|
||||
default_angle_units: Default::default(),
|
||||
kcl_version: "0.1".to_owned(),
|
||||
},
|
||||
artifacts: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,16 +104,6 @@ impl TypedPath {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn strip_prefix(&self, base: impl AsRef<std::path::Path>) -> Result<Self, std::path::StripPrefixError> {
|
||||
self.0.strip_prefix(base).map(|p| TypedPath(p.to_path_buf()))
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn canonicalize(&self) -> Result<Self, std::io::Error> {
|
||||
self.0.canonicalize().map(|p| TypedPath(p.to_path_buf()))
|
||||
}
|
||||
|
||||
pub fn to_string_lossy(&self) -> String {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
|
@ -438,7 +438,7 @@ impl fmt::Display for PrimitiveType {
|
||||
PrimitiveType::Any => write!(f, "any"),
|
||||
PrimitiveType::Number(NumericType::Known(unit)) => write!(f, "number({unit})"),
|
||||
PrimitiveType::Number(NumericType::Unknown) => write!(f, "number(unknown units)"),
|
||||
PrimitiveType::Number(NumericType::Default { .. }) => write!(f, "number"),
|
||||
PrimitiveType::Number(NumericType::Default { .. }) => write!(f, "number(default units)"),
|
||||
PrimitiveType::Number(NumericType::Any) => write!(f, "number(any units)"),
|
||||
PrimitiveType::String => write!(f, "string"),
|
||||
PrimitiveType::Boolean => write!(f, "bool"),
|
||||
@ -453,8 +453,8 @@ impl fmt::Display for PrimitiveType {
|
||||
PrimitiveType::Axis2d => write!(f, "Axis2d"),
|
||||
PrimitiveType::Axis3d => write!(f, "Axis3d"),
|
||||
PrimitiveType::Helix => write!(f, "Helix"),
|
||||
PrimitiveType::ImportedGeometry => write!(f, "ImportedGeometry"),
|
||||
PrimitiveType::Function => write!(f, "fn"),
|
||||
PrimitiveType::ImportedGeometry => write!(f, "imported geometry"),
|
||||
PrimitiveType::Function => write!(f, "function"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -499,6 +499,20 @@ impl NumericType {
|
||||
NumericType::Known(UnitType::Angle(UnitAngle::Degrees))
|
||||
}
|
||||
|
||||
pub fn expect_default_length(&self) -> Self {
|
||||
match self {
|
||||
NumericType::Default { len, .. } => NumericType::Known(UnitType::Length(*len)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_default_angle(&self) -> Self {
|
||||
match self {
|
||||
NumericType::Default { angle, .. } => NumericType::Known(UnitType::Angle(*angle)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Combine two types when we expect them to be equal, erring on the side of less coercion. To be
|
||||
/// precise, only adjusting one number or the other when they are of known types.
|
||||
///
|
||||
@ -540,10 +554,15 @@ impl NumericType {
|
||||
(at, Any) => (a.n, b.n, at),
|
||||
(Any, bt) => (a.n, b.n, bt),
|
||||
|
||||
(Default { .. }, Default { .. }) | (_, Unknown) | (Unknown, _) => (a.n, b.n, Unknown),
|
||||
|
||||
// Known types and compatible, but needs adjustment.
|
||||
(t @ Known(UnitType::Length(l1)), Known(UnitType::Length(l2))) => (a.n, l2.adjust_to(b.n, l1).0, t),
|
||||
(t @ Known(UnitType::Angle(a1)), Known(UnitType::Angle(a2))) => (a.n, a2.adjust_to(b.n, a1).0, t),
|
||||
|
||||
// Known but incompatible.
|
||||
(Known(_), Known(_)) => (a.n, b.n, Unknown),
|
||||
|
||||
// Known and unknown => we assume the known one, possibly with adjustment
|
||||
(Known(UnitType::Count), Default { .. }) | (Default { .. }, Known(UnitType::Count)) => {
|
||||
(a.n, b.n, Known(UnitType::Count))
|
||||
@ -551,12 +570,9 @@ impl NumericType {
|
||||
|
||||
(t @ Known(UnitType::Length(l1)), Default { len: l2, .. }) => (a.n, l2.adjust_to(b.n, l1).0, t),
|
||||
(Default { len: l1, .. }, t @ Known(UnitType::Length(l2))) => (l1.adjust_to(a.n, l2).0, b.n, t),
|
||||
|
||||
(t @ Known(UnitType::Angle(a1)), Default { angle: a2, .. }) => (a.n, a2.adjust_to(b.n, a1).0, t),
|
||||
(Default { angle: a1, .. }, t @ Known(UnitType::Angle(a2))) => (a1.adjust_to(a.n, a2).0, b.n, t),
|
||||
|
||||
(Known(_), Known(_)) | (Default { .. }, Default { .. }) | (_, Unknown) | (Unknown, _) => {
|
||||
(a.n, b.n, Unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -631,20 +647,6 @@ impl NumericType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Combine two types for modulo-like operations.
|
||||
pub fn combine_mod(a: TyF64, b: TyF64) -> (f64, f64, NumericType) {
|
||||
use NumericType::*;
|
||||
match (a.ty, b.ty) {
|
||||
(at @ Default { .. }, bt @ Default { .. }) if at == bt => (a.n, b.n, at),
|
||||
(at, bt) if at == bt => (a.n, b.n, at),
|
||||
(Default { .. }, Default { .. }) => (a.n, b.n, Unknown),
|
||||
(at, Known(UnitType::Count) | Any) => (a.n, b.n, at),
|
||||
(at @ Known(_), Default { .. }) => (a.n, b.n, at),
|
||||
(Known(UnitType::Count), _) => (a.n, b.n, Known(UnitType::Count)),
|
||||
_ => (a.n, b.n, Unknown),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_parsed(suffix: NumericSuffix, settings: &super::MetaSettings) -> Self {
|
||||
match suffix {
|
||||
NumericSuffix::None => NumericType::Default {
|
||||
@ -849,7 +851,7 @@ impl std::fmt::Display for UnitType {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO called UnitLen so as not to clash with UnitLength in settings.
|
||||
// TODO called UnitLen so as not to clash with UnitLength in settings)
|
||||
/// A unit of length.
|
||||
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
|
||||
#[ts(export)]
|
||||
@ -1508,23 +1510,6 @@ impl KclValue {
|
||||
KclValue::Module { .. } | KclValue::KclNone { .. } | KclValue::Type { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn principal_type_string(&self) -> String {
|
||||
if let Some(ty) = self.principal_type() {
|
||||
return format!("`{ty}`");
|
||||
}
|
||||
|
||||
match self {
|
||||
KclValue::Module { .. } => "module",
|
||||
KclValue::KclNone { .. } => "none",
|
||||
KclValue::Type { .. } => "type",
|
||||
_ => {
|
||||
debug_assert!(false);
|
||||
"<unexpected type>"
|
||||
}
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -2357,10 +2342,10 @@ d = cos(30)
|
||||
let result = parse_execute(program).await.unwrap();
|
||||
assert!(result.exec_state.errors().is_empty());
|
||||
|
||||
assert_value_and_type("a", &result, 1.0, NumericType::default());
|
||||
assert_value_and_type("a", &result, 1.0, NumericType::count());
|
||||
assert_value_and_type("b", &result, 3.0, NumericType::default());
|
||||
assert_value_and_type("c", &result, 1.0, NumericType::default());
|
||||
assert_value_and_type("d", &result, 1.0, NumericType::default());
|
||||
assert_value_and_type("c", &result, 1.0, NumericType::count());
|
||||
assert_value_and_type("d", &result, 1.0, NumericType::count());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
|
@ -47,7 +47,7 @@ impl Tag {
|
||||
match self {
|
||||
Tag::Deprecated => Some(vec![DiagnosticTag::DEPRECATED]),
|
||||
Tag::Unnecessary => Some(vec![DiagnosticTag::UNNECESSARY]),
|
||||
Tag::UnknownNumericUnits | Tag::None => None,
|
||||
Tag::None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -950,7 +950,7 @@ startSketchOn(XY)
|
||||
|
||||
match hover.unwrap().contents {
|
||||
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => {
|
||||
assert!(value.contains("foo: number = 42"));
|
||||
assert!(value.contains("foo: number(default units) = 42"));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
@ -3900,7 +3900,7 @@ startSketchOn(XY)
|
||||
|
||||
match hover.unwrap().contents {
|
||||
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => {
|
||||
assert!(value.contains("foo: number = 42"));
|
||||
assert!(value.contains("foo: number(default units) = 42"));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
exec::KclValue,
|
||||
execution::{typed_path::TypedPath, EnvironmentRef, ModuleArtifactState, PreImportedGeometry},
|
||||
execution::{typed_path::TypedPath, EnvironmentRef, PreImportedGeometry},
|
||||
fs::{FileManager, FileSystem},
|
||||
parsing::ast::types::{ImportPath, Node, Program},
|
||||
source_range::SourceRange,
|
||||
@ -131,11 +131,8 @@ impl ModuleInfo {
|
||||
pub enum ModuleRepr {
|
||||
Root,
|
||||
// AST, memory, exported names
|
||||
Kcl(
|
||||
Node<Program>,
|
||||
Option<(Option<KclValue>, EnvironmentRef, Vec<String>, ModuleArtifactState)>,
|
||||
),
|
||||
Foreign(PreImportedGeometry, Option<(Option<KclValue>, ModuleArtifactState)>),
|
||||
Kcl(Node<Program>, Option<(Option<KclValue>, EnvironmentRef, Vec<String>)>),
|
||||
Foreign(PreImportedGeometry, Option<KclValue>),
|
||||
Dummy,
|
||||
}
|
||||
|
||||
|
@ -3174,19 +3174,6 @@ impl PrimitiveType {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn display_multiple(&self) -> String {
|
||||
match self {
|
||||
PrimitiveType::Any => "values".to_owned(),
|
||||
PrimitiveType::Number(_) => "numbers".to_owned(),
|
||||
PrimitiveType::String => "strings".to_owned(),
|
||||
PrimitiveType::Boolean => "bools".to_owned(),
|
||||
PrimitiveType::ImportedGeometry => "imported geometries".to_owned(),
|
||||
PrimitiveType::Function(_) => "functions".to_owned(),
|
||||
PrimitiveType::Named { id } => format!("`{}`s", id.name),
|
||||
PrimitiveType::Tag => "tags".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PrimitiveType {
|
||||
@ -3277,53 +3264,6 @@ pub enum Type {
|
||||
},
|
||||
}
|
||||
|
||||
impl Type {
|
||||
pub fn human_friendly_type(&self) -> String {
|
||||
match self {
|
||||
Type::Primitive(ty) => format!("a value with type `{ty}`"),
|
||||
Type::Array {
|
||||
ty,
|
||||
len: ArrayLen::None | ArrayLen::Minimum(0),
|
||||
} => {
|
||||
format!("an array of {}", ty.display_multiple())
|
||||
}
|
||||
Type::Array {
|
||||
ty,
|
||||
len: ArrayLen::Minimum(1),
|
||||
} => format!("one or more {}", ty.display_multiple()),
|
||||
Type::Array {
|
||||
ty,
|
||||
len: ArrayLen::Minimum(n),
|
||||
} => {
|
||||
format!("an array of {n} or more {}", ty.display_multiple())
|
||||
}
|
||||
Type::Array {
|
||||
ty,
|
||||
len: ArrayLen::Known(n),
|
||||
} => format!("an array of {n} {}", ty.display_multiple()),
|
||||
Type::Union { tys } => tys
|
||||
.iter()
|
||||
.map(|t| t.human_friendly_type())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" or "),
|
||||
Type::Object { .. } => format!("an object with fields `{}`", self),
|
||||
}
|
||||
}
|
||||
|
||||
fn display_multiple(&self) -> String {
|
||||
match self {
|
||||
Type::Primitive(ty) => ty.display_multiple(),
|
||||
Type::Array { .. } => "arrays".to_owned(),
|
||||
Type::Union { tys } => tys
|
||||
.iter()
|
||||
.map(|t| t.display_multiple())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" or "),
|
||||
Type::Object { .. } => format!("objects with fields `{self}`"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Type {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
|
@ -4,6 +4,8 @@ excerpt: "{{{description}}}"
|
||||
layout: manual
|
||||
---
|
||||
|
||||
# {{title}}
|
||||
|
||||
{{{description}}}
|
||||
|
||||
## {{config_type}} Structure
|
||||
@ -62,4 +64,4 @@ This setting has further nested options. See the schema for full details.
|
||||
|
||||
```toml
|
||||
{{{example}}}
|
||||
```
|
||||
```
|
@ -3,18 +3,13 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use insta::rounded_redaction;
|
||||
|
||||
use crate::{
|
||||
errors::KclError,
|
||||
execution::{EnvironmentRef, ModuleArtifactState},
|
||||
ExecOutcome, ExecState, ExecutorContext, ModuleId,
|
||||
};
|
||||
use crate::{errors::KclError, ModuleId};
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
use crate::{
|
||||
exec::ArtifactCommand,
|
||||
execution::{ArtifactGraph, Operation},
|
||||
modules::{ModulePath, ModuleRepr},
|
||||
};
|
||||
|
||||
mod kcl_samples;
|
||||
@ -24,7 +19,8 @@ mod kcl_samples;
|
||||
struct Test {
|
||||
/// The name of the test.
|
||||
name: String,
|
||||
/// The KCL file that's the entry point, e.g. "main.kcl", in the `input_dir`.
|
||||
/// The name of the KCL file that's the entry point, e.g. "main.kcl", in the
|
||||
/// `input_dir`.
|
||||
entry_point: PathBuf,
|
||||
/// Input KCL files are in this directory.
|
||||
input_dir: PathBuf,
|
||||
@ -38,9 +34,6 @@ struct Test {
|
||||
|
||||
pub(crate) const RENDERED_MODEL_NAME: &str = "rendered_model.png";
|
||||
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
const REPO_ROOT: &str = "../..";
|
||||
|
||||
impl Test {
|
||||
fn new(name: &str) -> Self {
|
||||
Self {
|
||||
@ -59,75 +52,6 @@ impl Test {
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecState {
|
||||
/// Same as [`Self::into_exec_outcome`], but also returns the module state.
|
||||
async fn into_test_exec_outcome(
|
||||
self,
|
||||
main_ref: EnvironmentRef,
|
||||
ctx: &ExecutorContext,
|
||||
project_directory: &Path,
|
||||
) -> (ExecOutcome, IndexMap<String, ModuleArtifactState>) {
|
||||
let module_state = self.to_module_state(project_directory);
|
||||
let outcome = self.into_exec_outcome(main_ref, ctx).await;
|
||||
(outcome, module_state)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "artifact-graph"))]
|
||||
fn to_module_state(&self, _project_directory: &Path) -> IndexMap<String, ModuleArtifactState> {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// The keys of the map are the module paths. Can't use `ModulePath` since
|
||||
/// it needs to be converted to a string to be a JSON object key. The paths
|
||||
/// need to be relative so that generating locally works in CI.
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
fn to_module_state(&self, _project_directory: &Path) -> IndexMap<String, ModuleArtifactState> {
|
||||
let project_directory = std::path::Path::new(REPO_ROOT)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| panic!("Failed to canonicalize project directory: {REPO_ROOT}"));
|
||||
let mut module_state = IndexMap::new();
|
||||
for info in self.modules().values() {
|
||||
let relative_path = relative_module_path(&info.path, &project_directory).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"Failed to get relative module path for {:?} in {:?}; caused by {err:?}",
|
||||
&info.path, project_directory
|
||||
)
|
||||
});
|
||||
match &info.repr {
|
||||
ModuleRepr::Root => {
|
||||
module_state.insert(relative_path, self.root_module_artifact_state().clone());
|
||||
}
|
||||
ModuleRepr::Kcl(_, None) => {
|
||||
module_state.insert(relative_path, Default::default());
|
||||
}
|
||||
ModuleRepr::Kcl(_, Some((_, _, _, module_artifacts))) => {
|
||||
module_state.insert(relative_path, module_artifacts.clone());
|
||||
}
|
||||
ModuleRepr::Foreign(_, Some((_, module_artifacts))) => {
|
||||
module_state.insert(relative_path, module_artifacts.clone());
|
||||
}
|
||||
ModuleRepr::Foreign(_, None) | ModuleRepr::Dummy => {}
|
||||
}
|
||||
}
|
||||
module_state
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
fn relative_module_path(module_path: &ModulePath, abs_project_directory: &Path) -> Result<String, std::io::Error> {
|
||||
match module_path {
|
||||
ModulePath::Main => Ok("main".to_owned()),
|
||||
ModulePath::Local { value: path } => {
|
||||
let abs_path = path.canonicalize()?;
|
||||
abs_path
|
||||
.strip_prefix(abs_project_directory)
|
||||
.map(|p| p.to_string_lossy())
|
||||
.map_err(|_| std::io::Error::other(format!("Failed to strip prefix from module path {abs_path:?}")))
|
||||
}
|
||||
ModulePath::Std { value } => Ok(format!("std::{value}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_snapshot<F, R>(test: &Test, operation: &str, f: F)
|
||||
where
|
||||
F: FnOnce() -> R,
|
||||
@ -257,7 +181,7 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
|
||||
panic!("Step data was empty");
|
||||
}
|
||||
}
|
||||
let (outcome, module_state) = exec_state.into_test_exec_outcome(env_ref, &ctx, &test.input_dir).await;
|
||||
let outcome = exec_state.to_exec_outcome(env_ref, &ctx).await;
|
||||
|
||||
let mem_result = catch_unwind(AssertUnwindSafe(|| {
|
||||
assert_snapshot(test, "Variables in memory after executing", || {
|
||||
@ -278,10 +202,13 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
|
||||
})
|
||||
}));
|
||||
|
||||
#[cfg(not(feature = "artifact-graph"))]
|
||||
drop(module_state);
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
assert_artifact_snapshots(test, module_state, outcome.operations, outcome.artifact_graph);
|
||||
assert_common_snapshots(
|
||||
test,
|
||||
outcome.operations,
|
||||
outcome.artifact_commands,
|
||||
outcome.artifact_graph,
|
||||
);
|
||||
mem_result.unwrap();
|
||||
}
|
||||
Err(e) => {
|
||||
@ -311,23 +238,7 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
|
||||
}));
|
||||
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
{
|
||||
let global_operations = if !error.operations.is_empty() {
|
||||
error.operations
|
||||
} else if let Some(exec_state) = &e.exec_state {
|
||||
// Non-fatal compilation errors don't have artifact
|
||||
// output attached, so we need to get it from
|
||||
// ExecState.
|
||||
exec_state.operations().to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let module_state = e
|
||||
.exec_state
|
||||
.map(|e| e.to_module_state(&test.input_dir))
|
||||
.unwrap_or_default();
|
||||
assert_artifact_snapshots(test, module_state, global_operations, error.artifact_graph);
|
||||
}
|
||||
assert_common_snapshots(test, error.operations, error.artifact_commands, error.artifact_graph);
|
||||
err_result.unwrap();
|
||||
}
|
||||
e => {
|
||||
@ -341,44 +252,56 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert snapshots for artifacts that should happen both when KCL execution
|
||||
/// succeeds and when it results in an error.
|
||||
/// Assert snapshots that should happen both when KCL execution succeeds and
|
||||
/// when it results in an error.
|
||||
#[cfg(feature = "artifact-graph")]
|
||||
fn assert_artifact_snapshots(
|
||||
fn assert_common_snapshots(
|
||||
test: &Test,
|
||||
module_state: IndexMap<String, ModuleArtifactState>,
|
||||
global_operations: Vec<Operation>,
|
||||
operations: Vec<Operation>,
|
||||
artifact_commands: Vec<ArtifactCommand>,
|
||||
artifact_graph: ArtifactGraph,
|
||||
) {
|
||||
let module_operations = module_state
|
||||
.iter()
|
||||
.map(|(path, s)| (path, &s.operations))
|
||||
.collect::<IndexMap<_, _>>();
|
||||
let operations = {
|
||||
// Make the operations deterministic by sorting them by their module ID,
|
||||
// then by their range.
|
||||
let mut operations = operations.clone();
|
||||
operations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
operations
|
||||
};
|
||||
let artifact_commands = {
|
||||
// Due to our newfound concurrency, we're going to mess with the
|
||||
// artifact_commands a bit -- we're going to maintain the order,
|
||||
// but only for a given module ID. This means the artifact_commands
|
||||
// is no longer meaningful, but it is deterministic and will hopefully
|
||||
// catch meaningful changes in behavior.
|
||||
// We sort by the source range, like we do for the operations.
|
||||
|
||||
let mut artifact_commands = artifact_commands.clone();
|
||||
artifact_commands.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
artifact_commands
|
||||
};
|
||||
|
||||
let result1 = catch_unwind(AssertUnwindSafe(|| {
|
||||
assert_snapshot(test, "Operations executed", || {
|
||||
insta::assert_json_snapshot!("ops", module_operations, {
|
||||
".*[].*.unlabeledArg.*.value.**[].from[]" => rounded_redaction(3),
|
||||
".*[].*.unlabeledArg.*.value.**[].to[]" => rounded_redaction(3),
|
||||
".*[].**.value.value" => rounded_redaction(3),
|
||||
".*[].*.labeledArgs.*.value.**[].from[]" => rounded_redaction(3),
|
||||
".*[].*.labeledArgs.*.value.**[].to[]" => rounded_redaction(3),
|
||||
insta::assert_json_snapshot!("ops", operations, {
|
||||
"[].*.unlabeledArg.*.value.**[].from[]" => rounded_redaction(3),
|
||||
"[].*.unlabeledArg.*.value.**[].to[]" => rounded_redaction(3),
|
||||
"[].**.value.value" => rounded_redaction(3),
|
||||
"[].*.labeledArgs.*.value.**[].from[]" => rounded_redaction(3),
|
||||
"[].*.labeledArgs.*.value.**[].to[]" => rounded_redaction(3),
|
||||
".**.sourceRange" => Vec::new(),
|
||||
".**.functionSourceRange" => Vec::new(),
|
||||
".**.moduleId" => 0,
|
||||
});
|
||||
})
|
||||
}));
|
||||
let module_commands = module_state
|
||||
.iter()
|
||||
.map(|(path, s)| (path, &s.commands))
|
||||
.collect::<IndexMap<_, _>>();
|
||||
let result2 = catch_unwind(AssertUnwindSafe(|| {
|
||||
assert_snapshot(test, "Artifact commands", || {
|
||||
insta::assert_json_snapshot!("artifact_commands", module_commands, {
|
||||
".*[].command.**.value" => rounded_redaction(3),
|
||||
".*[].command.**.x" => rounded_redaction(3),
|
||||
".*[].command.**.y" => rounded_redaction(3),
|
||||
".*[].command.**.z" => rounded_redaction(3),
|
||||
insta::assert_json_snapshot!("artifact_commands", artifact_commands, {
|
||||
"[].command.**.value" => rounded_redaction(3),
|
||||
"[].command.**.x" => rounded_redaction(3),
|
||||
"[].command.**.y" => rounded_redaction(3),
|
||||
"[].command.**.z" => rounded_redaction(3),
|
||||
".**.range" => Vec::new(),
|
||||
});
|
||||
})
|
||||
@ -414,25 +337,6 @@ fn assert_artifact_snapshots(
|
||||
result1.unwrap();
|
||||
result2.unwrap();
|
||||
result3.unwrap();
|
||||
|
||||
// The global operations should be a superset of the main module. But it
|
||||
// won't always be a superset of the operations of all modules.
|
||||
let repo_root = std::path::Path::new(REPO_ROOT).canonicalize().unwrap();
|
||||
let root_string: String = test
|
||||
.entry_point
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| panic!("Should be able to canonicalize the entry point {:?}", &test.entry_point))
|
||||
.strip_prefix(&repo_root)
|
||||
.expect("Repo root dir should be a prefix of the entry point")
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let main_operations = module_operations
|
||||
.get(&root_string)
|
||||
.expect("Main module state not found");
|
||||
assert!(
|
||||
global_operations.len() >= main_operations.len(),
|
||||
"global_operations={global_operations:#?}, main_operations={main_operations:#?}"
|
||||
);
|
||||
}
|
||||
|
||||
mod cube {
|
||||
@ -498,8 +402,8 @@ mod any_type {
|
||||
super::execute(TEST_NAME, false).await
|
||||
}
|
||||
}
|
||||
mod coerce_from_trig_to_point {
|
||||
const TEST_NAME: &str = "coerce_from_trig_to_point";
|
||||
mod error_with_point_shows_numeric_units {
|
||||
const TEST_NAME: &str = "error_with_point_shows_numeric_units";
|
||||
|
||||
/// Test parsing KCL.
|
||||
#[test]
|
||||
@ -3642,27 +3546,3 @@ mod var_ref_in_own_def_decl {
|
||||
super::execute(TEST_NAME, true).await
|
||||
}
|
||||
}
|
||||
mod user_reported_union_2_bug {
|
||||
// TODO IF THIS TEST START PASSING, CLOSE THE FOLLOWING ISSUE
|
||||
// https://github.com/KittyCAD/modeling-app/issues/7310
|
||||
// and https://github.com/KittyCAD/engine/issues/3539
|
||||
const TEST_NAME: &str = "user_reported_union_2_bug";
|
||||
|
||||
/// Test parsing KCL.
|
||||
#[test]
|
||||
fn parse() {
|
||||
super::parse(TEST_NAME)
|
||||
}
|
||||
|
||||
/// Test that parsing and unparsing KCL produces the original KCL input.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn unparse() {
|
||||
super::unparse(TEST_NAME).await
|
||||
}
|
||||
|
||||
/// Test that KCL is executed correctly.
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kcl_test_execute() {
|
||||
super::execute(TEST_NAME, false).await
|
||||
}
|
||||
}
|
||||
|
@ -106,18 +106,17 @@ async fn inner_appearance(
|
||||
a: 100.0,
|
||||
};
|
||||
|
||||
exec_state
|
||||
.batch_modeling_cmd(
|
||||
(&args).into(),
|
||||
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
|
||||
object_id: solid_id,
|
||||
color,
|
||||
metalness: metalness.unwrap_or_default() as f32 / 100.0,
|
||||
roughness: roughness.unwrap_or_default() as f32 / 100.0,
|
||||
ambient_occlusion: 0.0,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
args.batch_modeling_cmd(
|
||||
exec_state.next_uuid(),
|
||||
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
|
||||
object_id: solid_id,
|
||||
color,
|
||||
metalness: metalness.unwrap_or_default() as f32 / 100.0,
|
||||
roughness: roughness.unwrap_or_default() as f32 / 100.0,
|
||||
ambient_occlusion: 0.0,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Idk if we want to actually modify the memory for the colors, but I'm not right now since
|
||||
// I can't think of a use case for it.
|
||||
|
@ -1,10 +1,14 @@
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
use anyhow::Result;
|
||||
use kcmc::{
|
||||
websocket::{ModelingCmdReq, OkWebSocketResponseData},
|
||||
ModelingCmd,
|
||||
};
|
||||
use kittycad_modeling_cmds as kcmc;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::fillet::EdgeReference;
|
||||
pub use crate::execution::fn_call::Args;
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
@ -24,6 +28,8 @@ use crate::{
|
||||
ModuleId,
|
||||
};
|
||||
|
||||
use super::fillet::EdgeReference;
|
||||
|
||||
const ERROR_STRING_SKETCH_TO_SOLID_HELPER: &str =
|
||||
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`";
|
||||
|
||||
@ -160,7 +166,7 @@ impl Args {
|
||||
None => msg_base,
|
||||
Some(sugg) => format!("{msg_base}. {sugg}"),
|
||||
};
|
||||
if message.contains("one or more Solids or ImportedGeometry but it's actually of type Sketch") {
|
||||
if message.contains("one or more Solids or imported geometry but it's actually of type Sketch") {
|
||||
message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
|
||||
}
|
||||
KclError::new_semantic(KclErrorDetails::new(message, arg.source_ranges()))
|
||||
@ -257,7 +263,7 @@ impl Args {
|
||||
Some(sugg) => format!("{msg_base}. {sugg}"),
|
||||
};
|
||||
|
||||
if message.contains("one or more Solids or ImportedGeometry but it's actually of type Sketch") {
|
||||
if message.contains("one or more Solids or imported geometry but it's actually of type Sketch") {
|
||||
message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
|
||||
}
|
||||
KclError::new_semantic(KclErrorDetails::new(message, arg.source_ranges()))
|
||||
@ -271,7 +277,36 @@ impl Args {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Move this to the modeling module.
|
||||
// Add a modeling command to the batch but don't fire it right away.
|
||||
pub(crate) async fn batch_modeling_cmd(
|
||||
&self,
|
||||
id: uuid::Uuid,
|
||||
cmd: ModelingCmd,
|
||||
) -> Result<(), crate::errors::KclError> {
|
||||
self.ctx.engine.batch_modeling_cmd(id, self.source_range, &cmd).await
|
||||
}
|
||||
|
||||
// Add multiple modeling commands to the batch but don't fire them right away.
|
||||
pub(crate) async fn batch_modeling_cmds(&self, cmds: &[ModelingCmdReq]) -> Result<(), crate::errors::KclError> {
|
||||
self.ctx.engine.batch_modeling_cmds(self.source_range, cmds).await
|
||||
}
|
||||
|
||||
// Add a modeling commandSolid> to the batch that gets executed at the end of the file.
|
||||
// This is good for something like fillet or chamfer where the engine would
|
||||
// eat the path id if we executed it right away.
|
||||
pub(crate) async fn batch_end_cmd(&self, id: uuid::Uuid, cmd: ModelingCmd) -> Result<(), crate::errors::KclError> {
|
||||
self.ctx.engine.batch_end_cmd(id, self.source_range, &cmd).await
|
||||
}
|
||||
|
||||
/// Send the modeling cmd and wait for the response.
|
||||
pub(crate) async fn send_modeling_cmd(
|
||||
&self,
|
||||
id: uuid::Uuid,
|
||||
cmd: ModelingCmd,
|
||||
) -> Result<OkWebSocketResponseData, KclError> {
|
||||
self.ctx.engine.send_modeling_cmd(id, self.source_range, &cmd).await
|
||||
}
|
||||
|
||||
fn get_tag_info_from_memory<'a, 'e>(
|
||||
&'a self,
|
||||
exec_state: &'e mut ExecState,
|
||||
@ -295,7 +330,6 @@ impl Args {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move this to the modeling module.
|
||||
pub(crate) fn get_tag_engine_info<'a, 'e>(
|
||||
&'a self,
|
||||
exec_state: &'e mut ExecState,
|
||||
@ -311,7 +345,6 @@ impl Args {
|
||||
self.get_tag_info_from_memory(exec_state, tag)
|
||||
}
|
||||
|
||||
// TODO: Move this to the modeling module.
|
||||
fn get_tag_engine_info_check_surface<'a, 'e>(
|
||||
&'a self,
|
||||
exec_state: &'e mut ExecState,
|
||||
@ -329,6 +362,63 @@ impl Args {
|
||||
self.get_tag_info_from_memory(exec_state, tag)
|
||||
}
|
||||
|
||||
/// Flush just the fillets and chamfers for this specific SolidSet.
|
||||
#[allow(clippy::vec_box)]
|
||||
pub(crate) async fn flush_batch_for_solids(
|
||||
&self,
|
||||
exec_state: &mut ExecState,
|
||||
solids: &[Solid],
|
||||
) -> Result<(), KclError> {
|
||||
// Make sure we don't traverse sketches more than once.
|
||||
let mut traversed_sketches = Vec::new();
|
||||
|
||||
// Collect all the fillet/chamfer ids for the solids.
|
||||
let mut ids = Vec::new();
|
||||
for solid in solids {
|
||||
// We need to traverse the solids that share the same sketch.
|
||||
let sketch_id = solid.sketch.id;
|
||||
if !traversed_sketches.contains(&sketch_id) {
|
||||
// Find all the solids on the same shared sketch.
|
||||
ids.extend(
|
||||
exec_state
|
||||
.stack()
|
||||
.walk_call_stack()
|
||||
.filter(|v| matches!(v, KclValue::Solid { value } if value.sketch.id == sketch_id))
|
||||
.flat_map(|v| match v {
|
||||
KclValue::Solid { value } => value.get_all_edge_cut_ids(),
|
||||
_ => unreachable!(),
|
||||
}),
|
||||
);
|
||||
traversed_sketches.push(sketch_id);
|
||||
}
|
||||
|
||||
ids.extend(solid.get_all_edge_cut_ids());
|
||||
}
|
||||
|
||||
// We can return early if there are no fillets or chamfers.
|
||||
if ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// We want to move these fillets and chamfers from batch_end to batch so they get executed
|
||||
// before what ever we call next.
|
||||
for id in ids {
|
||||
// Pop it off the batch_end and add it to the batch.
|
||||
let Some(item) = self.ctx.engine.batch_end().write().await.shift_remove(&id) else {
|
||||
// It might be in the batch already.
|
||||
continue;
|
||||
};
|
||||
// Add it to the batch.
|
||||
self.ctx.engine.batch().write().await.push(item);
|
||||
}
|
||||
|
||||
// Run flush.
|
||||
// Yes, we do need to actually flush the batch here, or references will fail later.
|
||||
self.ctx.engine.flush_batch(false, self.source_range).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn make_kcl_val_from_point(&self, p: [f64; 2], ty: NumericType) -> Result<KclValue, KclError> {
|
||||
let meta = Metadata {
|
||||
source_range: self.source_range,
|
||||
@ -358,7 +448,6 @@ impl Args {
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Move this to the modeling module.
|
||||
pub(crate) async fn get_adjacent_face_to_tag(
|
||||
&self,
|
||||
exec_state: &mut ExecState,
|
||||
@ -448,12 +537,107 @@ impl Args {
|
||||
}
|
||||
}
|
||||
|
||||
/// Types which impl this trait can be read out of the `Args` passed into a KCL function.
|
||||
pub trait FromArgs<'a>: Sized {
|
||||
/// Get this type from the args passed into a KCL function, at the given index in the argument list.
|
||||
fn from_args(args: &'a Args, index: usize) -> Result<Self, KclError>;
|
||||
}
|
||||
|
||||
/// Types which impl this trait can be extracted from a `KclValue`.
|
||||
pub trait FromKclValue<'a>: Sized {
|
||||
/// Try to convert a KclValue into this type.
|
||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self>;
|
||||
}
|
||||
|
||||
impl<'a, T> FromArgs<'a> for T
|
||||
where
|
||||
T: FromKclValue<'a> + Sized,
|
||||
{
|
||||
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
|
||||
let Some(arg) = args.args.get(i) else {
|
||||
return Err(KclError::new_semantic(KclErrorDetails::new(
|
||||
format!("Expected an argument at index {i}"),
|
||||
vec![args.source_range],
|
||||
)));
|
||||
};
|
||||
let Some(val) = T::from_kcl_val(&arg.value) else {
|
||||
return Err(KclError::new_semantic(KclErrorDetails::new(
|
||||
format!(
|
||||
"Argument at index {i} was supposed to be type {} but found {}",
|
||||
tynm::type_name::<T>(),
|
||||
arg.value.human_friendly_type(),
|
||||
),
|
||||
arg.source_ranges(),
|
||||
)));
|
||||
};
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> FromArgs<'a> for Option<T>
|
||||
where
|
||||
T: FromKclValue<'a> + Sized,
|
||||
{
|
||||
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
|
||||
let Some(arg) = args.args.get(i) else { return Ok(None) };
|
||||
if crate::parsing::ast::types::KclNone::from_kcl_val(&arg.value).is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(val) = T::from_kcl_val(&arg.value) else {
|
||||
return Err(KclError::new_semantic(KclErrorDetails::new(
|
||||
format!(
|
||||
"Argument at index {i} was supposed to be type Option<{}> but found {}",
|
||||
tynm::type_name::<T>(),
|
||||
arg.value.human_friendly_type()
|
||||
),
|
||||
arg.source_ranges(),
|
||||
)));
|
||||
};
|
||||
Ok(Some(val))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, A, B> FromArgs<'a> for (A, B)
|
||||
where
|
||||
A: FromArgs<'a>,
|
||||
B: FromArgs<'a>,
|
||||
{
|
||||
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
|
||||
let a = A::from_args(args, i)?;
|
||||
let b = B::from_args(args, i + 1)?;
|
||||
Ok((a, b))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, A, B, C> FromArgs<'a> for (A, B, C)
|
||||
where
|
||||
A: FromArgs<'a>,
|
||||
B: FromArgs<'a>,
|
||||
C: FromArgs<'a>,
|
||||
{
|
||||
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
|
||||
let a = A::from_args(args, i)?;
|
||||
let b = B::from_args(args, i + 1)?;
|
||||
let c = C::from_args(args, i + 2)?;
|
||||
Ok((a, b, c))
|
||||
}
|
||||
}
|
||||
impl<'a, A, B, C, D> FromArgs<'a> for (A, B, C, D)
|
||||
where
|
||||
A: FromArgs<'a>,
|
||||
B: FromArgs<'a>,
|
||||
C: FromArgs<'a>,
|
||||
D: FromArgs<'a>,
|
||||
{
|
||||
fn from_args(args: &'a Args, i: usize) -> Result<Self, KclError> {
|
||||
let a = A::from_args(args, i)?;
|
||||
let b = B::from_args(args, i + 1)?;
|
||||
let c = C::from_args(args, i + 2)?;
|
||||
let d = D::from_args(args, i + 3)?;
|
||||
Ok((a, b, c, d))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromKclValue<'a> for TagNode {
|
||||
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
|
||||
arg.get_tag_declarator().ok()
|
||||
|
@ -7,10 +7,7 @@ use kittycad_modeling_cmds as kcmc;
|
||||
use super::args::TyF64;
|
||||
use crate::{
|
||||
errors::{KclError, KclErrorDetails},
|
||||
execution::{
|
||||
types::RuntimeType, ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, ModelingCmdMeta,
|
||||
Solid,
|
||||
},
|
||||
execution::{types::RuntimeType, ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, Solid},
|
||||
parsing::ast::types::TagNode,
|
||||
std::{fillet::EdgeReference, Args},
|
||||
};
|
||||
@ -55,21 +52,20 @@ async fn inner_chamfer(
|
||||
};
|
||||
|
||||
let id = exec_state.next_uuid();
|
||||
exec_state
|
||||
.batch_end_cmd(
|
||||
ModelingCmdMeta::from_args_id(&args, id),
|
||||
ModelingCmd::from(mcmd::Solid3dFilletEdge {
|
||||
edge_id: None,
|
||||
edge_ids: vec![edge_id],
|
||||
extra_face_ids: vec![],
|
||||
strategy: Default::default(),
|
||||
object_id: solid.id,
|
||||
radius: LengthUnit(length.to_mm()),
|
||||
tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future.
|
||||
cut_type: CutType::Chamfer,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
args.batch_end_cmd(
|
||||
id,
|
||||
ModelingCmd::from(mcmd::Solid3dFilletEdge {
|
||||
edge_id: None,
|
||||
edge_ids: vec![edge_id],
|
||||
extra_face_ids: vec![],
|
||||
strategy: Default::default(),
|
||||
object_id: solid.id,
|
||||
radius: LengthUnit(length.to_mm()),
|
||||
tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future.
|
||||
cut_type: CutType::Chamfer,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
solid.edge_cuts.push(EdgeCut::Chamfer {
|
||||
id,
|
||||
|