Compare commits

..

1 Commits

Author SHA1 Message Date
078ffa02b0 Add the at token 2024-11-26 16:19:00 -06:00
642 changed files with 142199 additions and 137584 deletions

View File

@ -165,6 +165,7 @@ jobs:
- name: Build the app (release) - name: Build the app (release)
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
env: env:
PUBLISH_FOR_PULL_REQUEST: true
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
@ -172,6 +173,7 @@ jobs:
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always run: yarn electron-builder --config --publish always
@ -227,6 +229,7 @@ jobs:
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
CSC_FOR_PULL_REQUEST: true
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }} WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
run: yarn electron-builder --config --publish always run: yarn electron-builder --config --publish always
@ -359,17 +362,6 @@ jobs:
- name: List artifacts - name: List artifacts
run: "ls -R out" run: "ls -R out"
- name: Set more complete nightly release notes
if: ${{ env.IS_NIGHTLY == 'true' }}
run: |
# Note: preferred going this way instead of a full clone in the checkout step,
# see https://github.com/actions/checkout/issues/1471
git fetch --prune --unshallow --tags
export TAG="nightly-${VERSION}"
export PREVIOUS_TAG=$(git describe --tags --match="nightly-v[0-9]*" --abbrev=0)
export NOTES=$(./scripts/get-nightly-changelog.sh)
yarn files:set-notes
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
if: ${{ env.IS_NIGHTLY == 'true' }} if: ${{ env.IS_NIGHTLY == 'true' }}
uses: 'google-github-actions/auth@v2.1.7' uses: 'google-github-actions/auth@v2.1.7'
@ -391,17 +383,12 @@ jobs:
parent: false parent: false
destination: 'dl.kittycad.io/releases/modeling-app/nightly' destination: 'dl.kittycad.io/releases/modeling-app/nightly'
- name: Invalidate bucket cache on latest*.yml and last_download.json files - name: Create draft release
if: ${{ env.IS_NIGHTLY == 'true' }} uses: softprops/action-gh-release@v2
run: yarn files:invalidate-bucket:nightly if: ${{ env.IS_RELEASE == 'true' }}
- name: Tag nightly commit
if: ${{ env.IS_NIGHTLY == 'true' }}
uses: actions/github-script@v7
with: with:
script: | name: ${{ env.VERSION }}
const { VERSION } = process.env tag_name: ${{ env.VERSION }}
const { owner, repo } = context.repo draft: true
const { sha } = context generate_release_notes: true
const ref = `refs/tags/nightly-${VERSION}` files: 'out/Zoo*'
github.rest.git.createRef({ owner, repo, sha, ref })

View File

@ -2,8 +2,28 @@ on:
push: push:
branches: branches:
- main - main
paths:
- 'src/wasm-lib/**.rs'
- 'src/wasm-lib/**.hbs'
- 'src/wasm-lib/**.gen'
- 'src/wasm-lib/**.snap'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'src/wasm-lib/**.kcl'
- .github/workflows/cargo-test.yml
pull_request: pull_request:
paths:
- 'src/wasm-lib/**.rs'
- 'src/wasm-lib/**.hbs'
- 'src/wasm-lib/**.gen'
- 'src/wasm-lib/**.snap'
- '**/Cargo.toml'
- '**/Cargo.lock'
- '**/rust-toolchain.toml'
- 'src/wasm-lib/**.kcl'
- .github/workflows/cargo-test.yml
workflow_dispatch: workflow_dispatch:
permissions: read-all permissions: read-all
concurrency: concurrency:
@ -51,7 +71,7 @@ jobs:
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}} KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
RUST_MIN_STACK: 10485760000 RUST_MIN_STACK: 10485760000
- name: Upload to codecov.io - name: Upload to codecov.io
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v4
with: with:
token: ${{secrets.CODECOV_TOKEN}} token: ${{secrets.CODECOV_TOKEN}}
fail_ci_if_error: true fail_ci_if_error: true

View File

@ -68,7 +68,7 @@ jobs:
- name: Download Wasm Cache - name: Download Wasm Cache
id: download-wasm id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false' if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v7 uses: dawidd6/action-download-artifact@v6
continue-on-error: true continue-on-error: true
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}
@ -255,7 +255,7 @@ jobs:
- name: Download Wasm Cache - name: Download Wasm Cache
id: download-wasm id: download-wasm
if: needs.check-rust-changes.outputs.rust-changed == 'false' if: needs.check-rust-changes.outputs.rust-changed == 'false'
uses: dawidd6/action-download-artifact@v7 uses: dawidd6/action-download-artifact@v6
continue-on-error: true continue-on-error: true
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}

View File

@ -126,13 +126,11 @@ jobs:
destination: 'dl.kittycad.io/releases/modeling-app' destination: 'dl.kittycad.io/releases/modeling-app'
- name: Invalidate bucket cache on latest*.yml and last_download.json files - name: Invalidate bucket cache on latest*.yml and last_download.json files
run: yarn files:invalidate-bucket run: |
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/last_download.json" --async
- name: Upload release files to Github gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-linux-arm64.yml" --async
if: ${{ github.event_name == 'release' }} gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-mac.yml" --async
uses: softprops/action-gh-release@v2 gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest.yml" --async
with:
files: 'out/Zoo*'
announce_release: announce_release:

1
.gitignore vendored
View File

@ -61,7 +61,6 @@ Mac_App_Distribution.provisionprofile
*.tsbuildinfo *.tsbuildinfo
src/wasm-lib/pkg src/wasm-lib/pkg
.eslintcache
venv venv
.vite/ .vite/

2
.nvmrc
View File

@ -1 +1 @@
v22.12.0 v21.7.3

View File

