Compare commits

..

2 Commits

Author SHA1 Message Date
c42903d2e8 Merge branch 'main' into kurt-web-app-oauth 2025-06-06 14:28:39 +10:00
d98669fb8a web app oauth 2025-06-06 13:35:11 +10:00
627 changed files with 11566899 additions and 11651578 deletions

View File

@ -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

View File

@ -10,7 +10,7 @@ on:
env: env:
IS_RELEASE: ${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }} 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: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -91,14 +91,14 @@ jobs:
if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }} if: ${{ steps.wasm.outputs.should-build-wasm == 'true' }}
run: "npm run build:wasm" run: "npm run build:wasm"
- name: Set staging version, product name, release notes, and icons - name: Set nightly version, product name, release notes, and icons
if: ${{ env.IS_STAGING == 'true' }} if: ${{ env.IS_NIGHTLY == 'true' }}
run: | run: |
COMMIT=$(git rev-parse --short HEAD) COMMIT=$(git rev-parse --short HEAD)
DATE=$(date +'%-y.%-m.%-d') DATE=$(date +'%-y.%-m.%-d')
export VERSION=$DATE-main.$COMMIT export VERSION=$DATE-main.$COMMIT
npm run files:set-version npm run files:set-version
npm run files:flip-to-staging npm run files:flip-to-nightly
- name: Set release version - name: Set release version
if: ${{ env.IS_RELEASE == 'true' }} if: ${{ env.IS_RELEASE == 'true' }}
@ -130,14 +130,13 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: namespace-profile-macos-6-cores - os: macos-14
platform: macos platform: mac
- os: namespace-profile-windows-4-cores - os: windows-2022
platform: windows platform: win
- os: ubuntu-22.04 - os: ubuntu-22.04
platform: linux platform: linux
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: build-apps (${{ matrix.platform }})
env: env:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
steps: steps:
@ -167,14 +166,14 @@ jobs:
- run: npm install - run: npm install
- name: Prepare certificate and variables (Windows only) - 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: | run: |
echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /c/Certificate_pkcs12.p12 echo "${{secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
cat /c/Certificate_pkcs12.p12 cat /d/Certificate_pkcs12.p12
echo "::set-output name=version::${GITHUB_REF#refs/tags/v}" echo "::set-output name=version::${GITHUB_REF#refs/tags/v}"
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV" echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$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 "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)\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 echo "C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools" >> $GITHUB_PATH
@ -182,7 +181,7 @@ jobs:
shell: bash shell: bash
- name: Setup certicate with SSM KSP (Windows only) - 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: | 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 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 msiexec /i smtools-windows-x64.msi /quiet /qn
@ -192,7 +191,7 @@ jobs:
smksp_cert_sync.exe smksp_cert_sync.exe
smctl windows certsync smctl windows certsync
# This last line `smctl windows certsync` was added after windows codesign failures started happening # 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 # 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 # 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. # seems to be doing that extra sync that we need for scripts/sign-win.js to work.
@ -200,13 +199,13 @@ jobs:
shell: cmd shell: cmd
- name: Build the app (debug) - 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, # 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 # this is just not doing any codesign or release yml generation, and points to dev infra
run: npm run tronb:package:dev run: npm run tronb:package:dev
- name: Build the app (release) - name: Build the app (release)
if: ${{ env.IS_RELEASE == 'true' || env.IS_STAGING == 'true' }} if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
@ -216,7 +215,7 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} 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/ - name: List artifacts in out/
run: ls -R out run: ls -R out
@ -240,20 +239,20 @@ jobs:
out/*-x86_64-linux.* out/*-x86_64-linux.*
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ env.IS_RELEASE == 'true' || env.IS_STAGING == 'true' }} if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
with: with:
name: out-yml-${{ matrix.platform }} name: out-yml-${{ matrix.platform }}
path: | path: |
out/latest*.yml 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: upload-apps-release:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
permissions: permissions:
contents: write 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') }} if: ${{ (github.ref_type == 'tag' && startsWith(github.ref_name, 'v')) || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
env: env:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} VERSION_NO_V: ${{ needs.prepare-files.outputs.version }}
@ -264,32 +263,32 @@ jobs:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-arm64-windows name: out-arm64-win
path: out path: out
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-x64-windows name: out-x64-win
path: out path: out
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-yml-windows name: out-yml-win
path: out path: out
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-arm64-macos name: out-arm64-mac
path: out path: out
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-x64-macos name: out-x64-mac
path: out path: out
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-yml-macos name: out-yml-mac
path: out path: out
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
@ -311,8 +310,8 @@ jobs:
env: env:
NOTES: ${{ needs.prepare-files.outputs.notes }} NOTES: ${{ needs.prepare-files.outputs.notes }}
PUB_DATE: ${{ github.event.repository.updated_at }} 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' }} WEBSITE_DIR: ${{ env.IS_NIGHTLY == 'true' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }}
URL_CODED_NAME: ${{ env.IS_STAGING == 'true' && 'Zoo%20Design%20Studio%20%28Staging%29' || 'Zoo%20Design%20Studio' }} URL_CODED_NAME: ${{ env.IS_NIGHTLY == 'true' && 'Zoo%20Design%20Studio%20%28Nightly%29' || 'Zoo%20Design%20Studio' }}
run: | run: |
RELEASE_DIR=https://${WEBSITE_DIR} RELEASE_DIR=https://${WEBSITE_DIR}
jq --null-input \ jq --null-input \
@ -361,26 +360,26 @@ jobs:
run: "ls -R out" run: "ls -R out"
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
if: ${{ env.IS_STAGING == 'true' }} if: ${{ env.IS_NIGHTLY == 'true' }}
uses: 'google-github-actions/auth@v2.1.8' uses: 'google-github-actions/auth@v2.1.8'
with: with:
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}' credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
- name: Set up Google Cloud SDK - 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 uses: google-github-actions/setup-gcloud@v2.1.4
with: with:
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }} project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
- name: Upload staging files to public bucket - name: Upload nightly files to public bucket
if: ${{ env.IS_STAGING == 'true' }} if: ${{ env.IS_NIGHTLY == 'true' }}
uses: google-github-actions/upload-cloud-storage@v2.2.2 uses: google-github-actions/upload-cloud-storage@v2.2.2
with: with:
path: out path: out
glob: '*' glob: '*'
parent: false 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 - name: Invalidate bucket cache on latest*.yml and last_download.json files
if: ${{ env.IS_STAGING == 'true' }} if: ${{ env.IS_NIGHTLY == 'true' }}
run: npm run files:invalidate-bucket:staging run: npm run files:invalidate-bucket:nightly

View File

@ -177,16 +177,16 @@ jobs:
TARGET: web TARGET: web
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} if: ${{ !cancelled() && (success() || failure()) }}
with: with:
name: playwright-report-snapshot-${{ github.sha }} name: playwright-report-ubuntu-snapshot-${{ github.sha }}
path: playwright-report/ path: playwright-report/
include-hidden-files: true include-hidden-files: true
retention-days: 30 retention-days: 30
overwrite: true overwrite: true
- name: Check diff - name: Check diff
if: ${{ always() && github.ref != 'refs/heads/main' }} if: ${{ github.ref != 'refs/heads/main' }}
shell: bash shell: bash
id: git-check id: git-check
run: | run: |
@ -197,7 +197,7 @@ jobs:
fi fi
- name: Commit changes - name: Commit changes
if: ${{ always() && steps.git-check.outputs.modified == 'true' }} if: ${{ steps.git-check.outputs.modified == 'true' }}
shell: bash shell: bash
run: | run: |
git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots git add e2e/playwright/snapshot-tests.spec.ts-snapshots e2e/playwright/snapshots
@ -220,7 +220,7 @@ jobs:
include: include:
- os: "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64" - os: "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64"
- os: namespace-profile-macos-8-cores - os: namespace-profile-macos-8-cores
- os: namespace-profile-windows-8-cores - os: windows-latest-8-cores
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
name: e2e:web (${{ contains(matrix.os, 'ubuntu') && 'ubuntu' || (contains(matrix.os, 'windows') && 'windows' || 'macos') }}) name: e2e:web (${{ contains(matrix.os, 'ubuntu') && 'ubuntu' || (contains(matrix.os, 'windows') && 'windows' || 'macos') }})
env: env:
@ -293,7 +293,6 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() && (success() || failure()) }} if: ${{ !cancelled() && (success() || failure()) }}
with: with:
name: playwright-report-web-${{ env.OS_NAME }}-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/ path: playwright-report/
include-hidden-files: true include-hidden-files: true
retention-days: 30 retention-days: 30
@ -305,6 +304,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# TODO: enable namespace-profile-windows-latest once available
include: include:
- os: "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64" - os: "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64"
shardIndex: 1 shardIndex: 1
@ -336,10 +336,10 @@ jobs:
- os: namespace-profile-macos-8-cores - os: namespace-profile-macos-8-cores
shardIndex: 2 shardIndex: 2
shardTotal: 2 shardTotal: 2
- os: namespace-profile-windows-8-cores - os: windows-latest-8-cores
shardIndex: 1 shardIndex: 1
shardTotal: 2 shardTotal: 2
- os: namespace-profile-windows-8-cores - os: windows-latest-8-cores
shardIndex: 2 shardIndex: 2
shardTotal: 2 shardTotal: 2
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@ -418,7 +418,7 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: 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/ path: test-results/
include-hidden-files: true include-hidden-files: true
retention-days: 30 retention-days: 30
@ -427,7 +427,7 @@ jobs:
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: 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/ path: playwright-report/
include-hidden-files: true include-hidden-files: true
retention-days: 30 retention-days: 30

View File

@ -31,42 +31,42 @@ jobs:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-arm64-windows name: out-arm64-win
path: out path: out
run-id: ${{ steps.tag_workflow_id.outputs.id }} run-id: ${{ steps.tag_workflow_id.outputs.id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-x64-windows name: out-x64-win
path: out path: out
run-id: ${{ steps.tag_workflow_id.outputs.id }} run-id: ${{ steps.tag_workflow_id.outputs.id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-yml-windows name: out-yml-win
path: out path: out
run-id: ${{ steps.tag_workflow_id.outputs.id }} run-id: ${{ steps.tag_workflow_id.outputs.id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-arm64-macos name: out-arm64-mac
path: out path: out
run-id: ${{ steps.tag_workflow_id.outputs.id }} run-id: ${{ steps.tag_workflow_id.outputs.id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-x64-macos name: out-x64-mac
path: out path: out
run-id: ${{ steps.tag_workflow_id.outputs.id }} run-id: ${{ steps.tag_workflow_id.outputs.id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: out-yml-macos name: out-yml-mac
path: out path: out
run-id: ${{ steps.tag_workflow_id.outputs.id }} run-id: ${{ steps.tag_workflow_id.outputs.id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -251,8 +251,7 @@ Before you submit a contribution PR to this repo, please ensure that:
#### 1. Create a 'Cut release $VERSION' issue #### 1. Create a 'Cut release $VERSION' issue
Use the **Release** issue template. It will be used to document changelog discussions and release testing.
This will be used to facilitate changelog discussions and release testing.
https://github.com/KittyCAD/modeling-app/issues/new 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 #### 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). 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 #### 4. Bump the KCL version

View File

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

View File

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 259 KiB

View File

@ -46,7 +46,3 @@ KCL has no support for area, volume, or other higher dimension units. When inter
## Explicit conversions ## 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. 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)`.

View File

@ -4,6 +4,8 @@ excerpt: "Project specific settings for the app. These live in `project.toml` in
layout: manual 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 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 ## Project Configuration Structure

View File

@ -4,6 +4,8 @@ excerpt: "User specific settings for the app. These live in `user.toml` in the a
layout: manual 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 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 ## User Configuration Structure

File diff suppressed because one or more lines are too long

33
docs/kcl-std/consts.md Normal file
View 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)

View File

@ -18,7 +18,7 @@ E: number = 2.71828182845904523536028747135266250_
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 30deg, length = 2 * E ^ 2) |> angledLine(angle = 30, length = 2 * E ^ 2)
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -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 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. 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)`. 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 You might prefer to use `units::toRadians` or `units::toDegrees` to convert between angles with
different units where possible. different units.
### Examples ### Examples

View File

@ -18,7 +18,7 @@ TAU: number = 6.28318530717958647692528676655900577_
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 50deg, length = 10 * TAU) |> angledLine(angle = 50, length = 10 * TAU)
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -8,7 +8,7 @@ layout: manual
No turn, zero degrees/radians. No turn, zero degrees/radians.
```kcl ```kcl
turns::ZERO turns::ZERO: number = 0
``` ```

View File

@ -27,7 +27,7 @@ abs(@input: number): number
### Examples ### Examples
```kcl ```kcl
myAngle = -120deg myAngle = -120
sketch001 = startSketchOn(XZ) sketch001 = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])

View File

@ -8,7 +8,7 @@ layout: manual
Compute the cosine of a number. Compute the cosine of a number.
```kcl ```kcl
cos(@num: number(Angle)): number cos(@num: number(Angle)): number(_)
``` ```
@ -21,7 +21,7 @@ cos(@num: number(Angle)): number
### Returns ### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number. [`number(_)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples
@ -29,7 +29,7 @@ cos(@num: number(Angle)): number
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 30deg, length = 3 / cos(30deg)) |> angledLine(angle = 30, length = 3 / cos(30deg))
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -29,7 +29,7 @@ max(@input: [number; 1+]): number
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> 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]) |> line(end = [20, 0])
|> close() |> close()

View File

@ -29,7 +29,7 @@ min(@input: [number; 1+]): number
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> 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]) |> line(end = [20, 0])
|> close() |> close()

View File

@ -33,7 +33,7 @@ polar(
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> 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 = [0, 5])
|> line(end = [segEndX(thing), 0]) |> line(end = [segEndX(thing), 0])
|> line(end = [-20, 10]) |> line(end = [-20, 10])

View File

@ -33,7 +33,7 @@ pow(
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 50deg, length = pow(5, exp = 2)) |> angledLine(angle = 50, length = pow(5, exp = 2))
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -8,7 +8,7 @@ layout: manual
Compute the sine of a number. Compute the sine of a number.
```kcl ```kcl
sin(@num: number(Angle)): number sin(@num: number(Angle)): number(_)
``` ```
@ -21,7 +21,7 @@ sin(@num: number(Angle)): number
### Returns ### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number. [`number(_)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples
@ -29,7 +29,7 @@ sin(@num: number(Angle)): number
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 50deg, length = 15 / sin(135deg)) |> angledLine(angle = 50, length = 15 / sin(135deg))
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -29,7 +29,7 @@ sqrt(@input: number): number
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 50deg, length = sqrt(2500)) |> angledLine(angle = 50, length = sqrt(2500))
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -8,7 +8,7 @@ layout: manual
Compute the tangent of a number. Compute the tangent of a number.
```kcl ```kcl
tan(@num: number(Angle)): number tan(@num: number(Angle)): number(_)
``` ```
@ -21,7 +21,7 @@ tan(@num: number(Angle)): number
### Returns ### Returns
[`number`](/docs/kcl-std/types/std-types-number) - A number. [`number(_)`](/docs/kcl-std/types/std-types-number) - A number.
### Examples ### Examples
@ -29,7 +29,7 @@ tan(@num: number(Angle)): number
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> 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) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -46,7 +46,7 @@ angledLine(
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> yLine(endAbsolute = 15) |> yLine(endAbsolute = 15)
|> angledLine(angle = 30deg, length = 15) |> angledLine(angle = 30, length = 15)
|> line(end = [8, -10]) |> line(end = [8, -10])
|> yLine(endAbsolute = 0) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -42,7 +42,7 @@ exampleSketch = startSketchOn(XZ)
|> line(endAbsolute = [5, 10]) |> line(endAbsolute = [5, 10])
|> line(endAbsolute = [-10, 10], tag = $lineToIntersect) |> line(endAbsolute = [-10, 10], tag = $lineToIntersect)
|> line(endAbsolute = [0, 20]) |> line(endAbsolute = [0, 20])
|> angledLineThatIntersects(angle = 80deg, intersectTag = lineToIntersect, offset = 10) |> angledLineThatIntersects(angle = 80, intersectTag = lineToIntersect, offset = 10)
|> close() |> close()
example = extrude(exampleSketch, length = 10) example = extrude(exampleSketch, length = 10)

View File

@ -53,7 +53,7 @@ for to construct your shape, you're likely looking for tangentialArc.
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [10, 0]) |> line(end = [10, 0])
|> arc(angleStart = 0, angleEnd = 280deg, radius = 16) |> arc(angleStart = 0, angleEnd = 280, radius = 16)
|> close() |> close()
example = extrude(exampleSketch, length = 10) example = extrude(exampleSketch, length = 10)

View File

@ -43,7 +43,7 @@ extruded in the same direction.
example = startSketchOn(XZ) example = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [10, 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 = [5, 0])
|> line(end = [0, 10]) |> line(end = [0, 10])
|> bezierCurve(control1 = [-10, 0], control2 = [2, 10], end = [-5, 10]) |> bezierCurve(control1 = [-10, 0], control2 = [2, 10], end = [-5, 10])
@ -58,7 +58,7 @@ example = startSketchOn(XZ)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [-10, 0]) |> startProfile(at = [-10, 0])
|> arc(angleStart = 120deg, angleEnd = -60deg, radius = 5) |> arc(angleStart = 120, angleEnd = -60, radius = 5)
|> line(end = [10, 0]) |> line(end = [10, 0])
|> line(end = [5, 0]) |> line(end = [5, 0])
|> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10]) |> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10])
@ -75,7 +75,7 @@ example = extrude(exampleSketch, length = 10)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [-10, 0]) |> startProfile(at = [-10, 0])
|> arc(angleStart = 120deg, angleEnd = -60deg, radius = 5) |> arc(angleStart = 120, angleEnd = -60, radius = 5)
|> line(end = [10, 0]) |> line(end = [10, 0])
|> line(end = [5, 0]) |> line(end = [5, 0])
|> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10]) |> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10])
@ -92,7 +92,7 @@ example = extrude(exampleSketch, length = 20, symmetric = true)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [-10, 0]) |> startProfile(at = [-10, 0])
|> arc(angleStart = 120deg, angleEnd = -60deg, radius = 5) |> arc(angleStart = 120, angleEnd = -60, radius = 5)
|> line(end = [10, 0]) |> line(end = [10, 0])
|> line(end = [5, 0]) |> line(end = [5, 0])
|> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10]) |> bezierCurve(control1 = [-3, 0], control2 = [2, 10], end = [-5, 10])

View File

@ -30,10 +30,10 @@ getNextAdjacentEdge(@edge: tag): Edge
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [10, 0]) |> line(end = [10, 0])
|> angledLine(angle = 60deg, length = 10) |> angledLine(angle = 60, length = 10)
|> angledLine(angle = 120deg, length = 10) |> angledLine(angle = 120, length = 10)
|> line(end = [-10, 0]) |> line(end = [-10, 0])
|> angledLine(angle = 240deg, length = 10, tag = $referenceEdge) |> angledLine(angle = 240, length = 10, tag = $referenceEdge)
|> close() |> close()
example = extrude(exampleSketch, length = 5) example = extrude(exampleSketch, length = 5)

View File

@ -30,10 +30,10 @@ getOppositeEdge(@edge: tag): Edge
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [10, 0]) |> line(end = [10, 0])
|> angledLine(angle = 60deg, length = 10) |> angledLine(angle = 60, length = 10)
|> angledLine(angle = 120deg, length = 10) |> angledLine(angle = 120, length = 10)
|> line(end = [-10, 0]) |> line(end = [-10, 0])
|> angledLine(angle = 240deg, length = 10, tag = $referenceEdge) |> angledLine(angle = 240, length = 10, tag = $referenceEdge)
|> close() |> close()
example = extrude(exampleSketch, length = 5) example = extrude(exampleSketch, length = 5)

View File

@ -30,10 +30,10 @@ getPreviousAdjacentEdge(@edge: tag): Edge
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> line(end = [10, 0]) |> line(end = [10, 0])
|> angledLine(angle = 60deg, length = 10) |> angledLine(angle = 60, length = 10)
|> angledLine(angle = 120deg, length = 10) |> angledLine(angle = 120, length = 10)
|> line(end = [-10, 0]) |> line(end = [-10, 0])
|> angledLine(angle = 240deg, length = 10, tag = $referenceEdge) |> angledLine(angle = 240, length = 10, tag = $referenceEdge)
|> close() |> close()
example = extrude(exampleSketch, length = 5) example = extrude(exampleSketch, length = 5)

View File

@ -43,11 +43,11 @@ a = 10
b = 14 b = 14
startSketchOn(XZ) startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> involuteCircular(startRadius = a, endRadius = b, angle = 60deg) |> involuteCircular(startRadius = a, endRadius = b, angle = 60)
|> involuteCircular( |> involuteCircular(
startRadius = a, startRadius = a,
endRadius = b, endRadius = b,
angle = 60deg, angle = 60,
reverse = true, reverse = true,
) )

View File

@ -30,7 +30,7 @@ profileStart(@profile: Sketch): Point2d
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> startProfile(at = [5, 2]) |> startProfile(at = [5, 2])
|> angledLine(angle = 120, length = 50, tag = $seg01) |> angledLine(angle = 120, length = 50, tag = $seg01)
|> angledLine(angle = segAng(seg01) + 120deg, length = 50) |> angledLine(angle = segAng(seg01) + 120, length = 50)
|> line(end = profileStart(%)) |> line(end = profileStart(%))
|> close() |> close()
|> extrude(length = 20) |> extrude(length = 20)

View File

@ -30,8 +30,8 @@ profileStartX(@profile: Sketch): number(Length)
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> startProfile(at = [5, 2]) |> startProfile(at = [5, 2])
|> angledLine(angle = -26.6, length = 50) |> angledLine(angle = -26.6, length = 50)
|> angledLine(angle = 90deg, length = 50) |> angledLine(angle = 90, length = 50)
|> angledLine(angle = 30deg, endAbsoluteX = profileStartX(%)) |> angledLine(angle = 30, endAbsoluteX = profileStartX(%))
``` ```

View File

@ -29,8 +29,8 @@ profileStartY(@profile: Sketch): number(Length)
```kcl ```kcl
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> startProfile(at = [5, 2]) |> startProfile(at = [5, 2])
|> angledLine(angle = -60deg, length = 14) |> angledLine(angle = -60, length = 14)
|> angledLine(angle = 30deg, endAbsoluteY = profileStartY(%)) |> angledLine(angle = 30, endAbsoluteY = profileStartY(%))
``` ```

View File

@ -62,7 +62,7 @@ part001 = startSketchOn(XY)
|> line(end = [0, -5.5]) |> line(end = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> 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. // A donut shape.
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> circle(center = [15, 0], radius = 5) |> 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 = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> 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 = [0, -5.5])
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
|> revolve(axis = Y, angle = 180deg) |> revolve(axis = Y, angle = 180)
part002 = startSketchOn(part001, face = END) part002 = startSketchOn(part001, face = END)
|> startProfile(at = [4.5, -5]) |> startProfile(at = [4.5, -5])
@ -131,7 +131,7 @@ box = startSketchOn(XY)
sketch001 = startSketchOn(box, face = END) sketch001 = startSketchOn(box, face = END)
|> circle(center = [10, 10], radius = 4) |> 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) sketch001 = startSketchOn(box, face = END)
|> circle(center = [10, 10], radius = 4) |> 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) sketch001 = startSketchOn(box, face = END)
|> circle(center = [10, 10], radius = 4) |> 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) sketch001 = startSketchOn(XY)
|> circle(center = [-10, 10], radius = 4) |> 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) sketch001 = startSketchOn(XY)
|> circle(center = [-10, 10], radius = 4) |> 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) sketch001 = startSketchOn(XY)
|> circle(center = [-10, 10], radius = 4) |> 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) sketch001 = startSketchOn(XY)
|> circle(center = [-10, 10], radius = 4) |> circle(center = [-10, 10], radius = 4)
|> revolve(angle = 90deg, axis = revolveAxis, bidirectionalAngle = 50) |> revolve(angle = 90, axis = revolveAxis, bidirectionalAngle = 50)
``` ```

View File

@ -30,8 +30,8 @@ segLen(@tag: tag): number(Length)
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 60, length = 10, tag = $thing) |> angledLine(angle = 60, length = 10, tag = $thing)
|> tangentialArc(angle = -120deg, radius = 5) |> tangentialArc(angle = -120, radius = 5)
|> angledLine(angle = -60deg, length = segLen(thing)) |> angledLine(angle = -60, length = segLen(thing))
|> close() |> close()
example = extrude(exampleSketch, length = 5) example = extrude(exampleSketch, length = 5)

View File

@ -156,7 +156,7 @@ exampleSketch = startSketchOn(XY)
|> line(end = [-2, 0]) |> line(end = [-2, 0])
|> close() |> close()
example = revolve(exampleSketch, axis = Y, angle = 180deg) example = revolve(exampleSketch, axis = Y, angle = 180)
exampleSketch002 = startSketchOn(example, face = END) exampleSketch002 = startSketchOn(example, face = END)
|> startProfile(at = [4.5, -5]) |> startProfile(at = [4.5, -5])
@ -189,7 +189,7 @@ exampleSketch = startSketchOn(XY)
example = revolve( example = revolve(
exampleSketch, exampleSketch,
axis = Y, axis = Y,
angle = 180deg, angle = 180,
tagEnd = $end01, tagEnd = $end01,
) )

View File

@ -54,9 +54,9 @@ swept along the same path.
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90deg, radius = 5) |> tangentialArc(angle = 90, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90deg, radius = 5) |> tangentialArc(angle = -90, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -101,7 +101,7 @@ springSketch = startSketchOn(XZ)
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
rectangleSketch = startProfile(sketch001, at = [-200, 23.86]) rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
|> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001) |> 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)) |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
@ -111,7 +111,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
sketch002 = startSketchOn(YZ) sketch002 = startSketchOn(YZ)
sweepPath = startProfile(sketch002, at = [0, 0]) sweepPath = startProfile(sketch002, at = [0, 0])
|> yLine(length = 231.81) |> yLine(length = 231.81)
|> tangentialArc(radius = 80, angle = -90deg) |> tangentialArc(radius = 80, angle = -90)
|> xLine(length = 384.93) |> xLine(length = 384.93)
sweep([rectangleSketch, circleSketch], path = sweepPath) sweep([rectangleSketch, circleSketch], path = sweepPath)
@ -130,7 +130,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
sketch002 = startSketchOn(YZ) sketch002 = startSketchOn(YZ)
sweepPath = startProfile(sketch002, at = [0, 0]) sweepPath = startProfile(sketch002, at = [0, 0])
|> yLine(length = 231.81) |> yLine(length = 231.81)
|> tangentialArc(radius = 80, angle = -90deg) |> tangentialArc(radius = 80, angle = -90)
|> xLine(length = 384.93) |> xLine(length = 384.93)
sweep(circleSketch, path = sweepPath, sectional = true) sweep(circleSketch, path = sweepPath, sectional = true)

View File

@ -47,7 +47,7 @@ for 'angle' degrees along the imaginary circle.
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 45deg, length = 10) |> angledLine(angle = 45, length = 10)
|> tangentialArc(end = [0, -10]) |> tangentialArc(end = [0, -10])
|> line(end = [-10, 0]) |> line(end = [-10, 0])
|> close() |> close()
@ -61,7 +61,7 @@ example = extrude(exampleSketch, length = 10)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 60deg, length = 10) |> angledLine(angle = 60, length = 10)
|> tangentialArc(endAbsolute = [15, 15]) |> tangentialArc(endAbsolute = [15, 15])
|> line(end = [10, -15]) |> line(end = [10, -15])
|> close() |> close()
@ -75,9 +75,9 @@ example = extrude(exampleSketch, length = 10)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> angledLine(angle = 60deg, length = 10) |> angledLine(angle = 60, length = 10)
|> tangentialArc(radius = 10, angle = -120deg) |> tangentialArc(radius = 10, angle = -120)
|> angledLine(angle = -60deg, length = 10) |> angledLine(angle = -60, length = 10)
|> close() |> close()
example = extrude(exampleSketch, length = 10) example = extrude(exampleSketch, length = 10)

View File

@ -38,10 +38,10 @@ xLine(
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> xLine(length = 15) |> xLine(length = 15)
|> angledLine(angle = 80deg, length = 15) |> angledLine(angle = 80, length = 15)
|> line(end = [8, -10]) |> line(end = [8, -10])
|> xLine(length = 10) |> xLine(length = 10)
|> angledLine(angle = 120deg, length = 30) |> angledLine(angle = 120, length = 30)
|> xLine(length = -15) |> xLine(length = -15)
|> close() |> close()

View File

@ -38,7 +38,7 @@ yLine(
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> startProfile(at = [0, 0])
|> yLine(length = 15) |> yLine(length = 15)
|> angledLine(angle = 30deg, length = 15) |> angledLine(angle = 30, length = 15)
|> line(end = [8, -10]) |> line(end = [8, -10])
|> yLine(length = -5) |> yLine(length = -5)
|> close() |> close()

View File

@ -55,7 +55,7 @@ example = extrude(exampleSketch, length = 5)
// Add color to a revolved solid. // Add color to a revolved solid.
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
|> circle(center = [15, 0], radius = 5) |> circle(center = [15, 0], radius = 5)
|> revolve(angle = 360deg, axis = Y) |> revolve(angle = 360, axis = Y)
|> appearance(color = '#ff0000', metalness = 90, roughness = 90) |> appearance(color = '#ff0000', metalness = 90, roughness = 90)
``` ```
@ -196,9 +196,9 @@ example = extrude(exampleSketch, length = 1)
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90deg, radius = 5) |> tangentialArc(angle = 90, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90deg, radius = 5) |> tangentialArc(angle = -90, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
pipeHole = startSketchOn(XY) pipeHole = startSketchOn(XY)

View File

@ -50,7 +50,7 @@ Its properties are:
- `rotation.axis` (a 3D point, defaults to the Z axis) - `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") - `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) pow(0.9, exp = i)
], ],
// Turn by 15 degrees each time. // 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 { return {
translate = [0, 0, -i * width], translate = [0, 0, -i * width],
rotation = { rotation = {
angle = 90deg * i, angle = 90 * i,
// Rotate around the overall scene's origin. // Rotate around the overall scene's origin.
origin = "global" origin = "global"
} }
@ -219,7 +219,7 @@ fn transform(@i) {
// Transform functions can return multiple transforms. They'll be applied in order. // Transform functions can return multiple transforms. They'll be applied in order.
return [ return [
{ translate = [30 * i, 0, 0] }, { translate = [30 * i, 0, 0] },
{ rotation = { angle = 45deg * i } } { rotation = { angle = 45 * i } }
] ]
} }
startSketchOn(XY) startSketchOn(XY)

View File

@ -72,9 +72,9 @@ rotation.
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90deg, radius = 5) |> tangentialArc(angle = 90, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90deg, radius = 5) |> tangentialArc(angle = -90, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -98,9 +98,9 @@ sweepSketch = startSketchOn(XY)
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90deg, radius = 5) |> tangentialArc(angle = 90, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90deg, radius = 5) |> tangentialArc(angle = -90, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -124,9 +124,9 @@ sweepSketch = startSketchOn(XY)
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90deg, radius = 5) |> tangentialArc(angle = 90, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90deg, radius = 5) |> tangentialArc(angle = -90, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -137,7 +137,7 @@ sweepSketch = startSketchOn(XY)
|> circle(center = [0, 0], radius = 2) |> circle(center = [0, 0], radius = 2)
|> subtract2d(tool = pipeHole) |> subtract2d(tool = pipeHole)
|> sweep(path = sweepPath) |> 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 import "tests/inputs/cube.sldprt" as cube
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) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90deg, radius = 5) |> tangentialArc(angle = 90, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90deg, radius = 5) |> tangentialArc(angle = -90, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -176,7 +176,7 @@ sweepSketch = startSketchOn(XY)
|> circle(center = [0, 0], radius = 2) |> circle(center = [0, 0], radius = 2)
|> subtract2d(tool = pipeHole) |> subtract2d(tool = pipeHole)
|> sweep(path = sweepPath) |> 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) sketch002 = startSketchOn(YZ)
sweepPath = startProfile(sketch002, at = [0, 0]) sweepPath = startProfile(sketch002, at = [0, 0])
|> yLine(length = 231.81) |> yLine(length = 231.81)
|> tangentialArc(radius = 80, angle = -90deg) |> tangentialArc(radius = 80, angle = -90)
|> xLine(length = 384.93) |> xLine(length = 384.93)
parts = sweep([rectangleSketch, circleSketch], path = sweepPath) parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
// Rotate the sweeps. // 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() profile002 = square()
|> translate(x = 0, y = 0, z = 20) |> 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]) loft([profile001, profile002])

View File

@ -54,9 +54,9 @@ look like the model moves and gets bigger at the same time. Say you have a squar
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90deg, radius = 5) |> tangentialArc(angle = 90, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90deg, radius = 5) |> tangentialArc(angle = -90, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -93,7 +93,7 @@ cube
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
rectangleSketch = startProfile(sketch001, at = [-200, 23.86]) rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
|> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001) |> 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)) |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
@ -103,7 +103,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
sketch002 = startSketchOn(YZ) sketch002 = startSketchOn(YZ)
sweepPath = startProfile(sketch002, at = [0, 0]) sweepPath = startProfile(sketch002, at = [0, 0])
|> yLine(length = 231.81) |> yLine(length = 231.81)
|> tangentialArc(radius = 80, angle = -90deg) |> tangentialArc(radius = 80, angle = -90)
|> xLine(length = 384.93) |> xLine(length = 384.93)
parts = sweep([rectangleSketch, circleSketch], path = sweepPath) parts = sweep([rectangleSketch, circleSketch], path = sweepPath)

View File

@ -47,9 +47,9 @@ and then rotate it using the `rotate` function to create a loft.
sweepPath = startSketchOn(XZ) sweepPath = startSketchOn(XZ)
|> startProfile(at = [0.05, 0.05]) |> startProfile(at = [0.05, 0.05])
|> line(end = [0, 7]) |> line(end = [0, 7])
|> tangentialArc(angle = 90deg, radius = 5) |> tangentialArc(angle = 90, radius = 5)
|> line(end = [-3, 0]) |> line(end = [-3, 0])
|> tangentialArc(angle = -90deg, radius = 5) |> tangentialArc(angle = -90, radius = 5)
|> line(end = [0, 7]) |> line(end = [0, 7])
// Create a hole for the pipe. // Create a hole for the pipe.
@ -91,7 +91,7 @@ cube
sketch001 = startSketchOn(XY) sketch001 = startSketchOn(XY)
rectangleSketch = startProfile(sketch001, at = [-200, 23.86]) rectangleSketch = startProfile(sketch001, at = [-200, 23.86])
|> angledLine(angle = 0, length = 73.47, tag = $rectangleSegmentA001) |> 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)) |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)]) |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close() |> close()
@ -101,7 +101,7 @@ circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
sketch002 = startSketchOn(YZ) sketch002 = startSketchOn(YZ)
sweepPath = startProfile(sketch002, at = [0, 0]) sweepPath = startProfile(sketch002, at = [0, 0])
|> yLine(length = 231.81) |> yLine(length = 231.81)
|> tangentialArc(radius = 80, angle = -90deg) |> tangentialArc(radius = 80, angle = -90)
|> xLine(length = 384.93) |> xLine(length = 384.93)
parts = sweep([rectangleSketch, circleSketch], path = sweepPath) parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
@ -162,7 +162,7 @@ profile001 = square()
profile002 = square() profile002 = square()
|> translate(z = 20) |> translate(z = 20)
|> rotate(axis = [0, 0, 1.0], angle = 45deg) |> rotate(axis = [0, 0, 1.0], angle = 45)
loft([profile001, profile002]) loft([profile001, profile002])

View File

@ -29,7 +29,7 @@ units::toDegrees(@num: number(Angle)): number(deg)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> 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) |> yLine(endAbsolute = 0)
|> close() |> close()

View File

@ -29,7 +29,7 @@ units::toRadians(@num: number(Angle)): number(rad)
```kcl ```kcl
exampleSketch = startSketchOn(XZ) exampleSketch = startSketchOn(XZ)
|> startProfile(at = [0, 0]) |> 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) |> yLine(endAbsolute = 0)
|> close() |> close()

File diff suppressed because one or more lines are too long

View File

@ -17,7 +17,7 @@ startSketchOn(XZ)
|> startProfile(at = origin) |> startProfile(at = origin)
|> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001) |> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001)
|> angledLine( |> angledLine(
angle = segAng(rectangleSegmentA001) - 90deg, angle = segAng(rectangleSegmentA001) - 90,
length = 196.99, length = 196.99,
tag = $rectangleSegmentB001, tag = $rectangleSegmentB001,
) )
@ -80,7 +80,7 @@ fn rect(origin) {
|> startProfile(at = origin) |> startProfile(at = origin)
|> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001) |> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001)
|> angledLine( |> angledLine(
angle = segAng(rectangleSegmentA001) - 90deg, angle = segAng(rectangleSegmentA001) - 90,
length = 196.99 length = 196.99
tag = $rectangleSegmentB001, tag = $rectangleSegmentB001,
) )

View File

@ -302,7 +302,7 @@ test.describe('Command bar tests', () => {
// Assert that the an alternative variable name is chosen, // Assert that the an alternative variable name is chosen,
// since the default variable name is already in use (distance) // 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( await expect(page.getByPlaceholder('Variable name')).toHaveValue(
'length001' 'length001'
) )

View File

@ -281,7 +281,10 @@ test.describe('Feature Tree pane', () => {
await test.step('Add a named constant for distance argument and submit', async () => { await test.step('Add a named constant for distance argument and submit', async () => {
await expect(cmdBar.currentArgumentInput).toBeVisible() 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.progressCmdBar()
await cmdBar.expectState({ await cmdBar.expectState({
stage: 'review', stage: 'review',

View File

@ -172,10 +172,6 @@ export class CmdBarFixture {
return this.page.getByTestId('cmd-bar-arg-value') return this.page.getByTestId('cmd-bar-arg-value')
} }
get variableCheckbox() {
return this.page.getByTestId('cmd-bar-variable-checkbox')
}
get cmdOptions() { get cmdOptions() {
return this.page.getByTestId('cmd-bar-option') return this.page.getByTestId('cmd-bar-option')
} }
@ -195,7 +191,7 @@ export class CmdBarFixture {
* Clicks the Create new variable button for kcl input * Clicks the Create new variable button for kcl input
*/ */
createNewVariable = async () => { createNewVariable = async () => {
await this.variableCheckbox.click() await this.page.getByRole('button', { name: 'Create new variable' }).click()
} }
/** /**

View File

@ -3807,7 +3807,7 @@ sketch002 = startSketchOn(extrude001, face = rectangleSegmentA001)
stage: 'arguments', stage: 'arguments',
}) })
await page.keyboard.insertText(newAngle) 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( await expect(page.getByPlaceholder('Variable name')).toHaveValue(
'angle001' 'angle001'
) )

View File

@ -170,7 +170,7 @@ test(
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
const crypticErrorText = 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() await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared. // black pixel means the scene has been cleared.
@ -369,7 +369,7 @@ test(
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
const crypticErrorText = 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() await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared. // black pixel means the scene has been cleared.
@ -408,7 +408,7 @@ test(
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
const crypticErrorText = 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() await expect(page.getByText(crypticErrorText).first()).toBeVisible()
} }
) )

View File

@ -1,54 +1,45 @@
import { expect, test } from '@e2e/playwright/zoo-test' 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.describe('Share link tests', () => {
test( ;[
`Open in desktop app with 2000-long code works non-Windows`, {
{ tag: ['@web', '@macos', '@linux'] }, codeLength: 1000,
async ({ page }) => { showsErrorOnWindows: false,
test.skip(process.platform === 'win32') },
const codeLength = 2000 {
await navigateAndClickOpenInDesktopApp(page, codeLength) codeLength: 2000,
await expect(getToastError(page)).not.toBeVisible() 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( const code = Array(codeLength).fill('0').join('')
`Open in desktop app with 1000-long code works on Windows`, const targetURL = `?create-file=true&browser=test&code=${code}&ask-open-desktop=true`
{ tag: ['@web', '@windows'] }, expect(targetURL.length).toEqual(codeLength + 58)
async ({ page }) => { await page.goto(page.url() + targetURL)
test.skip(process.platform !== 'win32') expect(page.url()).toContain(targetURL)
const codeLength = 1000 const button = page.getByRole('button', { name: 'Open in desktop app' })
await navigateAndClickOpenInDesktopApp(page, codeLength) await button.click()
await expect(getToastError(page)).not.toBeVisible() const toastError = page.getByText(
} 'The URL is too long to open in the desktop app on Windows'
) )
if (isWindows && showsErrorOnWindows) {
test( await expect(toastError).toBeVisible()
`Open in desktop app with 2000-long code doesn't work on Windows`, } else {
{ tag: ['@web', '@windows'] }, await expect(toastError).not.toBeVisible()
async ({ page }) => { // TODO: check if we could verify the deep link dialog shows up
test.skip(process.platform !== 'win32') }
const codeLength = 2000 }
await navigateAndClickOpenInDesktopApp(page, codeLength) )
await expect(getToastError(page)).toBeVisible() })
}
)
}) })

View File

@ -3433,71 +3433,6 @@ profile003 = startProfile(sketch002, at = [-201.08, 254.17])
).toBeVisible() ).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 ({ test('empty draft sketch is cleaned up properly', async ({
scene, scene,
toolbar, toolbar,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -1198,174 +1198,3 @@ export async function enableConsoleLogEverything({
console.log(`[Main] ${msg.type()}: ${msg.text()}`) 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')
}

View File

@ -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
View File

@ -47,7 +47,6 @@
"diff": "^7.0.0", "diff": "^7.0.0",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"hammerjs": "^2.0.8",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"json-rpc-2.0": "^1.6.0", "json-rpc-2.0": "^1.6.0",
@ -94,7 +93,6 @@
"@testing-library/react": "^15.0.7", "@testing-library/react": "^15.0.7",
"@types/diff": "^7.0.2", "@types/diff": "^7.0.2",
"@types/electron": "^1.6.10", "@types/electron": "^1.6.10",
"@types/hammerjs": "^2.0.46",
"@types/isomorphic-fetch": "^0.0.39", "@types/isomorphic-fetch": "^0.0.39",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/minimist": "^1.2.5", "@types/minimist": "^1.2.5",
@ -7493,13 +7491,6 @@
"@types/node": "*" "@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": { "node_modules/@types/http-cache-semantics": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "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": "^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": { "node_modules/happy-dom": {
"version": "17.4.4", "version": "17.4.4",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.4.4.tgz", "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.4.4.tgz",

View File

@ -49,7 +49,6 @@
"diff": "^7.0.0", "diff": "^7.0.0",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"hammerjs": "^2.0.8",
"html2canvas-pro": "^1.5.8", "html2canvas-pro": "^1.5.8",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"json-rpc-2.0": "^1.6.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", "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-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
"files:set-notes": "./scripts/set-files-notes.sh", "files:set-notes": "./scripts/set-files-notes.sh",
"files:flip-to-staging": "./scripts/flip-files-to-staging.sh", "files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
"files:flip-to-staging:windows": "powershell -ExecutionPolicy Bypass -File ./scripts/flip-files-to-staging.ps1", "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": "./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", "postinstall": "electron-rebuild",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts", "generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"tron:start": "electron-forge start", "tron:start": "electron-forge start",
@ -169,7 +168,6 @@
"@testing-library/react": "^15.0.7", "@testing-library/react": "^15.0.7",
"@types/diff": "^7.0.2", "@types/diff": "^7.0.2",
"@types/electron": "^1.6.10", "@types/electron": "^1.6.10",
"@types/hammerjs": "^2.0.46",
"@types/isomorphic-fetch": "^0.0.39", "@types/isomorphic-fetch": "^0.0.39",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/minimist": "^1.2.5", "@types/minimist": "^1.2.5",

20
rust/Cargo.lock generated
View File

@ -1792,7 +1792,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-bumper" name = "kcl-bumper"
version = "0.1.81" version = "0.1.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1803,7 +1803,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-derive-docs" name = "kcl-derive-docs"
version = "0.1.81" version = "0.1.80"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1812,7 +1812,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-directory-test-macro" name = "kcl-directory-test-macro"
version = "0.1.81" version = "0.1.80"
dependencies = [ dependencies = [
"convert_case", "convert_case",
"proc-macro2", "proc-macro2",
@ -1822,7 +1822,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-language-server" name = "kcl-language-server"
version = "0.2.81" version = "0.2.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1843,7 +1843,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-language-server-release" name = "kcl-language-server-release"
version = "0.1.81" version = "0.1.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -1863,7 +1863,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-lib" name = "kcl-lib"
version = "0.2.81" version = "0.2.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"approx 0.5.1", "approx 0.5.1",
@ -1940,7 +1940,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-python-bindings" name = "kcl-python-bindings"
version = "0.3.81" version = "0.3.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"kcl-lib", "kcl-lib",
@ -1955,7 +1955,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-test-server" name = "kcl-test-server"
version = "0.1.81" version = "0.1.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"hyper 0.14.32", "hyper 0.14.32",
@ -1968,7 +1968,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-to-core" name = "kcl-to-core"
version = "0.1.81" version = "0.1.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1982,7 +1982,7 @@ dependencies = [
[[package]] [[package]]
name = "kcl-wasm-lib" name = "kcl-wasm-lib"
version = "0.1.81" version = "0.1.80"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bson", "bson",

View File

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

View File

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

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-directory-test-macro" name = "kcl-directory-test-macro"
description = "A tool for generating tests from a directory of kcl files" description = "A tool for generating tests from a directory of kcl files"
version = "0.1.81" version = "0.1.80"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

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

View File

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

View File

@ -1,7 +1,7 @@
[package] [package]
name = "kcl-lib" name = "kcl-lib"
description = "KittyCAD Language implementation and tools" description = "KittyCAD Language implementation and tools"
version = "0.2.81" version = "0.2.80"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app" repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -300,6 +300,12 @@ extrude001 = extrude(profile001, length = 4)
let first = &result.first().unwrap().2; let first = &result.first().unwrap().2;
let second = &result.last().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!( assert!(
first.artifact_graph.len() < second.artifact_graph.len(), first.artifact_graph.len() < second.artifact_graph.len(),
"Second should have all the artifacts of the first, plus more. first={:?}, second={:?}", "Second should have all the artifacts of the first, plus more. first={:?}, second={:?}",

View File

@ -1230,10 +1230,7 @@ secondSketch = startSketchOn(part001, face = '')
let result = execute_and_snapshot(code, None).await; let result = execute_and_snapshot(code, None).await;
let err = result.unwrap_err(); let err = result.unwrap_err();
let err = err.as_kcl_error().unwrap(); let err = err.as_kcl_error().unwrap();
assert_eq!( assert_eq!(err.message(), "face requires a value with type `tag`, but found string");
err.message(),
"face requires a value with type `tag`, but found a value with type `string`."
);
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
@ -1965,7 +1962,7 @@ someFunction('INVALID')
let err = err.as_kcl_error().unwrap(); let err = err.as_kcl_error().unwrap();
assert_eq!( assert_eq!(
err.message(), 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!( assert_eq!(
err.source_ranges(), err.source_ranges(),
@ -2090,7 +2087,7 @@ async fn kcl_test_better_type_names() {
}, },
None => todo!(), 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")] #[tokio::test(flavor = "multi_thread")]

View File

@ -508,9 +508,8 @@ impl EngineManager for EngineConnection {
.await?; .await?;
// Wait for the response. // Wait for the response.
let response_timeout = 300;
let current_time = std::time::Instant::now(); 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; let guard = self.socket_health.read().await;
if *guard == SocketHealth::Inactive { if *guard == SocketHealth::Inactive {
// Check if we have any pending errors. // Check if we have any pending errors.

View File

@ -135,10 +135,8 @@ pub struct KclErrorWithOutputs {
pub non_fatal: Vec<CompilationError>, pub non_fatal: Vec<CompilationError>,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub operations: Vec<Operation>, pub operations: Vec<Operation>,
// TODO: Remove this field. Doing so breaks the ts-rs output for some
// reason.
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub _artifact_commands: Vec<ArtifactCommand>, pub artifact_commands: Vec<ArtifactCommand>,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub artifact_graph: ArtifactGraph, pub artifact_graph: ArtifactGraph,
pub filenames: IndexMap<ModuleId, ModulePath>, pub filenames: IndexMap<ModuleId, ModulePath>,
@ -164,7 +162,7 @@ impl KclErrorWithOutputs {
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
operations, operations,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
_artifact_commands: artifact_commands, artifact_commands,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_graph, artifact_graph,
filenames, filenames,
@ -179,7 +177,7 @@ impl KclErrorWithOutputs {
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
operations: Default::default(), operations: Default::default(),
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
_artifact_commands: Default::default(), artifact_commands: Default::default(),
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_graph: Default::default(), artifact_graph: Default::default(),
filenames: Default::default(), filenames: Default::default(),
@ -783,7 +781,6 @@ impl Severity {
pub enum Tag { pub enum Tag {
Deprecated, Deprecated,
Unnecessary, Unnecessary,
UnknownNumericUnits,
None, None,
} }

View File

@ -45,6 +45,26 @@ pub struct ArtifactCommand {
pub command: ModelingCmd, 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<()>; pub type DummyPathToNode = Vec<()>;
fn serialize_dummy_path_to_node<S>(_path_to_node: &DummyPathToNode, serializer: S) -> Result<S::Ok, S::Error> fn serialize_dummy_path_to_node<S>(_path_to_node: &DummyPathToNode, serializer: S) -> Result<S::Ok, S::Error>

View File

@ -111,6 +111,8 @@ impl GlobalState {
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
operations: self.exec_state.artifacts.operations, operations: self.exec_state.artifacts.operations,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_commands: self.exec_state.artifacts.commands,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.exec_state.artifacts.graph, artifact_graph: self.exec_state.artifacts.graph,
errors: self.exec_state.errors, errors: self.exec_state.errors,
default_planes: ctx.engine.get_default_planes().read().await.clone(), default_planes: ctx.engine.get_default_planes().read().await.clone(),

View File

@ -2,9 +2,7 @@ use indexmap::IndexMap;
use serde::Serialize; use serde::Serialize;
use super::{types::NumericType, ArtifactId, KclValue}; use super::{types::NumericType, ArtifactId, KclValue};
#[cfg(feature = "artifact-graph")] use crate::{ModuleId, NodePath, SourceRange};
use crate::parsing::ast::types::{Node, Program};
use crate::{parsing::ast::types::ItemVisibility, ModuleId, NodePath, SourceRange};
/// A CAD modeling operation for display in the feature tree, AKA operations /// A CAD modeling operation for display in the feature tree, AKA operations
/// timeline. /// timeline.
@ -28,20 +26,6 @@ pub enum Operation {
is_error: bool, is_error: bool,
}, },
#[serde(rename_all = "camelCase")] #[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 { GroupBegin {
/// The details of the group. /// The details of the group.
group: Group, group: Group,
@ -53,36 +37,32 @@ pub enum Operation {
GroupEnd, 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 { impl Operation {
/// If the variant is `StdLibCall`, set the `is_error` field. /// If the variant is `StdLibCall`, set the `is_error` field.
pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) { pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) {
match self { match self {
Self::StdLibCall { ref mut is_error, .. } => *is_error = is_err, Self::StdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::VariableDeclaration { .. } | Self::GroupBegin { .. } | Self::GroupEnd => {} 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 => {}
} }
} }
} }

View File

@ -6,14 +6,13 @@ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{
annotations, annotations,
cad_op::OpKclValue,
fn_call::Args, fn_call::Args,
kcl_value::{FunctionSource, TypeDef}, kcl_value::{FunctionSource, TypeDef},
memory, memory,
state::ModuleState, state::ModuleState,
types::{NumericType, PrimitiveType, RuntimeType}, types::{NumericType, PrimitiveType, RuntimeType},
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, ModelingCmdMeta, ModuleArtifactState, BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, PlaneType, StatementKind,
Operation, PlaneType, StatementKind, TagIdentifier, TagIdentifier,
}, },
fmt, fmt,
modules::{ModuleId, ModulePath, ModuleRepr}, modules::{ModuleId, ModulePath, ModuleRepr},
@ -25,7 +24,7 @@ use crate::{
}, },
source_range::SourceRange, source_range::SourceRange,
std::args::TyF64, std::args::TyF64,
CompilationError, NodePath, CompilationError,
}; };
impl<'a> StatementKind<'a> { impl<'a> StatementKind<'a> {
@ -84,7 +83,7 @@ impl ExecutorContext {
preserve_mem: bool, preserve_mem: bool,
module_id: ModuleId, module_id: ModuleId,
path: &ModulePath, 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())); 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)); let mut local_state = ModuleState::new(path.clone(), exec_state.stack().memory.clone(), Some(module_id));
@ -109,16 +108,13 @@ impl ExecutorContext {
} else { } else {
exec_state.mut_stack().pop_env() 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); std::mem::swap(&mut exec_state.mod_local, &mut local_state);
local_state.artifacts }
} else {
Default::default()
};
crate::log::log(format!("leave {path}")); 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. /// Execute an AST's program.
@ -330,16 +326,6 @@ impl ExecutorContext {
.mut_stack() .mut_stack()
.add(var_name.clone(), rhs.clone(), source_range)?; .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. // Track exports.
if let ItemVisibility::Export = variable_declaration.visibility { if let ItemVisibility::Export = variable_declaration.visibility {
if matches!(body_type, BodyType::Root) { if matches!(body_type, BodyType::Root) {
@ -464,12 +450,12 @@ impl ExecutorContext {
if matches!(body_type, BodyType::Root) { if matches!(body_type, BodyType::Root) {
// Flush the batch queue. // Flush the batch queue.
exec_state self.engine
.flush_batch( .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 // 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. // and chamfers where the engine would otherwise eat the ID of the segments.
true, true,
SourceRange::new(program.end, program.end, program.module_id),
) )
.await?; .await?;
} }
@ -549,12 +535,12 @@ impl ExecutorContext {
let result = match &mut repr { let result = match &mut repr {
ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)), 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 ModuleRepr::Kcl(program, cache) => self
.exec_module_from_ast(program, module_id, &path, exec_state, source_range, false) .exec_module_from_ast(program, module_id, &path, exec_state, source_range, false)
.await .await
.map(|(val, er, items, module_artifacts)| { .map(|(val, er, items)| {
*cache = Some((val, er, items.clone(), module_artifacts.clone())); *cache = Some((val, er, items.clone()));
(er, items) (er, items)
}), }),
ModuleRepr::Foreign(geom, _) => Err(KclError::new_semantic(KclErrorDetails::new( ModuleRepr::Foreign(geom, _) => Err(KclError::new_semantic(KclErrorDetails::new(
@ -580,28 +566,28 @@ impl ExecutorContext {
let result = match &mut repr { let result = match &mut repr {
ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)), 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) => { ModuleRepr::Kcl(program, cached_items) => {
let result = self let result = self
.exec_module_from_ast(program, module_id, &path, exec_state, source_range, false) .exec_module_from_ast(program, module_id, &path, exec_state, source_range, false)
.await; .await;
match result { match result {
Ok((val, env, items, module_artifacts)) => { Ok((val, env, items)) => {
*cached_items = Some((val.clone(), env, items, module_artifacts)); *cached_items = Some((val.clone(), env, items));
Ok(val) Ok(val)
} }
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
ModuleRepr::Foreign(_, Some((imported, _))) => Ok(imported.clone()), ModuleRepr::Foreign(_, Some(imported)) => Ok(Some(imported.clone())),
ModuleRepr::Foreign(geom, cached) => { 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 .await
.map(|geom| Some(KclValue::ImportedGeometry(geom))); .map(|geom| Some(KclValue::ImportedGeometry(geom)));
match result { match result {
Ok(val) => { Ok(val) => {
*cached = Some((val.clone(), exec_state.mod_local.artifacts.clone())); *cached = val.clone();
Ok(val) Ok(val)
} }
Err(e) => Err(e), Err(e) => Err(e),
@ -623,7 +609,7 @@ impl ExecutorContext {
exec_state: &mut ExecState, exec_state: &mut ExecState,
source_range: SourceRange, source_range: SourceRange,
preserve_mem: bool, 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); exec_state.global.mod_loader.enter_module(path);
let result = self let result = self
.exec_module_body(program, exec_state, preserve_mem, module_id, path) .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()) let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into())
.map_err(|e| KclError::new_semantic(e.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(|_| { value.coerce(&ty, false, exec_state).map_err(|_| {
let suggestion = if ty == RuntimeType::length() { let suggestion = if ty == RuntimeType::length() {
", you might try coercing to a fully specified numeric type such as `number(mm)`" ", you might try coercing to a fully specified numeric type such as `number(mm)`"
@ -824,14 +806,9 @@ fn apply_ascription(
} else { } else {
"" ""
}; };
let ty_str = if let Some(ty) = value.principal_type() {
format!("(with type `{ty}`) ")
} else {
String::new()
};
KclError::new_semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"could not coerce {} {ty_str}to type `{ty}`{suggestion}", "could not coerce value of type {} to type {ty}{suggestion}",
value.human_friendly_type() value.human_friendly_type()
), ),
vec![source_range], vec![source_range],
@ -1041,13 +1018,14 @@ impl Node<MemberExpression> {
.map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned())))) .map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
.collect(), .collect(),
}), }),
(being_indexed, _, _) => Err(KclError::new_semantic(KclErrorDetails::new( (being_indexed, _, _) => {
format!( let t = being_indexed.human_friendly_type();
"Only arrays can be indexed, but you're trying to index {}", let article = article_for(&t);
being_indexed.human_friendly_type() Err(KclError::new_semantic(KclErrorDetails::new(
), format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
vec![self.clone().into()], vec![self.clone().into()],
))), )))
}
} }
} }
} }
@ -1175,7 +1153,7 @@ impl Node<BinaryExpression> {
KclValue::Number { value: l / r, meta, ty } KclValue::Number { value: l / r, meta, ty }
} }
BinaryOperator::Mod => { 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); self.warn_on_unknown(&ty, "Modulo of", exec_state);
KclValue::Number { value: l % r, meta, ty } 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) { fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) {
if ty == &NumericType::Unknown { if ty == &NumericType::Unknown {
let sr = self.as_source_range(); // TODO suggest how to fix this
exec_state.clear_units_warnings(&sr); exec_state.warn(CompilationError::err(
let mut err = CompilationError::err( self.as_source_range(),
sr, format!("{} numbers which have unknown or incompatible units.", verb),
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);
} }
} }
} }
@ -1778,7 +1753,7 @@ a = 42: string
let err = result.unwrap_err(); let err = result.unwrap_err();
assert!( assert!(
err.to_string() err.to_string()
.contains("could not coerce 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:?}" "Expected error but found {err:?}"
); );
@ -1789,7 +1764,7 @@ a = 42: Plane
let err = result.unwrap_err(); let err = result.unwrap_err();
assert!( assert!(
err.to_string() err.to_string()
.contains("could not coerce 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:?}" "Expected error but found {err:?}"
); );
@ -1800,7 +1775,7 @@ arr = [0]: [string]
let err = result.unwrap_err(); let err = result.unwrap_err();
assert!( assert!(
err.to_string().contains( 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:?}" "Expected error but found {err:?}"
); );
@ -1811,9 +1786,8 @@ mixedArr = [0, "a"]: [number(mm)]
let result = parse_execute(program).await; let result = parse_execute(program).await;
let err = result.unwrap_err(); let err = result.unwrap_err();
assert!( assert!(
err.to_string().contains( err.to_string()
"could not coerce an array of `number`, `string` (with type `[any; 2]`) to type `[number(mm)]`" .contains("could not coerce value of type array of number(default units), string with 2 values to type [number(mm)]"),
),
"Expected error but found {err:?}" "Expected error but found {err:?}"
); );
} }
@ -2118,19 +2092,4 @@ y = x: number(Length)"#;
assert_eq!(num.n, 2.0); assert_eq!(num.n, 2.0);
assert_eq!(num.ty, NumericType::mm()); 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);
}
} }

View File

@ -532,44 +532,6 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
Ok(()) 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 type_check_params_kw(
fn_name: Option<&str>, fn_name: Option<&str>,
fn_def: &FunctionDefinition<'_>, 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. // For optional args, passing None should be the same as not passing an arg.
if !(def.is_some() && matches!(arg.value, KclValue::KclNone { .. })) { if !(def.is_some() && matches!(arg.value, KclValue::KclNone { .. })) {
if let Some(ty) = ty { 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 arg.value = arg
.value .value
.coerce( .coerce(
&rty, &RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range).map_err(|e| KclError::new_semantic(e.into()))?,
true, true,
exec_state, exec_state,
) )
.map_err(|e| { .map_err(|e| {
let mut message = format!( let mut message = format!(
"{label} requires {}", "{label} requires a value with type `{}`, but found {}",
type_err_str(ty, &arg.value, &arg.source_range, exec_state), ty,
arg.value.human_friendly_type(),
); );
if let Some(ty) = e.explicit_coercion { 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. // 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(arg) = &mut args.unlabeled {
if let Some((_, Some(ty))) = &fn_def.input_arg { if let Some((_, Some(ty))) = &fn_def.input_arg {
let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range) arg.1.value = arg
.map_err(|e| KclError::new_semantic(e.into()))?; .1
arg.1.value = arg.1.value.coerce(&rty, true, exec_state).map_err(|_| { .value
KclError::new_semantic(KclErrorDetails::new( .coerce(
format!( &RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
"The input argument of {} requires {}", .map_err(|e| KclError::new_semantic(e.into()))?,
fn_name true,
.map(|n| format!("`{}`", n)) exec_state,
.unwrap_or_else(|| "this function".to_owned()), )
type_err_str(ty, &arg.1.value, &arg.1.source_range, exec_state), .map_err(|_| {
), KclError::new_semantic(KclErrorDetails::new(
vec![arg.1.source_range], 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 { } else if let Some((name, _)) = &fn_def.input_arg {
if let Some(arg) = args.labeled.get(name) { 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(|_| { let val = val.coerce(&ty, true, exec_state).map_err(|_| {
KclError::new_semantic(KclErrorDetails::new( KclError::new_semantic(KclErrorDetails::new(
format!( format!(
"This function requires its result to be {}", "This function requires its result to be of type `{}`, but found {}",
type_err_str(ret_ty, &val, &(&val).into(), exec_state) ty.human_friendly_type(),
val.human_friendly_type(),
), ),
ret_ty.as_source_ranges(), ret_ty.as_source_ranges(),
)) ))
@ -958,7 +928,7 @@ msg2 = makeMessage(prefix = 1, suffix = 3)"#;
let err = parse_execute(program).await.unwrap_err(); let err = parse_execute(program).await.unwrap_err();
assert_eq!( assert_eq!(
err.message(), 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)"
) )
} }
} }

View File

@ -15,10 +15,7 @@ use uuid::Uuid;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{annotations, typed_path::TypedPath, types::UnitLen, ExecState, ExecutorContext, ImportedGeometry},
annotations, typed_path::TypedPath, types::UnitLen, ExecState, ExecutorContext, ImportedGeometry,
ModelingCmdMeta,
},
fs::FileSystem, fs::FileSystem,
parsing::ast::types::{Annotation, Node}, parsing::ast::types::{Annotation, Node},
source_range::SourceRange, source_range::SourceRange,
@ -260,22 +257,15 @@ pub struct PreImportedGeometry {
pub source_range: SourceRange, pub source_range: SourceRange,
} }
pub async fn send_to_engine( pub async fn send_to_engine(pre: PreImportedGeometry, ctxt: &ExecutorContext) -> Result<ImportedGeometry, KclError> {
pre: PreImportedGeometry,
exec_state: &mut ExecState,
ctxt: &ExecutorContext,
) -> Result<ImportedGeometry, KclError> {
let imported_geometry = ImportedGeometry::new( let imported_geometry = ImportedGeometry::new(
pre.id, pre.id,
pre.command.files.iter().map(|f| f.path.to_string()).collect(), pre.command.files.iter().map(|f| f.path.to_string()).collect(),
vec![pre.source_range.into()], vec![pre.source_range.into()],
); );
exec_state ctxt.engine
.async_modeling_cmd( .async_modeling_cmd(pre.id, pre.source_range, &ModelingCmd::from(pre.command.clone()))
ModelingCmdMeta::with_id(ctxt, pre.source_range, pre.id),
&ModelingCmd::from(pre.command.clone()),
)
.await?; .await?;
Ok(imported_geometry) Ok(imported_geometry)

View File

@ -4,6 +4,7 @@ use anyhow::Result;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Serialize; use serde::Serialize;
use super::types::UnitType;
use crate::{ use crate::{
errors::KclErrorDetails, errors::KclErrorDetails,
execution::{ 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 /// Human readable type name used in error messages. Should not be relied
/// on for program logic. /// on for program logic.
pub(crate) fn human_friendly_type(&self) -> String { 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 { match self {
KclValue::Uuid { .. } => "a unique ID (uuid)".to_owned(), KclValue::Uuid { .. } => "Unique ID (uuid)",
KclValue::TagDeclarator(_) => "a tag declarator".to_owned(), KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "a tag identifier".to_owned(), KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::Solid { .. } => "a solid".to_owned(), KclValue::Solid { .. } => "Solid",
KclValue::Sketch { .. } => "a sketch".to_owned(), KclValue::Sketch { .. } => "Sketch",
KclValue::Helix { .. } => "a helix".to_owned(), KclValue::Helix { .. } => "Helix",
KclValue::ImportedGeometry(_) => "an imported geometry".to_owned(), KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "a function".to_owned(), KclValue::Function { .. } => "Function",
KclValue::Plane { .. } => "a plane".to_owned(), KclValue::Plane { .. } => "Plane",
KclValue::Face { .. } => "a face".to_owned(), KclValue::Face { .. } => "Face",
KclValue::Bool { .. } => "a boolean (`true` or `false`)".to_owned(), KclValue::Bool { .. } => "boolean (true/false value)",
KclValue::Number { KclValue::Number {
ty: NumericType::Unknown, ty: NumericType::Unknown,
.. ..
} => "a number with unknown units".to_owned(), } => "number(unknown units)",
KclValue::Number { KclValue::Number {
ty: NumericType::Known(units), ty: NumericType::Known(UnitType::Length(_)),
.. ..
} => format!("a number ({units})"), } => "number(Length)",
KclValue::Number { .. } => "a number".to_owned(), KclValue::Number {
KclValue::String { .. } => "a string".to_owned(), ty: NumericType::Known(UnitType::Angle(_)),
KclValue::Object { .. } => "an object".to_owned(), ..
KclValue::Module { .. } => "a module".to_owned(), } => "number(Angle)",
KclValue::Type { .. } => "a type".to_owned(), KclValue::Number { .. } => "number",
KclValue::KclNone { .. } => "none".to_owned(), KclValue::String { .. } => "string (text)",
KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => { KclValue::Tuple { .. } => "tuple (list)",
if value.is_empty() { KclValue::HomArray { .. } => "array (list)",
"an empty array".to_owned() KclValue::Object { .. } => "object",
} else { KclValue::Module { .. } => "module",
// A max of 3 is good because it's common to use 3D points. KclValue::Type { .. } => "type",
const MAX: usize = 3; KclValue::KclNone { .. } => "None",
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
}
}
} }
.to_owned()
} }
pub(crate) fn from_literal(literal: Node<Literal>, exec_state: &mut ExecState) -> Self { 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> { pub fn value_str(&self) -> Option<String> {
match self { match self {
KclValue::Bool { value, .. } => Some(format!("{value}")), KclValue::Bool { value, .. } => Some(format!("{value}")),
@ -668,8 +649,6 @@ impl From<GeometryWithImportedGeometry> for KclValue {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::exec::UnitType;
use super::*; use super::*;
#[test] #[test]
@ -679,21 +658,21 @@ mod tests {
ty: NumericType::Known(UnitType::Length(UnitLen::Unknown)), ty: NumericType::Known(UnitType::Length(UnitLen::Unknown)),
meta: vec![], 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 { let unknown = KclValue::Number {
value: 1.0, value: 1.0,
ty: NumericType::Unknown, ty: NumericType::Unknown,
meta: vec![], 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 { let mm = KclValue::Number {
value: 1.0, value: 1.0,
ty: NumericType::Known(UnitType::Length(UnitLen::Mm)), ty: NumericType::Known(UnitType::Length(UnitLen::Mm)),
meta: vec![], 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 { let array1_mm = KclValue::HomArray {
value: vec![mm.clone()], value: vec![mm.clone()],
@ -701,7 +680,7 @@ mod tests {
}; };
assert_eq!( assert_eq!(
array1_mm.human_friendly_type(), 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 { let array2_mm = KclValue::HomArray {
@ -710,7 +689,7 @@ mod tests {
}; };
assert_eq!( assert_eq!(
array2_mm.human_friendly_type(), 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 { let array3_mm = KclValue::HomArray {
@ -719,7 +698,7 @@ mod tests {
}; };
assert_eq!( assert_eq!(
array3_mm.human_friendly_type(), 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 { let inches = KclValue::Number {
@ -733,14 +712,14 @@ mod tests {
}; };
assert_eq!( assert_eq!(
array4.human_friendly_type(), 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 { let empty_array = KclValue::HomArray {
value: vec![], value: vec![],
ty: RuntimeType::any(), 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 { let array_nested = KclValue::HomArray {
value: vec![array2_mm.clone()], value: vec![array2_mm.clone()],
@ -748,7 +727,7 @@ mod tests {
}; };
assert_eq!( assert_eq!(
array_nested.human_friendly_type(), array_nested.human_friendly_type(),
"an array of `[any; 2]` with 1 value".to_string() "array of [any; 2] with 1 value".to_string()
); );
} }
} }

View File

@ -8,8 +8,7 @@ pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketc
use cache::GlobalState; use cache::GlobalState;
pub use cache::{bust_cache, clear_mem_cache}; pub use cache::{bust_cache, clear_mem_cache};
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub use cad_op::Group; pub use cad_op::{Group, Operation};
pub use cad_op::Operation;
pub use geometry::*; pub use geometry::*;
pub use id_generator::IdGenerator; pub use id_generator::IdGenerator;
pub(crate) use import::PreImportedGeometry; pub(crate) use import::PreImportedGeometry;
@ -23,10 +22,8 @@ use kcmc::{
}; };
use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId}; use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId};
pub use memory::EnvironmentRef; pub use memory::EnvironmentRef;
pub(crate) use modeling::ModelingCmdMeta;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub(crate) use state::ModuleArtifactState;
pub use state::{ExecState, MetaSettings}; pub use state::{ExecState, MetaSettings};
use uuid::Uuid; use uuid::Uuid;
@ -59,7 +56,6 @@ mod import;
mod import_graph; mod import_graph;
pub(crate) mod kcl_value; pub(crate) mod kcl_value;
mod memory; mod memory;
mod modeling;
mod state; mod state;
pub mod typed_path; pub mod typed_path;
pub(crate) mod types; pub(crate) mod types;
@ -80,6 +76,9 @@ pub struct ExecOutcome {
/// the Feature Tree. /// the Feature Tree.
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub operations: Vec<Operation>, 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. /// Output artifact graph.
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
pub artifact_graph: ArtifactGraph, pub artifact_graph: ArtifactGraph,
@ -576,7 +575,7 @@ impl ExecutorContext {
let mut mem = exec_state.stack().clone(); let mut mem = exec_state.stack().clone();
let module_infos = exec_state.global.module_infos.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); mem.squash_env(result.0);
cache::write_old_memory((mem, module_infos)).await; cache::write_old_memory((mem, module_infos)).await;
@ -774,12 +773,15 @@ impl ExecutorContext {
)) ))
.await; .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) Ok(outcome)
} }
/// Perform the execution of a program. /// 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`. /// To access non-fatal errors and warnings, extract them from the `ExecState`.
pub async fn run( pub async fn run(
&self, &self,
@ -792,6 +794,9 @@ impl ExecutorContext {
/// Perform the execution of a program using a concurrent /// Perform the execution of a program using a concurrent
/// execution model. /// 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`. /// To access non-fatal errors and warnings, extract them from the `ExecState`.
pub async fn run_concurrent( pub async fn run_concurrent(
&self, &self,
@ -837,8 +842,6 @@ impl ExecutorContext {
let module_id = *module_id; let module_id = *module_id;
let module_path = module_path.clone(); let module_path = module_path.clone();
let source_range = SourceRange::from(import_stmt); let source_range = SourceRange::from(import_stmt);
// Clone before mutating.
let module_exec_state = exec_state.clone();
self.add_import_module_ops( self.add_import_module_ops(
exec_state, exec_state,
@ -850,6 +853,7 @@ impl ExecutorContext {
); );
let repr = repr.clone(); let repr = repr.clone();
let exec_state = exec_state.clone();
let exec_ctxt = self.clone(); let exec_ctxt = self.clone();
let results_tx = results_tx.clone(); let results_tx = results_tx.clone();
@ -869,13 +873,11 @@ impl ExecutorContext {
result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val))) result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
} }
ModuleRepr::Foreign(geom, _) => { 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 .await
.map(|geom| Some(KclValue::ImportedGeometry(geom))); .map(|geom| Some(KclValue::ImportedGeometry(geom)));
result.map(|val| { result.map(|val| ModuleRepr::Foreign(geom.clone(), val))
ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
})
} }
ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new( ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
format!("Module {module_path} not found in universe"), format!("Module {module_path} not found in universe"),
@ -887,7 +889,7 @@ impl ExecutorContext {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
wasm_bindgen_futures::spawn_local(async move { 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 exec_ctxt = exec_ctxt;
let result = exec_module( let result = exec_module(
@ -909,7 +911,7 @@ impl ExecutorContext {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
set.spawn(async move { set.spawn(async move {
let mut exec_state = module_exec_state; let mut exec_state = exec_state;
let exec_ctxt = exec_ctxt; let exec_ctxt = exec_ctxt;
let result = exec_module( 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 self.inner_run(program, exec_state, preserve_mem).await
} }
@ -1000,18 +993,6 @@ impl ExecutorContext {
Ok((universe, root_imports)) 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")] #[cfg(feature = "artifact-graph")]
fn add_import_module_ops( fn add_import_module_ops(
&self, &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 /// Perform the execution of a program. Accept all possible parameters and
/// output everything. /// output everything.
async fn inner_run( async fn inner_run(
@ -1128,32 +1121,26 @@ impl ExecutorContext {
&ModulePath::Main, &ModulePath::Main,
) )
.await; .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")] #[cfg(feature = "artifact-graph")]
{ {
// Fill in NodePath for operations. // Fill in NodePath for operations.
let cached_body_items = exec_state.global.artifacts.cached_body_items(); let cached_body_items = exec_state.global.artifacts.cached_body_items();
for op in exec_state.global.artifacts.operations.iter_mut().skip(start_op) { for op in exec_state.global.artifacts.operations.iter_mut().skip(start_op) {
op.fill_node_paths(program, cached_body_items); match op {
} Operation::StdLibCall {
#[cfg(test)] node_path,
{ source_range,
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);
}
} }
| 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; self.engine.clear_queues().await;
match exec_state.build_artifact_graph(&self.engine, program).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)), 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. /// 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> { async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
if exec_state.stack().memory.requires_std() { 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 path = vec!["std".to_owned(), "prelude".to_owned()];
let resolved_path = ModulePath::from_std_import_path(&path)?; let resolved_path = ModulePath::from_std_import_path(&path)?;
let id = self let id = self
@ -1187,14 +1171,6 @@ impl ExecutorContext {
let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?; let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
exec_state.mut_stack().memory.set_std(module_memory); 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(()) Ok(())
@ -1943,13 +1919,13 @@ notNull = !myNull
"#; "#;
assert_eq!( assert_eq!(
parse_execute(code1).await.unwrap_err().message(), 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"; let code2 = "notZero = !0";
assert_eq!( assert_eq!(
parse_execute(code2).await.unwrap_err().message(), 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#" let code3 = r#"
@ -1957,7 +1933,7 @@ notEmptyString = !""
"#; "#;
assert_eq!( assert_eq!(
parse_execute(code3).await.unwrap_err().message(), 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#" let code4 = r#"
@ -1966,7 +1942,7 @@ notMember = !obj.a
"#; "#;
assert_eq!( assert_eq!(
parse_execute(code4).await.unwrap_err().message(), 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 = " let code5 = "
@ -1974,7 +1950,7 @@ a = []
notArray = !a"; notArray = !a";
assert_eq!( assert_eq!(
parse_execute(code5).await.unwrap_err().message(), 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 = " let code6 = "
@ -1982,7 +1958,7 @@ x = {}
notObject = !x"; notObject = !x";
assert_eq!( assert_eq!(
parse_execute(code6).await.unwrap_err().message(), 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 = " let code7 = "
@ -2008,7 +1984,7 @@ notTagDeclarator = !myTagDeclarator";
assert!( assert!(
tag_declarator_err tag_declarator_err
.message() .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: {:?}", "Actual error: {:?}",
tag_declarator_err tag_declarator_err
); );
@ -2022,7 +1998,7 @@ notTagIdentifier = !myTag";
assert!( assert!(
tag_identifier_err tag_identifier_err
.message() .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: {:?}", "Actual error: {:?}",
tag_identifier_err tag_identifier_err
); );

View File

@ -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(())
}
}

View File

@ -3,9 +3,7 @@ use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use indexmap::IndexMap; use indexmap::IndexMap;
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
use kcmc::websocket::WebSocketResponse; use kittycad_modeling_cmds::websocket::WebSocketResponse;
#[cfg(feature = "artifact-graph")]
use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
@ -52,8 +50,6 @@ pub(super) struct GlobalState {
pub errors: Vec<CompilationError>, pub errors: Vec<CompilationError>,
#[cfg_attr(not(feature = "artifact-graph"), allow(dead_code))] #[cfg_attr(not(feature = "artifact-graph"), allow(dead_code))]
pub artifacts: ArtifactState, pub artifacts: ArtifactState,
#[cfg_attr(not(all(test, feature = "artifact-graph")), expect(dead_code))]
pub root_module_artifacts: ModuleArtifactState,
} }
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
@ -81,20 +77,6 @@ pub(super) struct ArtifactState {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub(super) struct ArtifactState {} 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)] #[derive(Debug, Clone)]
pub(super) struct ModuleState { pub(super) struct ModuleState {
/// The id generator for this module. /// The id generator for this module.
@ -114,7 +96,6 @@ pub(super) struct ModuleState {
pub settings: MetaSettings, pub settings: MetaSettings,
pub(super) explicit_length_units: bool, pub(super) explicit_length_units: bool,
pub(super) path: ModulePath, pub(super) path: ModulePath,
pub artifacts: ModuleArtifactState,
} }
impl ExecState { impl ExecState {
@ -145,17 +126,6 @@ impl ExecState {
self.global.errors.push(e); 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] { pub fn errors(&self) -> &[CompilationError] {
&self.global.errors &self.global.errors
} }
@ -163,7 +133,7 @@ impl ExecState {
/// Convert to execution outcome when running in WebAssembly. We want to /// Convert to execution outcome when running in WebAssembly. We want to
/// reduce the amount of data that crosses the WASM boundary as much as /// reduce the amount of data that crosses the WASM boundary as much as
/// possible. /// 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 // Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState. // state when we add more to ExecState.
ExecOutcome { ExecOutcome {
@ -172,18 +142,22 @@ impl ExecState {
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
operations: self.global.artifacts.operations, operations: self.global.artifacts.operations,
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_commands: self.global.artifacts.commands,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.global.artifacts.graph, artifact_graph: self.global.artifacts.graph,
errors: self.global.errors, errors: self.global.errors,
default_planes: ctx.engine.get_default_planes().read().await.clone(), 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 { ExecOutcome {
variables: self.mod_local.variables(main_ref), variables: self.mod_local.variables(main_ref),
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
operations: Default::default(), operations: Default::default(),
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
artifact_commands: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_graph: Default::default(), artifact_graph: Default::default(),
errors: self.global.errors, errors: self.global.errors,
filenames: Default::default(), filenames: Default::default(),
@ -214,22 +188,12 @@ impl ExecState {
} }
pub(crate) fn push_op(&mut self, op: Operation) { 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")] #[cfg(feature = "artifact-graph")]
self.global.artifacts.operations.push(op); self.global.artifacts.operations.push(op);
#[cfg(not(feature = "artifact-graph"))] #[cfg(not(feature = "artifact-graph"))]
drop(op); 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 { pub(super) fn next_module_id(&self) -> ModuleId {
ModuleId::from_usize(self.global.path_to_source_id.len()) ModuleId::from_usize(self.global.path_to_source_id.len())
} }
@ -277,21 +241,6 @@ impl ExecState {
self.global.module_infos.get(&id) 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 { pub fn current_default_units(&self) -> NumericType {
NumericType::Default { NumericType::Default {
len: self.length_unit(), len: self.length_unit(),
@ -400,7 +349,6 @@ impl GlobalState {
path_to_source_id: Default::default(), path_to_source_id: Default::default(),
module_infos: Default::default(), module_infos: Default::default(),
artifacts: Default::default(), artifacts: Default::default(),
root_module_artifacts: Default::default(),
mod_loader: Default::default(), mod_loader: Default::default(),
errors: Default::default(), errors: Default::default(),
id_to_source: 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 { impl ModuleState {
pub(super) fn new(path: ModulePath, memory: Arc<ProgramMemory>, module_id: Option<ModuleId>) -> Self { pub(super) fn new(path: ModulePath, memory: Arc<ProgramMemory>, module_id: Option<ModuleId>) -> Self {
ModuleState { ModuleState {
@ -464,7 +403,6 @@ impl ModuleState {
default_angle_units: Default::default(), default_angle_units: Default::default(),
kcl_version: "0.1".to_owned(), kcl_version: "0.1".to_owned(),
}, },
artifacts: Default::default(),
} }
} }

View File

@ -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 { pub fn to_string_lossy(&self) -> String {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {

View File

@ -438,7 +438,7 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Any => write!(f, "any"), PrimitiveType::Any => write!(f, "any"),
PrimitiveType::Number(NumericType::Known(unit)) => write!(f, "number({unit})"), PrimitiveType::Number(NumericType::Known(unit)) => write!(f, "number({unit})"),
PrimitiveType::Number(NumericType::Unknown) => write!(f, "number(unknown units)"), 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::Number(NumericType::Any) => write!(f, "number(any units)"),
PrimitiveType::String => write!(f, "string"), PrimitiveType::String => write!(f, "string"),
PrimitiveType::Boolean => write!(f, "bool"), PrimitiveType::Boolean => write!(f, "bool"),
@ -453,8 +453,8 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Axis2d => write!(f, "Axis2d"), PrimitiveType::Axis2d => write!(f, "Axis2d"),
PrimitiveType::Axis3d => write!(f, "Axis3d"), PrimitiveType::Axis3d => write!(f, "Axis3d"),
PrimitiveType::Helix => write!(f, "Helix"), PrimitiveType::Helix => write!(f, "Helix"),
PrimitiveType::ImportedGeometry => write!(f, "ImportedGeometry"), PrimitiveType::ImportedGeometry => write!(f, "imported geometry"),
PrimitiveType::Function => write!(f, "fn"), PrimitiveType::Function => write!(f, "function"),
} }
} }
} }
@ -499,6 +499,20 @@ impl NumericType {
NumericType::Known(UnitType::Angle(UnitAngle::Degrees)) 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 /// 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. /// 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), (at, Any) => (a.n, b.n, at),
(Any, bt) => (a.n, b.n, bt), (Any, bt) => (a.n, b.n, bt),
(Default { .. }, Default { .. }) | (_, Unknown) | (Unknown, _) => (a.n, b.n, Unknown),
// Known types and compatible, but needs adjustment. // 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::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), (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 and unknown => we assume the known one, possibly with adjustment
(Known(UnitType::Count), Default { .. }) | (Default { .. }, Known(UnitType::Count)) => { (Known(UnitType::Count), Default { .. }) | (Default { .. }, Known(UnitType::Count)) => {
(a.n, b.n, 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), (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), (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), (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), (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 { pub fn from_parsed(suffix: NumericSuffix, settings: &super::MetaSettings) -> Self {
match suffix { match suffix {
NumericSuffix::None => NumericType::Default { 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. /// A unit of length.
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)] #[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)] #[ts(export)]
@ -1508,23 +1510,6 @@ impl KclValue {
KclValue::Module { .. } | KclValue::KclNone { .. } | KclValue::Type { .. } => None, 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)] #[cfg(test)]
@ -2357,10 +2342,10 @@ d = cos(30)
let result = parse_execute(program).await.unwrap(); let result = parse_execute(program).await.unwrap();
assert!(result.exec_state.errors().is_empty()); 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("b", &result, 3.0, NumericType::default());
assert_value_and_type("c", &result, 1.0, NumericType::default()); assert_value_and_type("c", &result, 1.0, NumericType::count());
assert_value_and_type("d", &result, 1.0, NumericType::default()); assert_value_and_type("d", &result, 1.0, NumericType::count());
} }
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]

View File

@ -47,7 +47,7 @@ impl Tag {
match self { match self {
Tag::Deprecated => Some(vec![DiagnosticTag::DEPRECATED]), Tag::Deprecated => Some(vec![DiagnosticTag::DEPRECATED]),
Tag::Unnecessary => Some(vec![DiagnosticTag::UNNECESSARY]), Tag::Unnecessary => Some(vec![DiagnosticTag::UNNECESSARY]),
Tag::UnknownNumericUnits | Tag::None => None, Tag::None => None,
} }
} }
} }

View File

@ -950,7 +950,7 @@ startSketchOn(XY)
match hover.unwrap().contents { match hover.unwrap().contents {
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { 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!(), _ => unreachable!(),
} }
@ -3900,7 +3900,7 @@ startSketchOn(XY)
match hover.unwrap().contents { match hover.unwrap().contents {
tower_lsp::lsp_types::HoverContents::Markup(tower_lsp::lsp_types::MarkupContent { value, .. }) => { 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!(), _ => unreachable!(),
} }

View File

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
exec::KclValue, exec::KclValue,
execution::{typed_path::TypedPath, EnvironmentRef, ModuleArtifactState, PreImportedGeometry}, execution::{typed_path::TypedPath, EnvironmentRef, PreImportedGeometry},
fs::{FileManager, FileSystem}, fs::{FileManager, FileSystem},
parsing::ast::types::{ImportPath, Node, Program}, parsing::ast::types::{ImportPath, Node, Program},
source_range::SourceRange, source_range::SourceRange,
@ -131,11 +131,8 @@ impl ModuleInfo {
pub enum ModuleRepr { pub enum ModuleRepr {
Root, Root,
// AST, memory, exported names // AST, memory, exported names
Kcl( Kcl(Node<Program>, Option<(Option<KclValue>, EnvironmentRef, Vec<String>)>),
Node<Program>, Foreign(PreImportedGeometry, Option<KclValue>),
Option<(Option<KclValue>, EnvironmentRef, Vec<String>, ModuleArtifactState)>,
),
Foreign(PreImportedGeometry, Option<(Option<KclValue>, ModuleArtifactState)>),
Dummy, Dummy,
} }

View File

@ -3174,19 +3174,6 @@ impl PrimitiveType {
_ => None, _ => 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 { 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 { impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {

View File

@ -4,6 +4,8 @@ excerpt: "{{{description}}}"
layout: manual layout: manual
--- ---
# {{title}}
{{{description}}} {{{description}}}
## {{config_type}} Structure ## {{config_type}} Structure

View File

@ -3,18 +3,13 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use indexmap::IndexMap;
use insta::rounded_redaction; use insta::rounded_redaction;
use crate::{ use crate::{errors::KclError, ModuleId};
errors::KclError,
execution::{EnvironmentRef, ModuleArtifactState},
ExecOutcome, ExecState, ExecutorContext, ModuleId,
};
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
use crate::{ use crate::{
exec::ArtifactCommand,
execution::{ArtifactGraph, Operation}, execution::{ArtifactGraph, Operation},
modules::{ModulePath, ModuleRepr},
}; };
mod kcl_samples; mod kcl_samples;
@ -24,7 +19,8 @@ mod kcl_samples;
struct Test { struct Test {
/// The name of the test. /// The name of the test.
name: String, 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, entry_point: PathBuf,
/// Input KCL files are in this directory. /// Input KCL files are in this directory.
input_dir: PathBuf, input_dir: PathBuf,
@ -38,9 +34,6 @@ struct Test {
pub(crate) const RENDERED_MODEL_NAME: &str = "rendered_model.png"; pub(crate) const RENDERED_MODEL_NAME: &str = "rendered_model.png";
#[cfg(feature = "artifact-graph")]
const REPO_ROOT: &str = "../..";
impl Test { impl Test {
fn new(name: &str) -> Self { fn new(name: &str) -> Self {
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) fn assert_snapshot<F, R>(test: &Test, operation: &str, f: F)
where where
F: FnOnce() -> R, 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"); 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(|| { let mem_result = catch_unwind(AssertUnwindSafe(|| {
assert_snapshot(test, "Variables in memory after executing", || { 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")] #[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(); mem_result.unwrap();
} }
Err(e) => { Err(e) => {
@ -311,23 +238,7 @@ async fn execute_test(test: &Test, render_to_png: bool, export_step: bool) {
})); }));
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
{ assert_common_snapshots(test, error.operations, error.artifact_commands, error.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);
}
err_result.unwrap(); err_result.unwrap();
} }
e => { 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 /// Assert snapshots that should happen both when KCL execution succeeds and
/// succeeds and when it results in an error. /// when it results in an error.
#[cfg(feature = "artifact-graph")] #[cfg(feature = "artifact-graph")]
fn assert_artifact_snapshots( fn assert_common_snapshots(
test: &Test, test: &Test,
module_state: IndexMap<String, ModuleArtifactState>, operations: Vec<Operation>,
global_operations: Vec<Operation>, artifact_commands: Vec<ArtifactCommand>,
artifact_graph: ArtifactGraph, artifact_graph: ArtifactGraph,
) { ) {
let module_operations = module_state let operations = {
.iter() // Make the operations deterministic by sorting them by their module ID,
.map(|(path, s)| (path, &s.operations)) // then by their range.
.collect::<IndexMap<_, _>>(); 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(|| { let result1 = catch_unwind(AssertUnwindSafe(|| {
assert_snapshot(test, "Operations executed", || { assert_snapshot(test, "Operations executed", || {
insta::assert_json_snapshot!("ops", module_operations, { insta::assert_json_snapshot!("ops", operations, {
".*[].*.unlabeledArg.*.value.**[].from[]" => rounded_redaction(3), "[].*.unlabeledArg.*.value.**[].from[]" => rounded_redaction(3),
".*[].*.unlabeledArg.*.value.**[].to[]" => rounded_redaction(3), "[].*.unlabeledArg.*.value.**[].to[]" => rounded_redaction(3),
".*[].**.value.value" => rounded_redaction(3), "[].**.value.value" => rounded_redaction(3),
".*[].*.labeledArgs.*.value.**[].from[]" => rounded_redaction(3), "[].*.labeledArgs.*.value.**[].from[]" => rounded_redaction(3),
".*[].*.labeledArgs.*.value.**[].to[]" => rounded_redaction(3), "[].*.labeledArgs.*.value.**[].to[]" => rounded_redaction(3),
".**.sourceRange" => Vec::new(), ".**.sourceRange" => Vec::new(),
".**.functionSourceRange" => Vec::new(), ".**.functionSourceRange" => Vec::new(),
".**.moduleId" => 0, ".**.moduleId" => 0,
}); });
}) })
})); }));
let module_commands = module_state
.iter()
.map(|(path, s)| (path, &s.commands))
.collect::<IndexMap<_, _>>();
let result2 = catch_unwind(AssertUnwindSafe(|| { let result2 = catch_unwind(AssertUnwindSafe(|| {
assert_snapshot(test, "Artifact commands", || { assert_snapshot(test, "Artifact commands", || {
insta::assert_json_snapshot!("artifact_commands", module_commands, { insta::assert_json_snapshot!("artifact_commands", artifact_commands, {
".*[].command.**.value" => rounded_redaction(3), "[].command.**.value" => rounded_redaction(3),
".*[].command.**.x" => rounded_redaction(3), "[].command.**.x" => rounded_redaction(3),
".*[].command.**.y" => rounded_redaction(3), "[].command.**.y" => rounded_redaction(3),
".*[].command.**.z" => rounded_redaction(3), "[].command.**.z" => rounded_redaction(3),
".**.range" => Vec::new(), ".**.range" => Vec::new(),
}); });
}) })
@ -414,25 +337,6 @@ fn assert_artifact_snapshots(
result1.unwrap(); result1.unwrap();
result2.unwrap(); result2.unwrap();
result3.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 { mod cube {
@ -498,8 +402,8 @@ mod any_type {
super::execute(TEST_NAME, false).await super::execute(TEST_NAME, false).await
} }
} }
mod coerce_from_trig_to_point { mod error_with_point_shows_numeric_units {
const TEST_NAME: &str = "coerce_from_trig_to_point"; const TEST_NAME: &str = "error_with_point_shows_numeric_units";
/// Test parsing KCL. /// Test parsing KCL.
#[test] #[test]
@ -3642,27 +3546,3 @@ mod var_ref_in_own_def_decl {
super::execute(TEST_NAME, true).await 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
}
}

View File

@ -106,18 +106,17 @@ async fn inner_appearance(
a: 100.0, a: 100.0,
}; };
exec_state args.batch_modeling_cmd(
.batch_modeling_cmd( exec_state.next_uuid(),
(&args).into(), ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr {
ModelingCmd::from(mcmd::ObjectSetMaterialParamsPbr { object_id: solid_id,
object_id: solid_id, color,
color, metalness: metalness.unwrap_or_default() as f32 / 100.0,
metalness: metalness.unwrap_or_default() as f32 / 100.0, roughness: roughness.unwrap_or_default() as f32 / 100.0,
roughness: roughness.unwrap_or_default() as f32 / 100.0, ambient_occlusion: 0.0,
ambient_occlusion: 0.0, }),
}), )
) .await?;
.await?;
// Idk if we want to actually modify the memory for the colors, but I'm not right now since // 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. // I can't think of a use case for it.

View File

@ -1,10 +1,14 @@
use std::num::NonZeroU32; use std::num::NonZeroU32;
use anyhow::Result; use anyhow::Result;
use kcmc::{
websocket::{ModelingCmdReq, OkWebSocketResponseData},
ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::Serialize; use serde::Serialize;
use super::fillet::EdgeReference;
pub use crate::execution::fn_call::Args; pub use crate::execution::fn_call::Args;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
@ -24,6 +28,8 @@ use crate::{
ModuleId, ModuleId,
}; };
use super::fillet::EdgeReference;
const ERROR_STRING_SKETCH_TO_SOLID_HELPER: &str = 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`"; "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, None => msg_base,
Some(sugg) => format!("{msg_base}. {sugg}"), 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}"); message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
} }
KclError::new_semantic(KclErrorDetails::new(message, arg.source_ranges())) KclError::new_semantic(KclErrorDetails::new(message, arg.source_ranges()))
@ -257,7 +263,7 @@ impl Args {
Some(sugg) => format!("{msg_base}. {sugg}"), 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}"); message = format!("{message}. {ERROR_STRING_SKETCH_TO_SOLID_HELPER}");
} }
KclError::new_semantic(KclErrorDetails::new(message, arg.source_ranges())) 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>( fn get_tag_info_from_memory<'a, 'e>(
&'a self, &'a self,
exec_state: &'e mut ExecState, 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>( pub(crate) fn get_tag_engine_info<'a, 'e>(
&'a self, &'a self,
exec_state: &'e mut ExecState, exec_state: &'e mut ExecState,
@ -311,7 +345,6 @@ impl Args {
self.get_tag_info_from_memory(exec_state, tag) 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>( fn get_tag_engine_info_check_surface<'a, 'e>(
&'a self, &'a self,
exec_state: &'e mut ExecState, exec_state: &'e mut ExecState,
@ -329,6 +362,63 @@ impl Args {
self.get_tag_info_from_memory(exec_state, tag) 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> { pub(crate) fn make_kcl_val_from_point(&self, p: [f64; 2], ty: NumericType) -> Result<KclValue, KclError> {
let meta = Metadata { let meta = Metadata {
source_range: self.source_range, 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( pub(crate) async fn get_adjacent_face_to_tag(
&self, &self,
exec_state: &mut ExecState, 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`. /// Types which impl this trait can be extracted from a `KclValue`.
pub trait FromKclValue<'a>: Sized { pub trait FromKclValue<'a>: Sized {
/// Try to convert a KclValue into this type. /// Try to convert a KclValue into this type.
fn from_kcl_val(arg: &'a KclValue) -> Option<Self>; 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 { impl<'a> FromKclValue<'a> for TagNode {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> { fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
arg.get_tag_declarator().ok() arg.get_tag_declarator().ok()

View File

@ -7,10 +7,7 @@ use kittycad_modeling_cmds as kcmc;
use super::args::TyF64; use super::args::TyF64;
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
execution::{ execution::{types::RuntimeType, ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, Solid},
types::RuntimeType, ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, ModelingCmdMeta,
Solid,
},
parsing::ast::types::TagNode, parsing::ast::types::TagNode,
std::{fillet::EdgeReference, Args}, std::{fillet::EdgeReference, Args},
}; };
@ -55,21 +52,20 @@ async fn inner_chamfer(
}; };
let id = exec_state.next_uuid(); let id = exec_state.next_uuid();
exec_state args.batch_end_cmd(
.batch_end_cmd( id,
ModelingCmdMeta::from_args_id(&args, id), ModelingCmd::from(mcmd::Solid3dFilletEdge {
ModelingCmd::from(mcmd::Solid3dFilletEdge { edge_id: None,
edge_id: None, edge_ids: vec![edge_id],
edge_ids: vec![edge_id], extra_face_ids: vec![],
extra_face_ids: vec![], strategy: Default::default(),
strategy: Default::default(), object_id: solid.id,
object_id: solid.id, radius: LengthUnit(length.to_mm()),
radius: LengthUnit(length.to_mm()), tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future.
tolerance: LengthUnit(DEFAULT_TOLERANCE), // We can let the user set this in the future. cut_type: CutType::Chamfer,
cut_type: CutType::Chamfer, }),
}), )
) .await?;
.await?;
solid.edge_cuts.push(EdgeCut::Chamfer { solid.edge_cuts.push(EdgeCut::Chamfer {
id, id,

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