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