@ -1,43 +0,0 @@
# Setting Up Zoo Modeling App
Compared to other CAD software, getting Zoo Modeling App up and running is quick and straightforward across platforms. It's about 100MB to download and is quick to install.
## Windows
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for Windows and for your processor type.
2. Once downloaded, run the installer `Zoo Modeling App-{version}-{arch}-win.exe` which should take a few seconds.
3. The installation happens at `C:\Program Files\Zoo Modeling App`. A shortcut in the start menu is also created so you can run the app easily by clicking on it.
## macOS
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for macOS and for your processor type.
2. Once downloaded, open the disk image `Zoo Modeling App-{version}-{arch}-mac.dmg` and drag the applications to your `Applications` directory.
3. You can then open your `Applications` directory and double-click on `Zoo Modeling App` to open.
## Linux
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for Linux and for your processor type.
2. Install the dependencies needed to run the [AppImage format](https://appimage.org/).
- On Ubuntu, install the FUSE library with these commands in a terminal.
```bash
sudo apt update
sudo apt install libfuse2
```
- Optionally, follow [these steps](https://github.com/probonopd/go-appimage/blob/master/src/appimaged/README.md#initial-setup) to install `appimaged`. It is a daemon that makes interacting with AppImage files more seamless.
- Once installed, copy the downloaded `Zoo Modeling App-{version}-{arch}-linux.AppImage` to the directory of your choice, for instance `~/Applications`.
- `appimaged` should automatically find it and make it executable. If not, run:
```bash
chmod a+x ~/Applications/Zoo\ Modeling\ App-{version}-{arch}-linux.AppImage
```
3. You can double-click on the AppImage to run it, or in a terminal with this command:
```bash
~/Applications/Zoo\ Modeling\ App-{version}-{arch}-linux.AppImage
```

View File

@ -99,7 +99,7 @@ yarn tron:start
This will start the application and hot-reload on changes. This will start the application and hot-reload on changes.
Devtools can be opened with the usual Cmd-Opt-I (Mac) or Ctrl-Shift-I (Linux and Windows). Devtools can be opened with the usual Cmd/Ctrl-Shift-I.
To build, run `yarn tron:package`. To build, run `yarn tron:package`.
@ -136,7 +136,7 @@ https://github.com/KittyCAD/modeling-app/issues/new
#### 2. Push a new tag #### 2. Push a new tag
Create a new tag and push it to the repo. The `semantic-release.sh` script will automatically bump the minor part, which we use the most. For instance going from `v0.27.0` to `v0.28.0`. Create a new tag and push it to the repo (eg. `v0.28.0` for `$VERSION`)
``` ```
VERSION=$(./scripts/semantic-release.sh) VERSION=$(./scripts/semantic-release.sh)
@ -146,14 +146,16 @@ git push origin --tags
This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files as well as updater-test artifacts. This will trigger the `build-apps` workflow, set the version, build & sign the apps, and generate release files as well as updater-test artifacts.
The workflow should be listed right away [in this list](https://github.com/KittyCAD/modeling-app/actions/workflows/build-apps.yml?query=event%3Apush)). Once the workflow succeeds, a draft release will be created at https://github.com/KittyCAD/modeling-app/releases.
#### 3. Manually test artifacts #### 3. Manually test artifacts from the Cut Release PR
##### Release builds ##### Release builds
The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in 2.). The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in 2.).
Alternatively, the draft release will also include these builds.
Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue. Manually test against this [list](https://github.com/KittyCAD/modeling-app/issues/3588) across Windows, MacOS, Linux and posting results as comments in the issue.
##### Updater-test builds ##### Updater-test builds
@ -176,11 +178,9 @@ If the prompt doesn't show up, start the app in command line to grab the electro
#### 4. Publish the release #### 4. Publish the release
Head over to https://github.com/KittyCAD/modeling-app/releases/new, pick the newly created tag and type it in the _Release title_ field as well. Head over to https://github.com/KittyCAD/modeling-app/releases, paste in the changelog discussed in the issue, and publish the draft release created by the `build-apps` workflow from step 2.
Hit _Generate release notes_ as a starting point to discuss the changelog in the issue. Once done, make sure _Set as the latest release_ is checked, and hit _Publish release_. A new Action kicks in at https://github.com/KittyCAD/modeling-app/actions, which can be found under `release` event filter. On success, the files will be uploaded to the public bucket and the announcement on Discord will be sent.
A new `publish-apps-release` will kick in and you should be able to find it [here](https://github.com/KittyCAD/modeling-app/actions?query=event%3Arelease). On success, the files will be uploaded to the public bucket as well as to the GitHub release, and the announcement on Discord will be sent.
#### 5. Close the issue #### 5. Close the issue
@ -450,9 +450,3 @@ PS: for the debug panel, the following JSON is useful for snapping the camera
## KCL ## KCL
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl). For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl).
### Logging
To display logging (to the terminal or console) set `ZOO_LOG=1`. This will log some warnings and simple performance metrics. To view these in test runs, use `-- --nocapture`.
To enable memory metrics, build with `--features dhat-heap`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -22,5 +22,3 @@ once fixed in engine will just start working here with no language changes.
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple - **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
chamfer cases work currently. chamfer cases work currently.
- **Appearance**: Changing the appearance on a loft does not work.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -58,7 +58,7 @@ mountingPlate = extrude(thickness, mountingPlateSketch)
```js ```js
// Sketch on the face of a chamfer. // Sketch on the face of a chamfer.
fn cube(pos, scale) { fn cube = (pos, scale) => {
sg = startSketchOn('XY') sg = startSketchOn('XY')
|> startProfileAt(pos, %) |> startProfileAt(pos, %)
|> line([0, scale], %) |> line([0, scale], %)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -19,7 +19,6 @@ layout: manual
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects) * [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
* [`angledLineToX`](kcl/angledLineToX) * [`angledLineToX`](kcl/angledLineToX)
* [`angledLineToY`](kcl/angledLineToY) * [`angledLineToY`](kcl/angledLineToY)
* [`appearance`](kcl/appearance)
* [`arc`](kcl/arc) * [`arc`](kcl/arc)
* [`arcTo`](kcl/arcTo) * [`arcTo`](kcl/arcTo)
* [`asin`](kcl/asin) * [`asin`](kcl/asin)
@ -30,7 +29,6 @@ layout: manual
* [`assertLessThan`](kcl/assertLessThan) * [`assertLessThan`](kcl/assertLessThan)
* [`assertLessThanOrEq`](kcl/assertLessThanOrEq) * [`assertLessThanOrEq`](kcl/assertLessThanOrEq)
* [`atan`](kcl/atan) * [`atan`](kcl/atan)
* [`atan2`](kcl/atan2)
* [`bezierCurve`](kcl/bezierCurve) * [`bezierCurve`](kcl/bezierCurve)
* [`ceil`](kcl/ceil) * [`ceil`](kcl/ceil)
* [`chamfer`](kcl/chamfer) * [`chamfer`](kcl/chamfer)
@ -103,7 +101,6 @@ layout: manual
* [`startProfileAt`](kcl/startProfileAt) * [`startProfileAt`](kcl/startProfileAt)
* [`startSketchAt`](kcl/startSketchAt) * [`startSketchAt`](kcl/startSketchAt)
* [`startSketchOn`](kcl/startSketchOn) * [`startSketchOn`](kcl/startSketchOn)
* [`sweep`](kcl/sweep)
* [`tan`](kcl/tan) * [`tan`](kcl/tan)
* [`tangentToEnd`](kcl/tangentToEnd) * [`tangentToEnd`](kcl/tangentToEnd)
* [`tangentialArc`](kcl/tangentialArc) * [`tangentialArc`](kcl/tangentialArc)

View File

@ -37,7 +37,7 @@ assertEqual(n, 3, 0.0001, "5/2 = 2.5, rounded up makes 3")
startSketchOn('XZ') startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 2 }, %) |> circle({ center = [0, 0], radius = 2 }, %)
|> extrude(5, %) |> extrude(5, %)
|> patternTransform(n, fn(id) { |> patternTransform(n, (id) => {
return { translate = [4 * id, 0, 0] } return { translate = [4 * id, 0, 0] }
}, %) }, %)
``` ```

File diff suppressed because one or more lines are too long

View File

@ -29,7 +29,7 @@ map(array: [KclValue], map_fn: FunctionParam) -> [KclValue]
```js ```js
r = 10 // radius r = 10 // radius
fn drawCircle(id) { fn drawCircle = (id) => {
return startSketchOn("XY") return startSketchOn("XY")
|> circle({ center = [id * 2 * r, 0], radius = r }, %) |> circle({ center = [id * 2 * r, 0], radius = r }, %)
} }
@ -45,7 +45,7 @@ circles = map([1..3], drawCircle)
```js ```js
r = 10 // radius r = 10 // radius
// Call `map`, using an anonymous function instead of a named one. // Call `map`, using an anonymous function instead of a named one.
circles = map([1..3], fn(id) { circles = map([1..3], (id) => {
return startSketchOn("XY") return startSketchOn("XY")
|> circle({ center = [id * 2 * r, 0], radius = r }, %) |> circle({ center = [id * 2 * r, 0], radius = r }, %)
}) })

View File

@ -12,7 +12,7 @@ to other modules.
``` ```
// util.kcl // util.kcl
export fn increment(x) { export fn increment = (x) => {
return x + 1 return x + 1
} }
``` ```
@ -37,11 +37,11 @@ Multiple functions can be exported in a file.
``` ```
// util.kcl // util.kcl
export fn increment(x) { export fn increment = (x) => {
return x + 1 return x + 1
} }
export fn decrement(x) { export fn decrement = (x) => {
return x - 1 return x - 1
} }
``` ```

File diff suppressed because one or more lines are too long

View File

@ -30,7 +30,7 @@ patternTransform2d(total_instances: u32, transform_function: FunctionParam, soli
```js ```js
// Each instance will be shifted along the X axis. // Each instance will be shifted along the X axis.
fn transform(id) { fn transform = (id) => {
return { translate = [4 * id, 0] } return { translate = [4 * id, 0] }
} }

View File

@ -30,20 +30,20 @@ reduce(array: [KclValue], start: KclValue, reduce_fn: FunctionParam) -> KclValue
```js ```js
// This function adds two numbers. // This function adds two numbers.
fn add(a, b) { fn add = (a, b) => {
return a + b return a + b
} }
// This function adds an array of numbers. // This function adds an array of numbers.
// It uses the `reduce` function, to call the `add` function on every // It uses the `reduce` function, to call the `add` function on every
// element of the `arr` parameter. The starting value is 0. // element of the `arr` parameter. The starting value is 0.
fn sum(arr) { fn sum = (arr) => {
return reduce(arr, 0, add) return reduce(arr, 0, add)
} }
/* The above is basically like this pseudo-code: /* The above is basically like this pseudo-code:
fn sum(arr): fn sum(arr):
sumSoFar = 0 let sumSoFar = 0
for i in arr: for i in arr:
sumSoFar = add(sumSoFar, i) sumSoFar = add(sumSoFar, i)
return sumSoFar */ return sumSoFar */
@ -61,7 +61,7 @@ assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
// an anonymous `add` function as its parameter, instead of declaring a // an anonymous `add` function as its parameter, instead of declaring a
// named function outside. // named function outside.
arr = [1, 2, 3] arr = [1, 2, 3]
sum = reduce(arr, 0, fn(i, result_so_far) { sum = reduce(arr, 0, (i, result_so_far) => {
return i + result_so_far return i + result_so_far
}) })
@ -74,7 +74,7 @@ assertEqual(sum, 6, 0.00001, "1 + 2 + 3 summed is 6")
```js ```js
// Declare a function that sketches a decagon. // Declare a function that sketches a decagon.
fn decagon(radius) { fn decagon = (radius) => {
// Each side of the decagon is turned this many degrees from the previous angle. // Each side of the decagon is turned this many degrees from the previous angle.
stepAngle = 1 / 10 * tau() stepAngle = 1 / 10 * tau()
@ -84,7 +84,7 @@ fn decagon(radius) {
// Use a `reduce` to draw the remaining decagon sides. // Use a `reduce` to draw the remaining decagon sides.
// For each number in the array 1..10, run the given function, // For each number in the array 1..10, run the given function,
// which takes a partially-sketched decagon and adds one more edge to it. // which takes a partially-sketched decagon and adds one more edge to it.
fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) { fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) => {
// Draw one edge of the decagon. // Draw one edge of the decagon.
x = cos(stepAngle * i) * radius x = cos(stepAngle * i) * radius
y = sin(stepAngle * i) * radius y = sin(stepAngle * i) * radius
@ -96,14 +96,14 @@ fn decagon(radius) {
/* The `decagon` above is basically like this pseudo-code: /* The `decagon` above is basically like this pseudo-code:
fn decagon(radius): fn decagon(radius):
stepAngle = (1/10) * tau() let stepAngle = (1/10) * tau()
startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)]) let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
// Here's the reduce part. // Here's the reduce part.
partialDecagon = startOfDecagonSketch let partialDecagon = startOfDecagonSketch
for i in [1..10]: for i in [1..10]:
x = cos(stepAngle * i) * radius let x = cos(stepAngle * i) * radius
y = sin(stepAngle * i) * radius let y = sin(stepAngle * i) * radius
partialDecagon = lineTo([x, y], partialDecagon) partialDecagon = lineTo([x, y], partialDecagon)
fullDecagon = partialDecagon // it's now full fullDecagon = partialDecagon // it's now full
return fullDecagon */ return fullDecagon */

File diff suppressed because one or more lines are too long

View File

@ -36,7 +36,7 @@ cube = startSketchAt([0, 0])
|> close(%) |> close(%)
|> extrude(5, %) |> extrude(5, %)
fn cylinder(radius, tag) { fn cylinder = (radius, tag) => {
return startSketchAt([0, 0]) return startSketchAt([0, 0])
|> circle({ |> circle({
radius = radius, radius = radius,

View File

@ -36,7 +36,7 @@ cube = startSketchAt([0, 0])
|> close(%) |> close(%)
|> extrude(5, %) |> extrude(5, %)
fn cylinder(radius, tag) { fn cylinder = (radius, tag) => {
return startSketchAt([0, 0]) return startSketchAt([0, 0])
|> circle({ |> circle({
radius = radius, radius = radius,

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -41,7 +41,7 @@ If you want to get a value from an array you can use the index like so:
An object is defined with `{}` braces. Here is an example object: An object is defined with `{}` braces. Here is an example object:
``` ```
myObj = { a = 0, b = "thing" } myObj = {a: 0, b: "thing"}
``` ```
We support two different ways of getting properties from objects, you can call We support two different ways of getting properties from objects, you can call
@ -54,7 +54,7 @@ We also have support for defining your own functions. Functions can take in any
type of argument. Below is an example of the syntax: type of argument. Below is an example of the syntax:
``` ```
fn myFn(x) { fn myFn = (x) => {
return x return x
} }
``` ```
@ -90,12 +90,12 @@ startSketchOn('XZ')
|> startProfileAt(origin, %) |> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001) |> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001) - 90, segAng(rectangleSegmentA001, %) - 90,
196.99 196.99
], %, $rectangleSegmentB001) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001), segAng(rectangleSegmentA001, %),
-segLen(rectangleSegmentA001) -segLen(rectangleSegmentA001, %)
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
@ -118,17 +118,17 @@ use the tag `rectangleSegmentA001` in any function or expression in the file.
However if the code was written like this: However if the code was written like this:
``` ```
fn rect(origin) { fn rect = (origin) => {
return startSketchOn('XZ') return startSketchOn('XZ')
|> startProfileAt(origin, %) |> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001) |> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001) - 90, segAng(rectangleSegmentA001, %) - 90,
196.99 196.99
], %, $rectangleSegmentB001) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001), segAng(rectangleSegmentA001, %),
-segLen(rectangleSegmentA001) -segLen(rectangleSegmentA001, %)
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
@ -146,17 +146,17 @@ Tags are accessible through the sketch group they are declared in.
For example the following code works. For example the following code works.
``` ```
fn rect(origin) { fn rect = (origin) => {
return startSketchOn('XZ') return startSketchOn('XZ')
|> startProfileAt(origin, %) |> startProfileAt(origin, %)
|> angledLine([0, 191.26], %, $rectangleSegmentA001) |> angledLine([0, 191.26], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001) - 90, segAng(rectangleSegmentA001, %) - 90,
196.99 196.99
], %, $rectangleSegmentB001) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA001), segAng(rectangleSegmentA001, %),
-segLen(rectangleSegmentA001) -segLen(rectangleSegmentA001, %)
], %, $rectangleSegmentC001) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
@ -167,10 +167,7 @@ myRect = rect([20, 0])
myRect myRect
|> extrude(10, %) |> extrude(10, %)
|> fillet({ |> fillet({radius: 0.5, tags: [myRect.tags.rectangleSegmentA001]}, %)
radius = 0.5,
tags = [myRect.tags.rectangleSegmentA001]
}, %)
``` ```
See how we use the tag `rectangleSegmentA001` in the `fillet` function outside See how we use the tag `rectangleSegmentA001` in the `fillet` function outside

View File

@ -1,23 +0,0 @@
---
title: "AppearanceData"
excerpt: "Data for appearance."
layout: manual
---
Data for appearance.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `color` |`string`| Color of the new material, a hex string like "#ff0000". | No |
| `metalness` |`number` (**maximum:** 100.0)| Metalness of the new material, a percentage like 95.7. | No |
| `roughness` |`number` (**maximum:** 100.0)| Roughness of the new material, a percentage like 95.7. | No |

View File

@ -0,0 +1,161 @@
---
title: "BinaryOperator"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Add two numbers.
**enum:** `+`
----
Subtract two numbers.
**enum:** `-`
----
Multiply two numbers.
**enum:** `*`
----
Divide two numbers.
**enum:** `/`
----
Modulo two numbers.
**enum:** `%`
----
Raise a number to a power.
**enum:** `^`
----
Are two numbers equal?
**enum:** `==`
----
Are two numbers not equal?
**enum:** `!=`
----
Is left greater than right
**enum:** `>`
----
Is left greater than or equal to right
**enum:** `>=`
----
Is left less than right
**enum:** `<`
----
Is left less than or equal to right
**enum:** `<=`
----

View File

@ -0,0 +1,161 @@
---
title: "BinaryPart"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `BinaryExpression`| | No |
| `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)| | No |
| `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CallExpression`| | No |
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| | No |
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `optional` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `UnaryExpression`| | No |
| `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)| | No |
| `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `IfExpression`| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| | No |
| `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`| | No |
| `final_else` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -0,0 +1,97 @@
---
title: "BodyItem"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ImportStatement`| | No |
| `items` |`[` [`ImportItem`](/docs/kcl/types/ImportItem) `]`| | No |
| `path` |`string`| | No |
| `raw_path` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ExpressionStatement`| | No |
| `expression` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `VariableDeclaration`| | No |
| `declarations` |`[` [`VariableDeclarator`](/docs/kcl/types/VariableDeclarator) `]`| | No |
| `visibility` |[`ItemVisibility`](/docs/kcl/types/ItemVisibility)| | No |
| `kind` |[`VariableKind`](/docs/kcl/types/VariableKind)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ReturnStatement`| | No |
| `argument` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -0,0 +1,41 @@
---
title: "CommentStyle"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Like // foo
**enum:** `line`
----
Like /* foo */
**enum:** `block`
----

24
docs/kcl/types/ElseIf.md Normal file
View File

@ -0,0 +1,24 @@
---
title: "ElseIf"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `cond` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,16 @@
---
title: "EnvironmentRef"
excerpt: "An index pointing to an environment."
layout: manual
---
An index pointing to an environment.
**Type:** `integer` (`uint`)

318
docs/kcl/types/Expr.md Normal file
View File

@ -0,0 +1,318 @@
---
title: "Expr"
excerpt: "An expression can be evaluated to yield a single KCL value."
layout: manual
---
An expression can be evaluated to yield a single KCL value.
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| An expression can be evaluated to yield a single KCL value. | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`TagDeclarator`](/docs/kcl/types#tag-declaration)| | No |
| `value` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `BinaryExpression`| | No |
| `operator` |[`BinaryOperator`](/docs/kcl/types/BinaryOperator)| An expression can be evaluated to yield a single KCL value. | No |
| `left` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `right` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`FunctionExpression`](/docs/kcl/types/FunctionExpression)| | No |
| `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`| | No |
| `body` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `CallExpression`| | No |
| `callee` |[`Identifier`](/docs/kcl/types/Identifier)| An expression can be evaluated to yield a single KCL value. | No |
| `arguments` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `optional` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `PipeExpression`| | No |
| `body` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `PipeSubstitution`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayExpression`| | No |
| `elements` |`[` [`Expr`](/docs/kcl/types/Expr) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ArrayRangeExpression`| | No |
| `startElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endElement` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `endInclusive` |`boolean`| Is the `end_element` included in the range? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `ObjectExpression`| | No |
| `properties` |`[` [`ObjectProperty`](/docs/kcl/types/ObjectProperty) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| An expression can be evaluated to yield a single KCL value. | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| An expression can be evaluated to yield a single KCL value. | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `UnaryExpression`| | No |
| `operator` |[`UnaryOperator`](/docs/kcl/types/UnaryOperator)| An expression can be evaluated to yield a single KCL value. | No |
| `argument` |[`BinaryPart`](/docs/kcl/types/BinaryPart)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `IfExpression`| | No |
| `cond` |[`Expr`](/docs/kcl/types/Expr)| An expression can be evaluated to yield a single KCL value. | No |
| `then_val` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `else_ifs` |`[` [`ElseIf`](/docs/kcl/types/ElseIf) `]`| | No |
| `final_else` |[`Program`](/docs/kcl/types/Program)| An expression can be evaluated to yield a single KCL value. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
KCL value for an optional parameter which was not given an argument. (remember, parameters are in the function declaration, arguments are in the function call/application).
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `None`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -0,0 +1,24 @@
---
title: "FunctionExpression"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `params` |`[` [`Parameter`](/docs/kcl/types/Parameter) `]`| | No |
| `body` |[`Program`](/docs/kcl/types/Program)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,23 @@
---
title: "Identifier"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,24 @@
---
title: "ImportItem"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `name` |[`Identifier`](/docs/kcl/types/Identifier)| Name of the item to import. | No |
| `alias` |[`Identifier`](/docs/kcl/types/Identifier)| Rename the item using an identifier after "as". | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,16 @@
---
title: "ItemVisibility"
excerpt: ""
layout: manual
---
**enum:** `default`, `export`

View File

@ -12,10 +12,5 @@ KCL value for an optional parameter which was not given an argument. (remember,
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -317,6 +317,7 @@ Data for an imported geometry.
| Property | Type | Description | Required | | Property | Type | Description | Required |
|----------|------|-------------|----------| |----------|------|-------------|----------|
| `type` |enum: `Function`| | No | | `type` |enum: `Function`| | No |
| `expression` |[`FunctionExpression`](/docs/kcl/types/FunctionExpression)| Any KCL value. | No |
| `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No | | `memory` |[`ProgramMemory`](/docs/kcl/types/ProgramMemory)| Any KCL value. | No |
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |

View File

@ -0,0 +1,56 @@
---
title: "LiteralIdentifier"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `Literal`| | No |
| `value` |[`LiteralValue`](/docs/kcl/types/LiteralValue)| | No |
| `raw` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -0,0 +1,47 @@
---
title: "LiteralValue"
excerpt: ""
layout: manual
---
**This schema accepts any of the following:**
**Type:** `number` (`double`)
----
**Type:** `string`
----
**Type:** `boolean`
----

View File

@ -0,0 +1,57 @@
---
title: "MemberObject"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `MemberExpression`| | No |
| `object` |[`MemberObject`](/docs/kcl/types/MemberObject)| | No |
| `property` |[`LiteralIdentifier`](/docs/kcl/types/LiteralIdentifier)| | No |
| `computed` |`boolean`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: [`Identifier`](/docs/kcl/types/Identifier)| | No |
| `name` |`string`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |
----

View File

@ -0,0 +1,22 @@
---
title: "NonCodeMeta"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `nonCodeNodes` |`object`| | No |
| `startNodes` |`[` [`NonCodeNode`](/docs/kcl/types/NonCodeNode) `]`| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

View File

@ -0,0 +1,23 @@
---
title: "NonCodeNode"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `value` |[`NonCodeValue`](/docs/kcl/types/NonCodeValue)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,86 @@
---
title: "NonCodeValue"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
An inline comment. Here are examples: `1 + 1 // This is an inline comment`. `1 + 1 /* Here's another */`.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `inlineComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
A block comment. An example of this is the following: ```python,no_run /* This is a block comment */ 1 + 1 ``` Now this is important. The block comment is attached to the next line. This is always the case. Also the block comment doesn't have a new line above it. If it did it would be a `NewLineBlockComment`.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `blockComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
A block comment that has a new line above it. The user explicitly added a new line above the block comment.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `newLineBlockComment`| | No |
| `value` |`string`| | No |
| `style` |[`CommentStyle`](/docs/kcl/types/CommentStyle)| | No |
----
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `type` |enum: `newLine`| | No |
----

View File

@ -0,0 +1,24 @@
---
title: "ObjectProperty"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `key` |[`Identifier`](/docs/kcl/types/Identifier)| | No |
| `value` |[`Expr`](/docs/kcl/types/Expr)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,23 @@
---
title: "Parameter"
excerpt: "Parameter of a KCL function."
layout: manual
---
Parameter of a KCL function.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `identifier` |[`Identifier`](/docs/kcl/types/Identifier)| The parameter's label or name. | No |
| `optional` |`boolean`| Is the parameter optional? | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |

26
docs/kcl/types/Program.md Normal file
View File

@ -0,0 +1,26 @@
---
title: "Program"
excerpt: "A KCL program top level, or function body."
layout: manual
---
A KCL program top level, or function body.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `body` |`[` [`BodyItem`](/docs/kcl/types/BodyItem) `]`| | No |
| `nonCodeMeta` |[`NonCodeMeta`](/docs/kcl/types/NonCodeMeta)| A KCL program top level, or function body. | No |
| `shebang` |[`Shebang`](/docs/kcl/types/Shebang)| | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

23
docs/kcl/types/Shebang.md Normal file
View File

@ -0,0 +1,23 @@
---
title: "Shebang"
excerpt: "A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```"
layout: manual
---
A shebang. This is a special type of comment that is at the top of the file. It looks like this: ```python,no_run #!/usr/bin/env python ```
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `content` |`string`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -1,23 +0,0 @@
---
title: "SweepData"
excerpt: "Data for a sweep."
layout: manual
---
Data for a sweep.
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No |
| `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
| `tolerance` |`number`| Tolerance for the sweep operation. | No |

15
docs/kcl/types/Uint.md Normal file
View File

@ -0,0 +1,15 @@
---
title: "Uint"
excerpt: ""
layout: manual
---
**Type:** `integer` (`uint32`)

View File

@ -0,0 +1,41 @@
---
title: "UnaryOperator"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Negate a number.
**enum:** `-`
----
Negate a boolean.
**enum:** `!`
----

View File

@ -0,0 +1,24 @@
---
title: "VariableDeclarator"
excerpt: ""
layout: manual
---
**Type:** `object`
## Properties
| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `id` |[`Identifier`](/docs/kcl/types/Identifier)| The identifier of the variable. | No |
| `init` |[`Expr`](/docs/kcl/types/Expr)| The value of the variable. | No |
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
| `start` |`integer`| | No |
| `end` |`integer`| | No |

View File

@ -0,0 +1,41 @@
---
title: "VariableKind"
excerpt: ""
layout: manual
---
**This schema accepts exactly one of the following:**
Declare a named constant.
**enum:** `const`
----
Declare a function.
**enum:** `fn`
----

View File

@ -57,26 +57,23 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(u.codeLocator).toContainText( await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)` |> startProfileAt(${commonPoints.startAt}, %)`)
)
} }
await page.waitForTimeout(500) await page.waitForTimeout(500)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(500) await page.waitForTimeout(500)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(u.codeLocator) await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001) |> startProfileAt(${commonPoints.startAt}, %)
|> xLine(${commonPoints.num1}, %)`) |> xLine(${commonPoints.num1}, %)`)
} }
await page.waitForTimeout(500) await page.waitForTimeout(500)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(u.codeLocator) await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ |> startProfileAt(${commonPoints.startAt}, %)
commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %) |> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %)`) |> yLine(${commonPoints.num1 + 0.01}, %)`)
} else { } else {
@ -85,10 +82,8 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
await page.waitForTimeout(200) await page.waitForTimeout(200)
await page.mouse.click(startXPx, 500 - PUR * 20) await page.mouse.click(startXPx, 500 - PUR * 20)
if (openPanes.includes('code')) { if (openPanes.includes('code')) {
await expect(u.codeLocator) await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ |> startProfileAt(${commonPoints.startAt}, %)
commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %) |> xLine(${commonPoints.num1}, %)
|> yLine(${commonPoints.num1 + 0.01}, %) |> yLine(${commonPoints.num1 + 0.01}, %)
|> xLine(${commonPoints.num2 * -1}, %)`) |> xLine(${commonPoints.num2 * -1}, %)`)
@ -145,10 +140,8 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
// Open the code pane. // Open the code pane.
await u.openKclCodePanel() await u.openKclCodePanel()
await expect(u.codeLocator) await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${ |> startProfileAt(${commonPoints.startAt}, %)
commonPoints.startAt
}, sketch001)
|> xLine(${commonPoints.num1}, %, $seg01) |> xLine(${commonPoints.num1}, %, $seg01)
|> yLine(${commonPoints.num1 + 0.01}, %) |> yLine(${commonPoints.num1 + 0.01}, %)
|> xLine(-segLen(seg01), %)`) |> xLine(-segLen(seg01), %)`)

View File

@ -44,7 +44,8 @@ test.describe('Can create sketches on all planes and their back sides', () => {
}, },
} }
const code = `sketch001 = startSketchOn('${plane}')profile001 = startProfileAt([0.9, -1.22], sketch001)` const code = `sketch001 = startSketchOn('${plane}')
|> startProfileAt([0.9, -1.22], %)`
await u.openDebugPanel() await u.openDebugPanel()

View File

@ -94,51 +94,6 @@ test.describe('Editor tests', () => {
|> close(%)`) |> close(%)`)
}) })
test('ensure we use the cache, and do not re-execute', async ({ page }) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1000, height: 500 })
await u.waitForAuthSkipAppStart()
await u.codeLocator.click()
await page.keyboard.type(`sketch001 = startSketchOn('XY')
|> startProfileAt([-10, -10], %)
|> line([20, 0], %)
|> line([0, 20], %)
|> line([-20, 0], %)
|> close(%)`)
// Ensure we execute the first time.
await u.openDebugPanel()
await expect(
page.locator('[data-receive-command-type="scene_clear_all"]')
).toHaveCount(2)
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(2)
// Add whitespace to the end of the code.
await u.codeLocator.click()
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('Home')
await page.keyboard.type(' ')
await page.keyboard.press('Enter')
await page.keyboard.type(' ')
// Ensure we don't execute the second time.
await u.openDebugPanel()
// Make sure we didn't clear the scene.
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(3)
await expect(
page.locator('[data-receive-command-type="scene_clear_all"]')
).toHaveCount(2)
})
test('if you click the format button it formats your code and executes so lints are still there', async ({ test('if you click the format button it formats your code and executes so lints are still there', async ({
page, page,
}) => { }) => {
@ -503,8 +458,8 @@ test.describe('Editor tests', () => {
/* add the following code to the editor ($ error is not a valid line) /* add the following code to the editor ($ error is not a valid line)
$ error $ error
topAng = 30 const topAng = 30
bottomAng = 25 const bottomAng = 25
*/ */
await u.codeLocator.click() await u.codeLocator.click()
await page.keyboard.type('$ error') await page.keyboard.type('$ error')
@ -519,14 +474,12 @@ test.describe('Editor tests', () => {
await page.keyboard.type('bottomAng = 25') await page.keyboard.type('bottomAng = 25')
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
// error in gutter // error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible() await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover // error text on hover
await page.hover('.cm-lint-marker-error') await page.hover('.cm-lint-marker-error')
await expect( await expect(page.getByText('Unexpected token: $').first()).toBeVisible()
page.getByText('Tag names must not be empty').first()
).toBeVisible()
// select the line that's causing the error and delete it // select the line that's causing the error and delete it
await page.getByText('$ error').click() await page.getByText('$ error').click()
@ -565,10 +518,7 @@ test.describe('Editor tests', () => {
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
}) })
// TODO currently multiple source ranges are not supported test('error with 2 source ranges gets 2 diagnostics', async ({ page }) => {
test.skip('error with 2 source ranges gets 2 diagnostics', async ({
page,
}) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(

View File

@ -45,6 +45,7 @@ test.describe('integrations tests', () => {
{ {
title: 'test-sample', title: 'test-sample',
fileCount: 1, fileCount: 1,
folderCount: 1,
}, },
], ],
sortBy: 'last-modified-desc', sortBy: 'last-modified-desc',
@ -232,6 +233,7 @@ test.describe('when using the file tree to', () => {
{ {
title: projectName, title: projectName,
fileCount: 2, fileCount: 2,
folderCount: 2, // TODO: This is a pre-existing bug, there are no folders within the project
}, },
], ],
sortBy: 'last-modified-desc', sortBy: 'last-modified-desc',

View File

@ -4,6 +4,7 @@ import { expect } from '@playwright/test'
interface ProjectCardState { interface ProjectCardState {
title: string title: string
fileCount: number fileCount: number
folderCount: number
} }
interface HomePageState { interface HomePageState {
@ -60,13 +61,15 @@ export class HomePageFixture {
const projectCards = await this.projectCard.all() const projectCards = await this.projectCard.all()
const projectCardStates: Array<ProjectCardState> = [] const projectCardStates: Array<ProjectCardState> = []
for (const projectCard of projectCards) { for (const projectCard of projectCards) {
const [title, fileCount] = await Promise.all([ const [title, fileCount, folderCount] = await Promise.all([
(await projectCard.locator(this.projectCardTitle).textContent()) || '', (await projectCard.locator(this.projectCardTitle).textContent()) || '',
Number(await projectCard.locator(this.projectCardFile).textContent()), Number(await projectCard.locator(this.projectCardFile).textContent()),
Number(await projectCard.locator(this.projectCardFolder).textContent()),
]) ])
projectCardStates.push({ projectCardStates.push({
title: title, title: title,
fileCount, fileCount,
folderCount,
}) })
} }
return projectCardStates return projectCardStates

View File

@ -11,7 +11,6 @@ import {
type mouseParams = { type mouseParams = {
pixelDiff?: number pixelDiff?: number
shouldDbClick?: boolean
} }
type mouseDragToParams = mouseParams & { type mouseDragToParams = mouseParams & {
fromPoint: { x: number; y: number } fromPoint: { x: number; y: number }
@ -29,7 +28,6 @@ type SceneSerialised = {
type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean> type ClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean> type MoveHandler = (moveParams?: mouseParams) => Promise<void | boolean>
type DblClickHandler = (clickParams?: mouseParams) => Promise<void | boolean>
type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean> type DragToHandler = (dragParams: mouseDragToParams) => Promise<void | boolean>
type DragFromHandler = ( type DragFromHandler = (
dragParams: mouseDragFromParams dragParams: mouseDragFromParams
@ -70,22 +68,17 @@ export class SceneFixture {
x: number, x: number,
y: number, y: number,
{ steps }: { steps: number } = { steps: 20 } { steps }: { steps: number } = { steps: 20 }
): [ClickHandler, MoveHandler, DblClickHandler] => ): [ClickHandler, MoveHandler] =>
[ [
(clickParams?: mouseParams) => { (clickParams?: mouseParams) => {
if (clickParams?.pixelDiff) { if (clickParams?.pixelDiff) {
return doAndWaitForImageDiff( return doAndWaitForImageDiff(
this.page, this.page,
() => () => this.page.mouse.click(x, y),
clickParams?.shouldDbClick
? this.page.mouse.dblclick(x, y)
: this.page.mouse.click(x, y),
clickParams.pixelDiff clickParams.pixelDiff
) )
} }
return clickParams?.shouldDbClick return this.page.mouse.click(x, y)
? this.page.mouse.dblclick(x, y)
: this.page.mouse.click(x, y)
}, },
(moveParams?: mouseParams) => { (moveParams?: mouseParams) => {
if (moveParams?.pixelDiff) { if (moveParams?.pixelDiff) {
@ -97,16 +90,6 @@ export class SceneFixture {
} }
return this.page.mouse.move(x, y, { steps }) return this.page.mouse.move(x, y, { steps })
}, },
(clickParams?: mouseParams) => {
if (clickParams?.pixelDiff) {
return doAndWaitForImageDiff(
this.page,
() => this.page.mouse.dblclick(x, y),
clickParams.pixelDiff
)
}
return this.page.mouse.dblclick(x, y)
},
] as const ] as const
makeDragHelpers = ( makeDragHelpers = (
x: number, x: number,
@ -220,7 +203,23 @@ export class SceneFixture {
coords: { x: number; y: number }, coords: { x: number; y: number },
diff: number diff: number
) => { ) => {
await expectPixelColor(this.page, colour, coords, diff) let finalValue = colour
await expect
.poll(async () => {
const pixel = (await getPixelRGBs(this.page)(coords, 1))[0]
if (!pixel) return null
finalValue = pixel
return pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff
)
})
.toBeTruthy()
.catch((cause) => {
throw new Error(
`ExpectPixelColor: expecting ${colour} got ${finalValue}`,
{ cause }
)
})
} }
get gizmo() { get gizmo() {
@ -236,28 +235,3 @@ export class SceneFixture {
await buttonToTest.click() await buttonToTest.click()
} }
} }
export async function expectPixelColor(
page: Page,
colour: [number, number, number],
coords: { x: number; y: number },
diff: number
) {
let finalValue = colour
await expect
.poll(async () => {
const pixel = (await getPixelRGBs(page)(coords, 1))[0]
if (!pixel) return null
finalValue = pixel
return pixel.every(
(channel, index) => Math.abs(channel - colour[index]) < diff
)
})
.toBeTruthy()
.catch((cause) => {
throw new Error(
`ExpectPixelColor: expecting ${colour} got ${finalValue}`,
{ cause }
)
})
}

View File

@ -6,15 +6,10 @@ export class ToolbarFixture {
public page: Page public page: Page
extrudeButton!: Locator extrudeButton!: Locator
loftButton!: Locator
shellButton!: Locator
offsetPlaneButton!: Locator offsetPlaneButton!: Locator
startSketchBtn!: Locator startSketchBtn!: Locator
lineBtn!: Locator lineBtn!: Locator
tangentialArcBtn!: Locator
circleBtn!: Locator
rectangleBtn!: Locator rectangleBtn!: Locator
lengthConstraintBtn!: Locator
exitSketchBtn!: Locator exitSketchBtn!: Locator
editSketchBtn!: Locator editSketchBtn!: Locator
fileTreeBtn!: Locator fileTreeBtn!: Locator
@ -31,15 +26,10 @@ export class ToolbarFixture {
reConstruct = (page: Page) => { reConstruct = (page: Page) => {
this.page = page this.page = page
this.extrudeButton = page.getByTestId('extrude') this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.shellButton = page.getByTestId('shell')
this.offsetPlaneButton = page.getByTestId('plane-offset') this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch') this.startSketchBtn = page.getByTestId('sketch')
this.lineBtn = page.getByTestId('line') this.lineBtn = page.getByTestId('line')
this.tangentialArcBtn = page.getByTestId('tangential-arc')
this.circleBtn = page.getByTestId('circle-center')
this.rectangleBtn = page.getByTestId('corner-rectangle') this.rectangleBtn = page.getByTestId('corner-rectangle')
this.lengthConstraintBtn = page.getByTestId('constraint-length')
this.exitSketchBtn = page.getByTestId('sketch-exit') this.exitSketchBtn = page.getByTestId('sketch-exit')
this.editSketchBtn = page.getByText('Edit Sketch') this.editSketchBtn = page.getByText('Edit Sketch')
this.fileTreeBtn = page.locator('[id="files-button-holder"]') this.fileTreeBtn = page.locator('[id="files-button-holder"]')
@ -97,13 +87,4 @@ export class ToolbarFixture {
await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 }) await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
} }
} }
selectCenterRectangle = async () => {
await this.page
.getByRole('button', { name: 'caret down Corner rectangle:' })
.click()
await expect(
this.page.getByTestId('dropdown-center-rectangle')
).toBeVisible()
await this.page.getByTestId('dropdown-center-rectangle').click()
}
} }

View File

@ -19,7 +19,6 @@ import {
TEST_SETTINGS_ONBOARDING_USER_MENU, TEST_SETTINGS_ONBOARDING_USER_MENU,
} from './storageStates' } from './storageStates'
import * as TOML from '@iarna/toml' import * as TOML from '@iarna/toml'
import { expectPixelColor } from './fixtures/sceneFixture'
test.beforeEach(async ({ context, page }, testInfo) => { test.beforeEach(async ({ context, page }, testInfo) => {
if (testInfo.tags.includes('@electron')) { if (testInfo.tags.includes('@electron')) {
@ -46,7 +45,7 @@ test.describe('Onboarding tests', () => {
{ settingsKey: TEST_SETTINGS_KEY } { settingsKey: TEST_SETTINGS_KEY }
) )
await page.setViewportSize({ width: 1200, height: 1000 }) await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
@ -55,12 +54,6 @@ test.describe('Onboarding tests', () => {
// *and* that the code is shown in the editor // *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket') await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// Make sure the model loaded
const XYPlanePoint = { x: 774, y: 116 } as const
const modelColor: [number, number, number] = [45, 45, 45]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(8)
}) })
test( test(
@ -79,7 +72,7 @@ test.describe('Onboarding tests', () => {
const u = await getUtils(page) const u = await getUtils(page)
const viewportSize = { width: 1200, height: 1000 } const viewportSize = { width: 1200, height: 500 }
await page.setViewportSize(viewportSize) await page.setViewportSize(viewportSize)
await test.step(`Create a project and open to the onboarding`, async () => { await test.step(`Create a project and open to the onboarding`, async () => {
@ -99,14 +92,6 @@ test.describe('Onboarding tests', () => {
await expect(page.locator('.cm-content')).toContainText( await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket' '// Shelf Bracket'
) )
// TODO: jess make less shit
// Make sure the model loaded
//const XYPlanePoint = { x: 986, y: 522 } as const
//const modelColor: [number, number, number] = [76, 76, 76]
//await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
//await expectPixelColor(page, modelColor, XYPlanePoint, 8)
}) })
await electronApp.close() await electronApp.close()
@ -123,7 +108,7 @@ test.describe('Onboarding tests', () => {
}, initialCode) }, initialCode)
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 1000 }) await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
// Replay the onboarding // Replay the onboarding
@ -155,12 +140,6 @@ test.describe('Onboarding tests', () => {
return localStorage.getItem('persistCode') return localStorage.getItem('persistCode')
}) })
).toContain('// Shelf Bracket') ).toContain('// Shelf Bracket')
// Make sure the model loaded
const XYPlanePoint = { x: 986, y: 522 } as const
const modelColor: [number, number, number] = [76, 76, 76]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
}) })
test('Click through each onboarding step', async ({ page }) => { test('Click through each onboarding step', async ({ page }) => {
@ -200,17 +179,6 @@ test.describe('Onboarding tests', () => {
// Test that the onboarding pane is gone // Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible() await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect(page.url()).not.toContain('onboarding') await expect(page.url()).not.toContain('onboarding')
await u.openAndClearDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
// TODO: jess to fix
// Make sure the model loaded
//const XYPlanePoint = { x: 774, y: 516 } as const
// const modelColor: [number, number, number] = [129, 129, 129]
// await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
// await expectPixelColor(page, modelColor, XYPlanePoint, 20)
}) })
test('Onboarding redirects and code updating', async ({ page }) => { test('Onboarding redirects and code updating', async ({ page }) => {
@ -471,7 +439,7 @@ test(
}) })
await test.step('Navigate into project', async () => { await test.step('Navigate into project', async () => {
await page.setViewportSize({ width: 1200, height: 1000 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
@ -494,15 +462,7 @@ test(
await test.step('Confirm that the onboarding has restarted', async () => { await test.step('Confirm that the onboarding has restarted', async () => {
await expect(tutorialProjectIndicator).toBeVisible() await expect(tutorialProjectIndicator).toBeVisible()
await expect(tutorialModalText).toBeVisible() await expect(tutorialModalText).toBeVisible()
// Make sure the model loaded
const XYPlanePoint = { x: 988, y: 523 } as const
const modelColor: [number, number, number] = [76, 76, 76]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
await tutorialDismissButton.click() await tutorialDismissButton.click()
// Make sure model still there.
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
}) })
await test.step('Clear code and restart onboarding from settings', async () => { await test.step('Clear code and restart onboarding from settings', async () => {

View File

@ -135,9 +135,7 @@ test.describe('verify sketch on chamfer works', () => {
pixelDiff: 50, pixelDiff: 50,
}) })
await rectangle2ndClick() await rectangle2ndClick()
await editor.expectEditor.toContain(afterRectangle2ndClickSnippet, { await editor.expectEditor.toContain(afterRectangle2ndClickSnippet)
shouldNormalise: true,
})
}) })
await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => { await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => {
@ -179,11 +177,16 @@ test.describe('verify sketch on chamfer works', () => {
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch002 = startSketchOn(extrude001, seg03)', 'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
'startProfileAt([205.96, 254.59], sketch002)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%) |> angledLine([
|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%) segAng(rectangleSegmentA002) - 90,
105.26
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`, |> close(%)`,
}) })
@ -206,15 +209,19 @@ test.describe('verify sketch on chamfer works', () => {
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch003 = startSketchOn(extrude001, seg04)', 'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
'startProfileAt([-209.64, 255.28], sketch003)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003) afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|>angledLine([segAng(rectangleSegmentA003)-90,106.84],%) |> angledLine([
|>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%) segAng(rectangleSegmentA003) - 90,
106.84
], %, $rectangleSegmentB002)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`, |> close(%)`,
}) })
await sketchOnAChamfer({ await sketchOnAChamfer({
clickCoords: { x: 677, y: 87 }, clickCoords: { x: 677, y: 87 },
cameraPos: { x: -6200, y: 1500, z: 6200 }, cameraPos: { x: -6200, y: 1500, z: 6200 },
@ -227,14 +234,19 @@ test.describe('verify sketch on chamfer works', () => {
] ]
}, %)`, }, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch004 = startSketchOn(extrude001, seg05)', 'sketch003 = startSketchOn(extrude001, seg04)',
afterRectangle1stClickSnippet: afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
'startProfileAt([82.57, 322.96], sketch004)', afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
afterRectangle2ndClickSnippet: `angledLine([0,11.16],%,$rectangleSegmentA004) |> angledLine([
|>angledLine([segAng(rectangleSegmentA004)-90,103.07],%) segAng(rectangleSegmentA003) - 90,
|>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%) 106.84
|>lineTo([profileStartX(%),profileStartY(%)],%)| ], %, $rectangleSegmentB002)
>close(%)`, |> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`,
}) })
/// last one /// last one
await sketchOnAChamfer({ await sketchOnAChamfer({
@ -247,11 +259,17 @@ test.describe('verify sketch on chamfer works', () => {
}, %)`, }, %)`,
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch005 = startSketchOn(extrude001, seg06)', 'sketch005 = startSketchOn(extrude001, seg06)',
afterRectangle1stClickSnippet: afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
'startProfileAt([-23.43, 19.69], sketch005)',
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005) afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
|>angledLine([segAng(rectangleSegmentA005)-90,84.07],%)
|>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%) |> angledLine([
segAng(rectangleSegmentA005) - 90,
84.07
], %, $rectangleSegmentB004)
|> angledLine([
segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005)
], %, $rectangleSegmentC004)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`, |> close(%)`,
}) })
@ -259,6 +277,7 @@ test.describe('verify sketch on chamfer works', () => {
await test.step('verify at the end of the test that final code is what is expected', async () => { await test.step('verify at the end of the test that final code is what is expected', async () => {
await editor.expectEditor.toContain( await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag] |> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|> angledLine([0, 268.43], %, $rectangleSegmentA001) |> angledLine([0, 268.43], %, $rectangleSegmentA001)
|> angledLine([ |> angledLine([
@ -286,55 +305,55 @@ extrude001 = extrude(100, sketch001)
tags = [getNextAdjacentEdge(yo)] tags = [getNextAdjacentEdge(yo)]
}, %, $seg06) }, %, $seg06)
sketch005 = startSketchOn(extrude001, seg06) sketch005 = startSketchOn(extrude001, seg06)
profile004 = startProfileAt([-23.43, 19.69], sketch005) |> startProfileAt([-23.43, 19.69], %)
|> angledLine([0, 9.1], %, $rectangleSegmentA005) |> angledLine([0, 9.1], %, $rectangleSegmentA005)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA005) - 90, segAng(rectangleSegmentA005) - 90,
84.07 84.07
], %) ], %, $rectangleSegmentB004)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA005), segAng(rectangleSegmentA005),
-segLen(rectangleSegmentA005) -segLen(rectangleSegmentA005)
], %) ], %, $rectangleSegmentC004)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch004 = startSketchOn(extrude001, seg05) sketch004 = startSketchOn(extrude001, seg05)
profile003 = startProfileAt([82.57, 322.96], sketch004) |> startProfileAt([82.57, 322.96], %)
|> angledLine([0, 11.16], %, $rectangleSegmentA004) |> angledLine([0, 11.16], %, $rectangleSegmentA004)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA004) - 90, segAng(rectangleSegmentA004) - 90,
103.07 103.07
], %) ], %, $rectangleSegmentB003)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA004), segAng(rectangleSegmentA004),
-segLen(rectangleSegmentA004) -segLen(rectangleSegmentA004)
], %) ], %, $rectangleSegmentC003)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch003 = startSketchOn(extrude001, seg04) sketch003 = startSketchOn(extrude001, seg04)
profile002 = startProfileAt([-209.64, 255.28], sketch003) |> startProfileAt([-209.64, 255.28], %)
|> angledLine([0, 11.56], %, $rectangleSegmentA003) |> angledLine([0, 11.56], %, $rectangleSegmentA003)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA003) - 90, segAng(rectangleSegmentA003) - 90,
106.84 106.84
], %) ], %, $rectangleSegmentB002)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA003), segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003) -segLen(rectangleSegmentA003)
], %) ], %, $rectangleSegmentC002)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
sketch002 = startSketchOn(extrude001, seg03) sketch002 = startSketchOn(extrude001, seg03)
profile001 = startProfileAt([205.96, 254.59], sketch002) |> startProfileAt([205.96, 254.59], %)
|> angledLine([0, 11.39], %, $rectangleSegmentA002) |> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002) - 90, segAng(rectangleSegmentA002) - 90,
105.26 105.26
], %) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002), segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002) -segLen(rectangleSegmentA002)
], %) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
`, `,
@ -373,11 +392,16 @@ profile001 = startProfileAt([205.96, 254.59], sketch002)
beforeChamferSnippetEnd: '}, extrude001)', beforeChamferSnippetEnd: '}, extrude001)',
afterChamferSelectSnippet: afterChamferSelectSnippet:
'sketch002 = startSketchOn(extrude001, seg03)', 'sketch002 = startSketchOn(extrude001, seg03)',
afterRectangle1stClickSnippet: afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
'startProfileAt([205.96, 254.59], sketch002)',
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002) afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%) |> angledLine([
|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%) segAng(rectangleSegmentA002) - 90,
105.26
], %, $rectangleSegmentB001)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`, |> close(%)`,
}) })
@ -409,16 +433,16 @@ chamf = chamfer({
] ]
}, %) }, %)
sketch002 = startSketchOn(extrude001, seg03) sketch002 = startSketchOn(extrude001, seg03)
profile001 = startProfileAt([205.96, 254.59], sketch002) |> startProfileAt([205.96, 254.59], %)
|> angledLine([0, 11.39], %, $rectangleSegmentA002) |> angledLine([0, 11.39], %, $rectangleSegmentA002)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002) - 90, segAng(rectangleSegmentA002) - 90,
105.26 105.26
], %) ], %, $rectangleSegmentB001)
|> angledLine([ |> angledLine([
segAng(rectangleSegmentA002), segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002) -segLen(rectangleSegmentA002)
], %) ], %, $rectangleSegmentC001)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%) |> close(%)
`, `,
@ -480,10 +504,10 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
const expectedCodeSnippets = { const expectedCodeSnippets = {
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`, sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], sketch001)`, pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`, segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], sketch001)`, afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], sketch001)`, afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
} }
await app.initialise() await app.initialise()
@ -528,82 +552,6 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
}) })
}) })
test(`Verify user can double-click to edit a sketch`, async ({
app,
editor,
toolbar,
scene,
}) => {
const initialCode = `closedSketch = startSketchOn('XZ')
|> circle({ center = [8, 5], radius = 2 }, %)
openSketch = startSketchOn('XY')
|> startProfileAt([-5, 0], %)
|> lineTo([0, 5], %)
|> xLine(5, %)
|> tangentialArcTo([10, 0], %)
`
await app.initialise(initialCode)
const pointInsideCircle = {
x: app.viewPortSize.width * 0.63,
y: app.viewPortSize.height * 0.5,
}
const pointOnPathAfterSketching = {
x: app.viewPortSize.width * 0.58,
y: app.viewPortSize.height * 0.5,
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_clickOpenPath, moveToOpenPath, dblClickOpenPath] =
scene.makeMouseHelpers(
pointOnPathAfterSketching.x,
pointOnPathAfterSketching.y
)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_clickCircle, moveToCircle, dblClickCircle] = scene.makeMouseHelpers(
pointInsideCircle.x,
pointInsideCircle.y
)
const exitSketch = async () => {
await test.step(`Exit sketch mode`, async () => {
await toolbar.exitSketchBtn.click()
await expect(toolbar.exitSketchBtn).not.toBeVisible()
await expect(toolbar.startSketchBtn).toBeEnabled()
})
}
await test.step(`Double-click on the closed sketch`, async () => {
await moveToCircle()
await dblClickCircle()
await expect(toolbar.startSketchBtn).not.toBeVisible()
await expect(toolbar.exitSketchBtn).toBeVisible()
await editor.expectState({
activeLines: [`|>circle({center=[8,5],radius=2},%)`],
highlightedCode: 'circle({center=[8,5],radius=2},%)',
diagnostics: [],
})
})
await exitSketch()
await test.step(`Double-click on the open sketch`, async () => {
await moveToOpenPath()
await scene.expectPixelColor([250, 250, 250], pointOnPathAfterSketching, 15)
// There is a full execution after exiting sketch that clears the scene.
await app.page.waitForTimeout(500)
await dblClickOpenPath()
await expect(toolbar.startSketchBtn).not.toBeVisible()
await expect(toolbar.exitSketchBtn).toBeVisible()
// Wait for enter sketch mode to complete
await app.page.waitForTimeout(500)
await editor.expectState({
activeLines: [`|>xLine(5,%)`],
highlightedCode: 'xLine(5,%)',
diagnostics: [],
})
})
})
test(`Offset plane point-and-click`, async ({ test(`Offset plane point-and-click`, async ({
app, app,
scene, scene,
@ -653,259 +601,3 @@ test(`Offset plane point-and-click`, async ({
await scene.expectPixelColor([74, 74, 74], testPoint, 15) await scene.expectPixelColor([74, 74, 74], testPoint, 15)
}) })
}) })
const loftPointAndClickCases = [
{ shouldPreselect: true },
{ shouldPreselect: false },
]
loftPointAndClickCases.forEach(({ shouldPreselect }) => {
test(`Loft point-and-click (preselected sketches: ${shouldPreselect})`, async ({
app,
page,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %)
plane001 = offsetPlane('XZ', 50)
sketch002 = startSketchOn(plane001)
|> circle({ center = [0, 0], radius = 20 }, %)
`
await app.initialise(initialCode)
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(
testPoint.x,
testPoint.y + 80
)
const loftDeclaration = 'loft001 = loft([sketch001, sketch002])'
await test.step(`Look for the white of the sketch001 shape`, async () => {
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
})
async function selectSketches() {
await clickOnSketch1()
await page.keyboard.down('Shift')
await clickOnSketch2()
await app.page.waitForTimeout(500)
await page.keyboard.up('Shift')
}
if (!shouldPreselect) {
await test.step(`Go through the command bar flow without preselected sketches`, async () => {
await toolbar.loftButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: { Selection: '' },
highlightedHeaderArg: 'selection',
commandName: 'Loft',
})
await selectSketches()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Selection: '2 faces' },
commandName: 'Loft',
})
await cmdBar.progressCmdBar()
})
} else {
await test.step(`Preselect the two sketches`, async () => {
await selectSketches()
})
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
await toolbar.loftButton.click()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Selection: '2 faces' },
commandName: 'Loft',
})
await cmdBar.progressCmdBar()
})
}
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(loftDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [loftDeclaration],
highlightedCode: '',
})
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
})
})
})
const shellPointAndClickCapCases = [
{ shouldPreselect: true },
{ shouldPreselect: false },
]
shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
test(`Shell point-and-click cap (preselected sketches: ${shouldPreselect})`, async ({
app,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 30 }, %)
extrude001 = extrude(30, sketch001)
`
await app.initialise(initialCode)
// One dumb hardcoded screen pixel value
const testPoint = { x: 575, y: 200 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const shellDeclaration =
"shell001 = shell({ faces = ['end'], thickness = 5 }, extrude001)"
await test.step(`Look for the grey of the shape`, async () => {
await scene.expectPixelColor([127, 127, 127], testPoint, 15)
})
if (!shouldPreselect) {
await test.step(`Go through the command bar flow without preselected faces`, async () => {
await toolbar.shellButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Thickness: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Shell',
})
await clickOnCap()
await app.page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 cap',
Thickness: '5',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
})
} else {
await test.step(`Preselect the cap`, async () => {
await clickOnCap()
await app.page.waitForTimeout(500)
})
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
await toolbar.shellButton.click()
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 cap',
Thickness: '5',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
})
}
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(shellDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [shellDeclaration],
highlightedCode: '',
})
await scene.expectPixelColor([146, 146, 146], testPoint, 15)
})
})
})
test('Shell point-and-click wall', async ({
app,
page,
scene,
editor,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('XY')
|> startProfileAt([-20, 20], %)
|> xLine(40, %)
|> yLine(-60, %)
|> xLine(-40, %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(40, sketch001)
`
await app.initialise(initialCode)
// One dumb hardcoded screen pixel value
const testPoint = { x: 580, y: 180 }
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70)
const mutatedCode = 'xLine(-40, %, $seg01)'
const shellDeclaration =
"shell001 = shell({ faces = ['end', seg01], thickness = 5}, extrude001)"
const formattedOutLastLine = '}, extrude001)'
await test.step(`Look for the grey of the shape`, async () => {
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
})
await test.step(`Go through the command bar flow, selecting a wall and keeping default thickness`, async () => {
await toolbar.shellButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'selection',
currentArgValue: '',
headerArguments: {
Selection: '',
Thickness: '',
},
highlightedHeaderArg: 'selection',
commandName: 'Shell',
})
await clickOnCap()
await page.keyboard.down('Shift')
await clickOnWall()
await app.page.waitForTimeout(500)
await page.keyboard.up('Shift')
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Selection: '1 cap, 1 face',
Thickness: '5',
},
commandName: 'Shell',
})
await cmdBar.progressCmdBar()
})
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
await editor.expectEditor.toContain(mutatedCode)
await editor.expectEditor.toContain(shellDeclaration)
await editor.expectState({
diagnostics: [],
activeLines: [formattedOutLastLine],
highlightedCode: '',
})
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
})
})

View File

@ -136,335 +136,6 @@ test(
} }
) )
test(
'open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
const errorDir = join(dir, 'broken-code')
await fsp.mkdir(errorDir, { recursive: true })
await fsp.copyFile(
executorInputPath('broken-code-test.kcl'),
join(errorDir, 'main.kcl')
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
await page.getByTestId('app-logo').click()
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('broken-code')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('opening broken code project should clear the scene and show the error', async () => {
// Go back home.
await expect(page.getByText('broken-code')).toBeVisible()
await page.getByText('broken-code').click()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
const crypticErrorText = `Expected a tag declarator`
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [30, 30, 30]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await electronApp.close()
}
)
test(
'open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
const emptyDir = join(dir, 'empty')
await fsp.mkdir(emptyDir, { recursive: true })
await fsp.writeFile(join(emptyDir, 'main.kcl'), '')
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
await page.getByTestId('app-logo').click()
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
await expect(page.getByText('empty')).toBeVisible()
await expect(page.getByText('bracket')).toBeVisible()
await expect(page.getByText('New Project')).toBeVisible()
})
await test.step('opening empty code project should clear the scene', async () => {
// Go back home.
await expect(page.getByText('empty')).toBeVisible()
await page.getByText('empty').click()
// Ensure the code is empty.
await expect(u.codeLocator).toContainText('')
expect(u.codeLocator.innerHTML.length).toBeLessThan(2)
// planes colors means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [92, 53, 53]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await electronApp.close()
}
)
test(
'open a file in a project works and renders, open empty file, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
await fsp.writeFile(join(bracketDir, 'empty.kcl'), '')
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('creating a empty file should clear the scene', async () => {
// open the file pane.
await page.getByTestId('files-pane-button').click()
// OPen the other file.
const file = page.getByRole('button', { name: 'empty.kcl' })
await expect(file).toBeVisible()
await file.click()
// planes colors means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [92, 53, 53]), {
timeout: 10_000,
})
.toBeLessThan(15)
// Ensure the code is empty.
await expect(u.codeLocator).toContainText('')
expect(u.codeLocator.innerHTML.length).toBeLessThan(2)
})
await electronApp.close()
}
)
test(
'open a file in a project works and renders, open another file in the same project with errors, it should clear the scene',
{ tag: '@electron' },
async ({ browserName }, testInfo) => {
const { electronApp, page } = await setupElectron({
testInfo,
folderSetupFn: async (dir) => {
const bracketDir = join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true })
await fsp.copyFile(
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
join(bracketDir, 'main.kcl')
)
await fsp.copyFile(
executorInputPath('broken-code-test.kcl'),
join(bracketDir, 'broken-code-test.kcl')
)
},
})
await page.setViewportSize({ width: 1200, height: 500 })
const u = await getUtils(page)
page.on('console', console.log)
const pointOnModel = { x: 630, y: 280 }
await test.step('Opening the bracket project should load the stream', async () => {
// expect to see the text bracket
await expect(page.getByText('bracket')).toBeVisible()
await page.getByText('bracket').click()
await expect(page.getByTestId('loading')).toBeAttached()
await expect(page.getByTestId('loading')).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).toBeEnabled({
timeout: 20_000,
})
// gray at this pixel means the stream has loaded in the most
// user way we can verify it (pixel color)
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await test.step('opening broken code file should clear the scene and show the error', async () => {
// open the file pane.
await page.getByTestId('files-pane-button').click()
// OPen the other file.
const file = page.getByRole('button', { name: 'broken-code-test.kcl' })
await expect(file).toBeVisible()
await file.click()
// error in guter
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
// error text on hover
await page.hover('.cm-lint-marker-error')
const crypticErrorText = `Expected a tag declarator`
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
// black pixel means the scene has been cleared.
await expect
.poll(() => u.getGreatestPixDiff(pointOnModel, [30, 30, 30]), {
timeout: 10_000,
})
.toBeLessThan(15)
})
await electronApp.close()
}
)
test( test(
'when code with error first loads you get errors in console', 'when code with error first loads you get errors in console',
{ tag: '@electron' }, { tag: '@electron' },

View File

@ -550,7 +550,7 @@ sketch001 = startSketchAt([-0, -0])
const u = await getUtils(page) const u = await getUtils(page)
// Constants and locators // Constants and locators
const planeColor: [number, number, number] = [161, 220, 155] const planeColor: [number, number, number] = [170, 220, 170]
const bgColor: [number, number, number] = [27, 27, 27] const bgColor: [number, number, number] = [27, 27, 27]
const middlePixelIsColor = async (color: [number, number, number]) => { const middlePixelIsColor = async (color: [number, number, number]) => {
return u.getGreatestPixDiff({ x: 600, y: 250 }, color) return u.getGreatestPixDiff({ x: 600, y: 250 }, color)

View File

@ -7,8 +7,6 @@ try {
.split('\n') .split('\n')
.filter((line) => line && line.length > 1) .filter((line) => line && line.length > 1)
.forEach((line) => { .forEach((line) => {
// Allow line comments.
if (line.trimStart().startsWith('#')) return
const [key, value] = line.split('=') const [key, value] = line.split('=')
// prefer env vars over secrets file // prefer env vars over secrets file
secrets[key] = process.env[key] || (value as any).replaceAll('"', '') secrets[key] = process.env[key] || (value as any).replaceAll('"', '')

View File

@ -114,9 +114,9 @@ test.describe('Sketch tests', () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
`sketch001 = startSketchOn('XZ') `sketch001 = startSketchOn('XZ')
|> startProfileAt([2.61, -4.01], %) |> startProfileAt([4.61, -14.01], %)
|> xLine(8.73, %) |> xLine(12.73, %)
|> tangentialArcTo([8.33, -1.31], %)` |> tangentialArcTo([24.95, -5.38], %)`
) )
}) })
@ -126,7 +126,7 @@ test.describe('Sketch tests', () => {
await expect(async () => { await expect(async () => {
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await page.getByText('tangentialArcTo([8.33, -1.31], %)').click() await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
await expect( await expect(
page.getByRole('button', { name: 'Edit Sketch' }) page.getByRole('button', { name: 'Edit Sketch' })
).toBeEnabled({ timeout: 1000 }) ).toBeEnabled({ timeout: 1000 })
@ -135,7 +135,7 @@ test.describe('Sketch tests', () => {
await page.waitForTimeout(600) // wait for animation await page.waitForTimeout(600) // wait for animation
await page.getByText('tangentialArcTo([8.33, -1.31], %)').click() await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
await page.keyboard.press('End') await page.keyboard.press('End')
await page.keyboard.down('Shift') await page.keyboard.down('Shift')
await page.keyboard.press('ArrowUp') await page.keyboard.press('ArrowUp')
@ -149,21 +149,17 @@ test.describe('Sketch tests', () => {
await page.getByRole('button', { name: 'line Line', exact: true }).click() await page.getByRole('button', { name: 'line Line', exact: true }).click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// click start profileAt handle to continue profile
await page.mouse.click(702, 407)
await page.waitForTimeout(100)
await expect(async () => { await expect(async () => {
// click to add segment
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await expect.poll(u.normalisedEditorCode, { timeout: 1000 }) await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
.toBe(`sketch002 = startSketchOn('XZ') .toBe(`sketch001 = startSketchOn('XZ')
sketch001 = startProfileAt([12.34, -12.34], sketch002) |> startProfileAt([12.34, -12.34], %)
|> yLine(12.34, %) |> yLine(12.34, %)
`) `)
}).toPass({ timeout: 5_000, intervals: [1_000] }) }).toPass({ timeout: 40_000, intervals: [1_000] })
}) })
test('Can exit selection of face', async ({ page }) => { test('Can exit selection of face', async ({ page }) => {
// Load the app with the code panes // Load the app with the code panes
@ -673,7 +669,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
await click00r(0, 0) await click00r(0, 0)
codeStr += `profile001 = startProfileAt(${toSU([0, 0])}, sketch001)` codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
await click00r(50, 0) await click00r(50, 0)
@ -709,7 +705,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
await u.closeDebugPanel() await u.closeDebugPanel()
await click00r(30, 0) await click00r(30, 0)
codeStr += `profile002 = startProfileAt([2.03, 0], sketch002)` codeStr += ` |> startProfileAt([2.03, 0], %)`
await expect(u.codeLocator).toHaveText(codeStr) await expect(u.codeLocator).toHaveText(codeStr)
// TODO: I couldn't use `toSU` here because of some rounding error causing // TODO: I couldn't use `toSU` here because of some rounding error causing
@ -746,9 +742,7 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
await u.openDebugPanel() await u.openDebugPanel()
const code = `sketch001 = startSketchOn('-XZ') const code = `sketch001 = startSketchOn('-XZ')
profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff( |> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %)
scale * 34.8
)}], sketch001)
|> xLine(${roundOff(scale * 139.19)}, %) |> xLine(${roundOff(scale * 139.19)}, %)
|> yLine(-${roundOff(scale * 139.2)}, %) |> yLine(-${roundOff(scale * 139.2)}, %)
|> lineTo([profileStartX(%), profileStartY(%)], %) |> lineTo([profileStartX(%), profileStartY(%)], %)
@ -814,17 +808,11 @@ profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
await expect(page.locator('.cm-content')).not.toHaveText(prevContent) await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
prevContent = await page.locator('.cm-content').innerText() prevContent = await page.locator('.cm-content').innerText()
await expect await expect(page.locator('.cm-content')).toHaveText(code)
.poll(async () => { // Assert the tool was unequipped
const text = await page.locator('.cm-content').innerText()
return text.replace(/\s/g, '')
})
.toBe(code.replace(/\s/g, ''))
// Assert the tool stays equipped after a profile is closed (ready for the next one)
await expect( await expect(
page.getByRole('button', { name: 'line Line', exact: true }) page.getByRole('button', { name: 'line Line', exact: true })
).toHaveAttribute('aria-pressed', 'true') ).not.toHaveAttribute('aria-pressed', 'true')
// exit sketch // exit sketch
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
@ -955,110 +943,6 @@ sketch002 = startSketchOn(extrude001, 'END')
`.replace(/\s/g, '') `.replace(/\s/g, '')
) )
}) })
/* TODO: once we fix bug turn on.
test('empty-scene default-planes act as expected when spaces in file', async ({
page,
browserName,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const XYPlanePoint = { x: 774, y: 116 } as const
const unHoveredColor: [number, number, number] = [47, 47, 93]
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await u.openAndClearDebugPanel()
// Fill with spaces
await u.codeLocator.fill(`
`)
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
})
test('empty-scene default-planes act as expected when only code comments in file', async ({
page,
browserName,
}) => {
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const XYPlanePoint = { x: 774, y: 116 } as const
const unHoveredColor: [number, number, number] = [47, 47, 93]
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await u.openAndClearDebugPanel()
// Fill with spaces
await u.codeLocator.fill(`// this is a code comments ya nerds
`)
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200)
// color should not change for having been hovered
expect(
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
).toBeLessThan(8)
})*/
test('empty-scene default-planes act as expected', async ({ test('empty-scene default-planes act as expected', async ({
page, page,
browserName, browserName,
@ -1142,17 +1026,11 @@ sketch002 = startSketchOn(extrude001, 'END')
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y) await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
await page.waitForTimeout(200) await page.waitForTimeout(200)
await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50) await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50)
await expect await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
.poll(async () => { |> startProfileAt([11.8, 9.09], %)
const text = await u.codeLocator.innerText()
return text.replace(/\s/g, '')
})
.toBe(
`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([11.8, 9.09], sketch001)
|> line([3.39, -3.39], %) |> line([3.39, -3.39], %)
`.replace(/\s/g, '') `)
)
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
'persistCode', 'persistCode',
@ -1437,560 +1315,3 @@ test2.describe(`Sketching with offset planes`, () => {
} }
) )
}) })
test2.describe('multi-profile sketching', () => {
test2(
'Can add multiple profiles to a sketch (all tool types)',
async ({ app, scene, toolbar, editor }) => {
await app.initialise(``)
const [selectXZPlane] = scene.makeMouseHelpers(650, 150)
const [startProfile1] = scene.makeMouseHelpers(568, 70)
const [endLineStartTanArc] = scene.makeMouseHelpers(701, 78)
const [endArcStartLine] = scene.makeMouseHelpers(745, 189)
const [startProfile2] = scene.makeMouseHelpers(782, 80)
const [profile2Point2] = scene.makeMouseHelpers(921, 90)
const [profile2Point3] = scene.makeMouseHelpers(953, 178)
const [circle1Center] = scene.makeMouseHelpers(842, 147)
const [circle1Radius] = scene.makeMouseHelpers(870, 171)
const [circle2Center] = scene.makeMouseHelpers(850, 222)
const [circle2Radius] = scene.makeMouseHelpers(843, 230)
const [crnRect1point1] = scene.makeMouseHelpers(583, 205)
const [crnRect1point2] = scene.makeMouseHelpers(618, 320)
const [crnRect2point1] = scene.makeMouseHelpers(663, 215)
const [crnRect2point2] = scene.makeMouseHelpers(744, 276)
const [cntrRect1point1] = scene.makeMouseHelpers(624, 387)
const [cntrRect1point2] = scene.makeMouseHelpers(676, 355)
const [cntrRect2point1] = scene.makeMouseHelpers(785, 332)
const [cntrRect2point2] = scene.makeMouseHelpers(808, 286)
await toolbar.startSketchPlaneSelection()
await selectXZPlane()
// timeout wait for engine animation is unavoidable
await app.page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
await test.step('Create a close profile stopping mid profile to equip the tangential arc, and than back to the line tool', async () => {
await startProfile1()
await editor.expectEditor.toContain(
`profile001 = startProfileAt([4.61, 12.21], sketch001)`
)
await endLineStartTanArc()
await editor.expectEditor.toContain(`|> line([9.02, -0.55], %)`)
await toolbar.tangentialArcBtn.click()
await app.page.waitForTimeout(100)
await endLineStartTanArc()
await endArcStartLine()
await editor.expectEditor.toContain(
`|> tangentialArcTo([16.61, 4.14], %)`
)
await toolbar.lineBtn.click()
await app.page.waitForTimeout(100)
await endArcStartLine()
await app.page.mouse.click(572, 110)
await editor.expectEditor.toContain(`|> line([-11.73, 5.35], %)`)
await startProfile1()
await editor.expectEditor
.toContain(`|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`)
await app.page.waitForTimeout(100)
})
await test.step('Without unequipping from the last step, make another profile, and one that is not closed', async () => {
await startProfile2()
await editor.expectEditor.toContain(
`profile002 = startProfileAt([19.12, 11.53], sketch001)`
)
await profile2Point2()
await editor.expectEditor.toContain(`|> line([9.43, -0.68], %)`)
await profile2Point3()
await editor.expectEditor.toContain(`|> line([2.17, -5.97], %)`)
})
await test.step('create two circles in a row without unequip', async () => {
await toolbar.circleBtn.click()
await circle1Center()
await app.page.waitForTimeout(100)
await circle1Radius()
await editor.expectEditor.toContain(
`profile003 = circle({ center = [23.19, 6.98], radius = 2.5 }, sketch001)`
)
await test.step('hover in empty space to wait for overlays to get out of the way', async () => {
await app.page.mouse.move(951, 223)
await app.page.waitForTimeout(1000)
})
await circle2Center()
await app.page.waitForTimeout(100)
await circle2Radius()
await editor.expectEditor.toContain(
`profile004 = circle({ center = [23.74, 1.9], radius = 0.72 }, sketch001)`
)
})
await test.step('create two corner rectangles in a row without unequip', async () => {
await toolbar.rectangleBtn.click()
await crnRect1point1()
await editor.expectEditor.toContain(
`profile005 = startProfileAt([5.63, 3.05], sketch001)`
)
await crnRect1point2()
await editor.expectEditor
.toContain(`|> angledLine([0, 2.37], %, $rectangleSegmentA001)
|> angledLine([segAng(rectangleSegmentA001) - 90, 7.8], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`)
await app.page.waitForTimeout(100)
await crnRect2point1()
await editor.expectEditor.toContain(
`profile006 = startProfileAt([11.05, 2.37], sketch001)`
)
await crnRect2point2()
await editor.expectEditor
.toContain(`|> angledLine([0, 5.49], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) - 90,
4.14
], %)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`)
})
await test.step('create two center rectangles in a row without unequip', async () => {
await toolbar.selectCenterRectangle()
await cntrRect1point1()
await editor.expectEditor.toContain(
`profile007 = startProfileAt([8.41, -9.29], sketch001)`
)
await cntrRect1point2()
await editor.expectEditor
.toContain(`|> angledLine([0, 7.06], %, $rectangleSegmentA003)
|> angledLine([
segAng(rectangleSegmentA003) + 90,
4.34
], %)
|> angledLine([
segAng(rectangleSegmentA003),
-segLen(rectangleSegmentA003)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`)
await app.page.waitForTimeout(100)
await cntrRect2point1()
await editor.expectEditor.toContain(
`profile008 = startProfileAt([19.33, -5.56], sketch001)`
)
await cntrRect2point2()
await editor.expectEditor
.toContain(`|> angledLine([0, 3.12], %, $rectangleSegmentA004)
|> angledLine([
segAng(rectangleSegmentA004) + 90,
6.24
], %)
|> angledLine([
segAng(rectangleSegmentA004),
-segLen(rectangleSegmentA004)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`)
})
}
)
test2(
'Can edit a sketch with multiple profiles, dragging segments to edit them, and adding one new profile',
async ({ app, scene, toolbar, editor }) => {
await app.initialise(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([6.24, 4.54], sketch001)
|> line([-0.41, 6.99], %)
|> line([8.61, 0.74], %)
|> line([10.99, -5.22], %)
profile002 = startProfileAt([11.19, 5.02], sketch001)
|> angledLine([0, 10.78], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
4.14
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
`)
const [pointOnSegment] = scene.makeMouseHelpers(590, 141)
const [profileEnd] = scene.makeMouseHelpers(970, 105)
const profileEndMv = scene.makeMouseHelpers(951, 101)[1]
const [newProfileEnd] = scene.makeMouseHelpers(764, 104)
const dragSegmentTo = scene.makeMouseHelpers(850, 104)[1]
const rectHandle = scene.makeMouseHelpers(901, 150)[1]
const rectDragTo = scene.makeMouseHelpers(901, 180)[1]
const circleEdge = scene.makeMouseHelpers(691, 331)[1]
const dragCircleTo = scene.makeMouseHelpers(720, 331)[1]
const [rectStart] = scene.makeMouseHelpers(794, 322)
const [rectEnd] = scene.makeMouseHelpers(757, 395)
await test2.step('enter sketch and setup', async () => {
await pointOnSegment({ shouldDbClick: true })
await app.page.waitForTimeout(600)
await toolbar.lineBtn.click()
await app.page.waitForTimeout(100)
})
await test2.step('extend existing profile', async () => {
await profileEnd()
await app.page.waitForTimeout(100)
await newProfileEnd()
await editor.expectEditor.toContain(`|> line([-11.4, 0.71], %)`)
await toolbar.lineBtn.click()
await app.page.waitForTimeout(100)
})
await test2.step('edit existing profile', async () => {
await profileEndMv()
await app.page.mouse.down()
await dragSegmentTo()
await app.page.mouse.up()
await editor.expectEditor.toContain(`line([4.16, -4.51], %)`)
})
await test2.step('edit existing rect', async () => {
await rectHandle()
await app.page.mouse.down()
await rectDragTo()
await app.page.mouse.up()
await editor.expectEditor.toContain(
`angledLine([-7, 10.2], %, $rectangleSegmentA001)`
)
})
await test2.step('edit existing circl', async () => {
await circleEdge()
await app.page.mouse.down()
await dragCircleTo()
await app.page.mouse.up()
await editor.expectEditor.toContain(
`profile003 = circle({ center = [6.92, -4.2], radius = 4.77 }, sketch001)`
)
})
await test2.step('add new profile', async () => {
await toolbar.rectangleBtn.click()
await app.page.waitForTimeout(100)
await rectStart()
await editor.expectEditor.toContain(
`profile004 = startProfileAt([15.62, -3.83], sketch001)`
)
await app.page.waitForTimeout(100)
await rectEnd()
await editor.expectEditor
.toContain(`|> angledLine([180, 1.97], %, $rectangleSegmentA002)
|> angledLine([
segAng(rectangleSegmentA002) + 90,
3.88
], %)
|> angledLine([
segAng(rectangleSegmentA002),
-segLen(rectangleSegmentA002)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)`)
})
}
)
test2(
'Can delete a profile in the editor while is sketch mode, and sketch mode does not break, can ctrl+z to undo after constraint with variable was added',
async ({ app, scene, toolbar, editor, cmdBar }) => {
await app.initialise(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([6.24, 4.54], sketch001)
|> line([-0.41, 6.99], %)
|> line([8.61, 0.74], %)
|> line([10.99, -5.22], %)
profile002 = startProfileAt([11.19, 5.02], sketch001)
|> angledLine([0, 10.78], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
4.14
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
`)
const [pointOnSegment] = scene.makeMouseHelpers(590, 141)
const [segment1Click] = scene.makeMouseHelpers(616, 131)
const sketchIsDrawnProperly = async () => {
await test2.step(
'check the sketch is still drawn properly',
async () => {
await app.page.waitForTimeout(200)
await scene.expectPixelColor(
[255, 255, 255],
{ x: 617, y: 163 },
15
)
await scene.expectPixelColor(
[255, 255, 255],
{ x: 629, y: 331 },
15
)
}
)
}
await test2.step('enter sketch and setup', async () => {
await pointOnSegment({ shouldDbClick: true })
await app.page.waitForTimeout(600)
await toolbar.lineBtn.click()
await app.page.waitForTimeout(100)
})
await test2.step('select and delete code for a profile', async () => {})
await app.page.getByText('close(%)').click()
await app.page.keyboard.down('Shift')
for (let i = 0; i < 11; i++) {
await app.page.keyboard.press('ArrowUp')
}
await app.page.keyboard.press('Home')
await app.page.keyboard.up('Shift')
await app.page.keyboard.press('Backspace')
await sketchIsDrawnProperly()
await test2.step('add random new var between profiles', async () => {
await app.page.keyboard.type('myVar = 5')
await app.page.keyboard.press('Enter')
await app.page.waitForTimeout(600)
})
await sketchIsDrawnProperly()
await test2.step(
'Adding a constraint with a variable, and than ctrl-z-ing which will remove the variable again does not break sketch mode',
async () => {
await expect(async () => {
await segment1Click()
await editor.expectState({
diagnostics: [],
activeLines: ['|>line([-0.41,6.99],%)'],
highlightedCode: 'line([-0.41,6.99],%)',
})
}).toPass({ timeout: 5_000, intervals: [500] })
await toolbar.lengthConstraintBtn.click()
await cmdBar.progressCmdBar()
await editor.expectEditor.toContain('length001 = 7')
// wait for execute defer
await app.page.waitForTimeout(600)
await sketchIsDrawnProperly()
await app.page.keyboard.down('Meta')
await app.page.keyboard.press('KeyZ')
await app.page.keyboard.up('Meta')
await editor.expectEditor.not.toContain('length001 = 7')
await sketchIsDrawnProperly()
}
)
}
)
test2(
'can enter sketch when there is an extrude',
async ({ app, scene, toolbar }) => {
await app.initialise(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([-63.43, 193.08], sketch001)
|> line([168.52, 149.87], %)
|> line([190.29, -39.18], %)
|> tangentialArcTo([319.63, 129.65], %)
|> line([-217.65, -21.76], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile003 = startProfileAt([16.79, 38.24], sketch001)
|> angledLine([0, 182.82], %, $rectangleSegmentA001)
|> angledLine([
segAng(rectangleSegmentA001) - 90,
105.71
], %)
|> angledLine([
segAng(rectangleSegmentA001),
-segLen(rectangleSegmentA001)
], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
profile004 = circle({
center = [280.45, 47.57],
radius = 55.26
}, sketch001)
extrude002 = extrude(50, profile001)
extrude001 = extrude(5, profile003)
`)
const [pointOnSegment] = scene.makeMouseHelpers(574, 207)
await pointOnSegment()
await toolbar.editSketch()
// wait for engine animation
await app.page.waitForTimeout(600)
await test2.step('check the sketch is still drawn properly', async () => {
await scene.expectPixelColor([255, 255, 255], { x: 591, y: 167 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 638, y: 222 }, 15)
await scene.expectPixelColor([255, 255, 255], { x: 756, y: 214 }, 15)
})
}
)
test2(
'exit new sketch without drawing anything should not be a problem',
async ({ app, scene, toolbar, editor, cmdBar }) => {
await app.initialise(`myVar = 5`)
const [selectXZPlane] = scene.makeMouseHelpers(650, 150)
await toolbar.startSketchPlaneSelection()
await selectXZPlane()
// timeout wait for engine animation is unavoidable
await app.page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
await toolbar.exitSketchBtn.click()
await editor.expectEditor.not.toContain(`sketch001 = startSketchOn('XZ')`)
await test2.step(
"still renders code, hasn't got into a weird state",
async () => {
await editor.replaceCode(
'myVar = 5',
`myVar = 5
sketch001 = startSketchOn('XZ')
profile001 = circle({
center = [12.41, 3.87],
radius = myVar
}, sketch001)`
)
await scene.expectPixelColor([255, 255, 255], { x: 633, y: 211 }, 15)
}
)
}
)
test2(
'A sketch with only "startProfileAt" and no segments should still be able to be continued ',
async ({ app, scene, toolbar, editor }) => {
await app.initialise(`sketch001 = startSketchOn('XZ')
profile001 = startProfileAt([85.19, 338.59], sketch001)
|> line([213.3, -94.52], %)
|> line([-230.09, -55.34], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
sketch002 = startSketchOn('XY')
profile002 = startProfileAt([85.81, 52.55], sketch002)
`)
const [startProfileAt] = scene.makeMouseHelpers(606, 184)
const [nextPoint] = scene.makeMouseHelpers(763, 130)
await app.page
.getByText('startProfileAt([85.81, 52.55], sketch002)')
.click()
await toolbar.editSketch()
// timeout wait for engine animation is unavoidable
await app.page.waitForTimeout(600)
// equip line tool
await toolbar.lineBtn.click()
await app.page.waitForTimeout(100)
await startProfileAt()
await app.page.waitForTimeout(100)
await nextPoint()
await editor.expectEditor.toContain(`|> line([126.05, 44.12], %)`)
}
)
test2(
'old style sketch all in one pipe (with extrude) will break up to allow users to add a new profile to the same sketch',
async ({ app, scene, toolbar, editor }) => {
await app.initialise(`thePart = startSketchOn('XZ')
|> startProfileAt([7.53, 10.51], %)
|> line([12.54, 1.83], %)
|> line([6.65, -6.91], %)
|> line([-6.31, -8.69], %)
|> lineTo([profileStartX(%), profileStartY(%)], %)
|> close(%)
extrude001 = extrude(75, thePart)
`)
const [objClick] = scene.makeMouseHelpers(565, 343)
const [profilePoint1] = scene.makeMouseHelpers(609, 289)
const [profilePoint2] = scene.makeMouseHelpers(714, 389)
await test2.step('enter sketch and setup', async () => {
await objClick()
await toolbar.editSketch()
// timeout wait for engine animation is unavoidable
await app.page.waitForTimeout(600)
})
await test2.step(
'expect code to match initial conditions still',
async () => {
await editor.expectEditor.toContain(`thePart = startSketchOn('XZ')
|> startProfileAt([7.53, 10.51], %)`)
}
)
await test2.step(
'equiping the line tool should break up the pipe expression',
async () => {
await toolbar.lineBtn.click()
await editor.expectEditor.toContain(
`sketch001 = startSketchOn('XZ')thePart = startProfileAt([7.53, 10.51], sketch001)`
)
}
)
await test2.step(
'can continue on to add a new profile to this sketch',
async () => {
await profilePoint1()
await editor.expectEditor.toContain(
`profile001 = startProfileAt([19.77, -7.08], sketch001)`
)
await profilePoint2()
await editor.expectEditor.toContain(`|> line([19.05, -18.14], %)`)
}
)
}
)
})

View File

@ -446,7 +446,8 @@ test(
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` code += `
|> startProfileAt([7.19, -9.7], %)`
await expect(page.locator('.cm-content')).toHaveText(code) await expect(page.locator('.cm-content')).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -468,10 +469,6 @@ test(
.getByRole('button', { name: 'arc Tangential Arc', exact: true }) .getByRole('button', { name: 'arc Tangential Arc', exact: true })
.click() .click()
// click to continue profile
await page.mouse.move(813, 392, { steps: 10 })
await page.waitForTimeout(100)
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 }) await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
@ -594,7 +591,8 @@ test(
mask: [page.getByTestId('model-state-indicator')], mask: [page.getByTestId('model-state-indicator')],
}) })
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn('XZ')profile001 = circle({ center = [14.44, -2.44], radius = 1 }, sketch001)` `sketch001 = startSketchOn('XZ')
|> circle({ center = [14.44, -2.44], radius = 1 }, %)`
) )
} }
) )
@ -638,7 +636,8 @@ test.describe(
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)` code += `
|> startProfileAt([7.19, -9.7], %)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -656,10 +655,6 @@ test.describe(
.click() .click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// click to continue profile
await page.mouse.click(813, 392)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
code += ` code += `
@ -746,7 +741,8 @@ test.describe(
const startXPx = 600 const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10) await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
code += `profile001 = startProfileAt([182.59, -246.32], sketch001)` code += `
|> startProfileAt([182.59, -246.32], %)`
await expect(u.codeLocator).toHaveText(code) await expect(u.codeLocator).toHaveText(code)
await page.waitForTimeout(100) await page.waitForTimeout(100)
@ -764,10 +760,6 @@ test.describe(
.click() .click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// click to continue profile
await page.mouse.click(813, 392)
await page.waitForTimeout(100)
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20) await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
code += ` code += `
@ -958,75 +950,7 @@ test(
test.describe('Grid visibility', { tag: '@snapshot' }, () => { test.describe('Grid visibility', { tag: '@snapshot' }, () => {
// FIXME: Skip on macos its being weird. // FIXME: Skip on macos its being weird.
// test.skip(process.platform === 'darwin', 'Skip on macos') test.skip(process.platform === 'darwin', 'Skip on macos')
test('Grid turned off to on via command bar', async ({ page }) => {
const u = await getUtils(page)
const stream = page.getByTestId('stream')
const mask = [
page.locator('#app-header'),
page.locator('#sidebar-top-ribbon'),
page.locator('#sidebar-bottom-ribbon'),
]
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
// wait for execution done
await expect(
page.locator('[data-message-type="execution-done"]')
).toHaveCount(1)
await u.closeDebugPanel()
await u.closeKclCodePanel()
// TODO: Find a way to truly know that the objects have finished
// rendering, because an execution-done message is not sufficient.
await page.waitForTimeout(1000)
// Open the command bar.
await page
.getByRole('button', { name: 'Commands', exact: false })
.or(page.getByRole('button', { name: '⌘K' }))
.click()
const commandName = 'show scale grid'
const commandOption = page.getByRole('option', {
name: commandName,
exact: false,
})
const cmdSearchBar = page.getByPlaceholder('Search commands')
// This selector changes after we set the setting
await cmdSearchBar.fill(commandName)
await expect(commandOption).toBeVisible()
await commandOption.click()
const toggleInput = page.getByPlaceholder('Off')
await expect(toggleInput).toBeVisible()
await expect(toggleInput).toBeFocused()
// Select On
await page.keyboard.press('ArrowDown')
await expect(page.getByRole('option', { name: 'Off' })).toHaveAttribute(
'data-headlessui-state',
'active selected'
)
await page.keyboard.press('ArrowUp')
await expect(page.getByRole('option', { name: 'On' })).toHaveAttribute(
'data-headlessui-state',
'active'
)
await page.keyboard.press('Enter')
// Check the toast appeared
await expect(
page.getByText(`Set show scale grid to "true" as a user default`)
).toBeVisible()
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask,
})
})
test('Grid turned off', async ({ page }) => { test('Grid turned off', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
@ -1172,109 +1096,3 @@ test.fixme('theme persists', async ({ page, context }) => {
maxDiffPixels: 100, maxDiffPixels: 100,
}) })
}) })
test.describe('code color goober', { tag: '@snapshot' }, () => {
test('code color goober', async ({ page, context }) => {
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`// Create a pipe using a sweep.
// Create a path for the sweep.
sweepPath = startSketchOn('XZ')
|> startProfileAt([0.05, 0.05], %)
|> line([0, 7], %)
|> tangentialArc({ offset = 90, radius = 5 }, %)
|> line([-3, 0], %)
|> tangentialArc({ offset = -90, radius = 5 }, %)
|> line([0, 7], %)
sweepSketch = startSketchOn('XY')
|> startProfileAt([2, 0], %)
|> arc({
angleEnd = 360,
angleStart = 0,
radius = 2
}, %)
|> sweep({
path = sweepPath,
}, %)
|> appearance({
color = "#bb00ff",
metalness = 90,
roughness = 90
}, %)
`
)
})
await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await expect(page, 'expect small color widget').toHaveScreenshot({
maxDiffPixels: 100,
})
})
test('code color goober opening window', async ({ page, context }) => {
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`// Create a pipe using a sweep.
// Create a path for the sweep.
sweepPath = startSketchOn('XZ')
|> startProfileAt([0.05, 0.05], %)
|> line([0, 7], %)
|> tangentialArc({ offset = 90, radius = 5 }, %)
|> line([-3, 0], %)
|> tangentialArc({ offset = -90, radius = 5 }, %)
|> line([0, 7], %)
sweepSketch = startSketchOn('XY')
|> startProfileAt([2, 0], %)
|> arc({
angleEnd = 360,
angleStart = 0,
radius = 2
}, %)
|> sweep({
path = sweepPath,
}, %)
|> appearance({
color = "#bb00ff",
metalness = 90,
roughness = 90
}, %)
`
)
})
await page.setViewportSize({ width: 1200, height: 1000 })
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await expect(page.locator('.cm-css-color-picker-wrapper')).toBeVisible()
// Click the color widget
await page.locator('.cm-css-color-picker-wrapper input').click()
await expect(
page,
'expect small color widget to have window open'
).toHaveScreenshot({
maxDiffPixels: 100,
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

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