Compare commits
51 Commits
nightly-v2
...
jtran/json
Author | SHA1 | Date | |
---|---|---|---|
4a5ef17c92 | |||
83220a7255 | |||
a0f3f0f50e | |||
dad0d5488f | |||
77773fb052 | |||
1240ca6f21 | |||
ce3a6ad286 | |||
a20f420a55 | |||
03c4121ab7 | |||
8c2c877817 | |||
c77bb92da9 | |||
bd4bad0020 | |||
0b9889e313 | |||
f9fbaa2298 | |||
6f2d127c4f | |||
056a4d4a22 | |||
bab79331cb | |||
16bbc970ae | |||
0f1cff316c | |||
938a2bae13 | |||
3324835b72 | |||
1628ea86b2 | |||
be119248a6 | |||
ac75181f7f | |||
d9fe78171f | |||
5586646a60 | |||
e955e783f4 | |||
9162c4d723 | |||
f345a07bcd | |||
f5e3b4bbe7 | |||
941911ed30 | |||
599bd2d958 | |||
a847c7f608 | |||
d9be308c36 | |||
3cca4a30af | |||
c51db5bd4a | |||
7fce38b212 | |||
d0e9b111af | |||
add1b21503 | |||
39af110ac1 | |||
bf7ec424a7 | |||
6c7e42b541 | |||
542a9a3f13 | |||
264779a9d0 | |||
22dd4a67dd | |||
5c188664bd | |||
f6e23fe619 | |||
160f55ede5 | |||
7d7b153085 | |||
8f487a441b | |||
c8cee6a0dc |
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser,fromM,FromM
|
||||
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.package-lock.json,**/package-lock.json,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,./public/kcl-samples,./rust/kcl-lib/tests/kcl_samples,tsconfig.tsbuildinfo,./src/lib/machine-api.d.ts
|
||||
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,./e2e/playwright/lib/console-error-whitelist.ts,.package-lock.json,**/package-lock.json,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,./public/kcl-samples,./rust/kcl-lib/tests/kcl_samples,tsconfig.tsbuildinfo,./src/lib/machine-api.d.ts
|
||||
|
27
.github/workflows/build-apps.yml
vendored
@ -174,14 +174,7 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm' # Set this to npm, npm or pnpm.
|
||||
|
||||
- name: npm install
|
||||
# Windows is picky sometimes and fails on fetch. Step takes about ~30s
|
||||
uses: nick-fields/retry@v3.0.2
|
||||
with:
|
||||
shell: bash
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: npm install
|
||||
- run: npm install
|
||||
|
||||
- name: Prepare certificate and variables (Windows only)
|
||||
if: ${{ (env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true') && matrix.os == 'windows-2022' }}
|
||||
@ -233,14 +226,7 @@ jobs:
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||
DEBUG: "electron-notarize*"
|
||||
# TODO: Fix electron-notarize flakes. The logs above should help gather more data on failures
|
||||
uses: nick-fields/retry@v3.0.2
|
||||
with:
|
||||
shell: bash
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run tronb:package:prod
|
||||
run: npm run tronb:package:prod
|
||||
|
||||
- name: List artifacts in out/
|
||||
run: ls -R out
|
||||
@ -295,14 +281,7 @@ jobs:
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||
DEBUG: "electron-notarize*"
|
||||
# TODO: Fix electron-notarize flakes. The logs above should help gather more data on failures
|
||||
uses: nick-fields/retry@v3.0.2
|
||||
with:
|
||||
shell: bash
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: npm run tronb:package:prod
|
||||
run: npm run tronb:package:prod
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ env.IS_RELEASE == 'true' }}
|
||||
|
3
.github/workflows/cargo-bench.yml
vendored
@ -50,13 +50,12 @@ jobs:
|
||||
- name: Build the benchmark target(s)
|
||||
run: |
|
||||
cd rust
|
||||
cargo codspeed build --measurement-mode walltime
|
||||
cargo codspeed build
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@v3
|
||||
with:
|
||||
working-directory: rust
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
mode: walltime
|
||||
env:
|
||||
KITTYCAD_API_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN }}
|
||||
|
8
.github/workflows/e2e-tests.yml
vendored
@ -226,8 +226,8 @@ jobs:
|
||||
with:
|
||||
shell: bash
|
||||
command: npm run test:snapshots
|
||||
timeout_minutes: 30
|
||||
max_attempts: 3
|
||||
timeout_minutes: 5
|
||||
max_attempts: 5
|
||||
env:
|
||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
|
||||
@ -285,7 +285,7 @@ jobs:
|
||||
# TODO: enable namespace-profile-windows-latest once available
|
||||
os:
|
||||
- "runs-on=${{ github.run_id }}/family=i7ie.2xlarge/image=ubuntu22-full-x64"
|
||||
- namespace-profile-macos-8-cores
|
||||
- namespace-profile-macos-6-cores
|
||||
- windows-latest-8-cores
|
||||
shardIndex: [1, 2, 3, 4]
|
||||
shardTotal: [4]
|
||||
@ -295,7 +295,7 @@ jobs:
|
||||
isScheduled:
|
||||
- ${{ github.event_name == 'schedule' }}
|
||||
exclude:
|
||||
- os: namespace-profile-macos-8-cores
|
||||
- os: namespace-profile-macos-6-cores
|
||||
isScheduled: true
|
||||
- os: windows-latest-8-cores
|
||||
isScheduled: true
|
||||
|
2
.github/workflows/static-analysis.yml
vendored
@ -25,7 +25,7 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run fmt-check
|
||||
- run: npm run fmt:check
|
||||
|
||||
npm-build-wasm:
|
||||
# Build the wasm blob once on the fastest runner.
|
||||
|
1
.gitignore
vendored
@ -84,3 +84,4 @@ dist
|
||||
venv
|
||||
|
||||
.vscode-test
|
||||
.biome/
|
||||
|
432
CONTRIBUTING.md
Normal file
@ -0,0 +1,432 @@
|
||||
# Contributor Guide
|
||||
|
||||
## Running a development build
|
||||
|
||||
Install a node version manager such as [fnm](https://github.com/Schniz/fnm?tab=readme-ov-#installation).
|
||||
|
||||
On Windows, it's also recommended to [upgrade your PowerShell version](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.5#winget), we're using 7.
|
||||
|
||||
Then in the repo run the following to install and use the node version specified in `.nvmrc`. You might need to specify your processor architecture with `--arch arm64` or `--arch x64` if it's not autodetected.
|
||||
|
||||
```
|
||||
fnm install --corepack-enabled
|
||||
fnm use
|
||||
```
|
||||
|
||||
Install the NPM dependencies with:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
This project uses a lot of Rust compiled to [WASM](https://webassembly.org/) within it. We have package scripts to run rustup, see `package.json` for reference:
|
||||
|
||||
```
|
||||
# macOS/Linux
|
||||
npm run install:rust
|
||||
npm run install:wasm-pack:sh
|
||||
|
||||
# Windows
|
||||
npm run install:rust:windows
|
||||
npm run install:wasm-pack:cargo
|
||||
```
|
||||
|
||||
Then to build the WASM layer, run:
|
||||
|
||||
```
|
||||
# macOS/Linux
|
||||
npm run build:wasm
|
||||
|
||||
# Windows
|
||||
npm run build:wasm:windows
|
||||
```
|
||||
|
||||
Or if you have the `gh` cli installed and want to download the latest main wasm bundle. Note that on Windows, you need to associate .ps1 files with PowerShell, which can be done via the right click menu, selecting `C:\Program Files\PowerShell\7\pwsh.exe`, and you can install tools like `gh` via `npm run install:tools:windows`.
|
||||
|
||||
```
|
||||
# macOS/Linux
|
||||
npm run fetch:wasm
|
||||
|
||||
# Windows
|
||||
npm run fetch:wasm:windows
|
||||
```
|
||||
|
||||
That will build the WASM binary and put in the `public` dir (though gitignored).
|
||||
|
||||
Finally, to run the web app only, run:
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
If you're not a Zoo employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development.local` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens of course, then navigate to `localhost:3000` again. Note that navigating to `localhost:3000/signin` removes your token so you will need to set the token again.
|
||||
|
||||
### Development environment variables
|
||||
|
||||
The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `dev.zoo.dev`). There is an optional environment variable called `VITE_KC_DEV_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service.
|
||||
|
||||
### Developing in Chrome
|
||||
|
||||
Chrome is in the process of rolling out a new default which
|
||||
[blocks Third-Party Cookies](https://developer.chrome.com/en/docs/privacy-sandbox/third-party-cookie-phase-out/).
|
||||
If you're having trouble logging into the `modeling-app`, you may need to
|
||||
enable third-party cookies. You can enable third-party cookies by clicking on
|
||||
the eye with a slash through it in the URL bar, and clicking on "Enable
|
||||
Third-Party Cookies".
|
||||
|
||||
## Desktop
|
||||
|
||||
To spin up the desktop app, `npm install` and `npm run build:wasm` need to have been done before hand then:
|
||||
|
||||
```
|
||||
npm run tron:start
|
||||
```
|
||||
|
||||
This will start the application and hot-reload on changes.
|
||||
|
||||
Devtools can be opened with the usual Command-Option-I (macOS) or Ctrl-Shift-I (Linux and Windows).
|
||||
|
||||
To package the app for your platform with electron-builder, run `npm run tronb:package:dev` (or `npm run tronb:package:prod` to point to the .env.production variables).
|
||||
|
||||
## Checking out commits / Bisecting
|
||||
|
||||
Which commands from setup are one off vs. need to be run every time?
|
||||
|
||||
The following will need to be run when checking out a new commit and guarantees the build is not stale:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build:wasm
|
||||
npm start # or npm run build:local && npm run serve for slower but more production-like build
|
||||
```
|
||||
|
||||
## Before submitting a PR
|
||||
|
||||
Before you submit a contribution PR to this repo, please ensure that:
|
||||
|
||||
- There is a corresponding issue for the changes you want to make, so that discussion of approach can be had before work begins.
|
||||
- You have separated out refactoring commits from feature commits as much as possible
|
||||
- You have run all of the following commands locally:
|
||||
- `npm run fmt`
|
||||
- `npm run tsc`
|
||||
- `npm run test`
|
||||
- Here they are all together: `npm run fmt && npm run tsc && npm run test`
|
||||
|
||||
## Release a new version
|
||||
|
||||
#### 1. Create a 'Cut release $VERSION' issue
|
||||
|
||||
It will be used to document changelog discussions and release testing.
|
||||
|
||||
https://github.com/KittyCAD/modeling-app/issues/new
|
||||
|
||||
#### 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`.
|
||||
|
||||
```
|
||||
VERSION=$(./scripts/semantic-release.sh)
|
||||
git tag $VERSION
|
||||
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.
|
||||
|
||||
The workflow should be listed right away [in this list](https://github.com/KittyCAD/modeling-app/actions/workflows/build-apps.yml?query=event%3Apush)).
|
||||
|
||||
#### 3. Manually test artifacts
|
||||
|
||||
##### Release builds
|
||||
|
||||
The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in 2.).
|
||||
|
||||
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
|
||||
|
||||
The other `build-apps` output in the release `build-apps` workflow (triggered by 2.) is `updater-test-{arch}-{platform}`. It's a semi-automated process: for macOS, Windows, and Linux, download the corresponding updater-test artifact file, install the app, run it, expect an updater prompt to a dummy v0.255.255, install it and check that the app comes back at that version.
|
||||
|
||||
The only difference with these builds is that they point to a different update location on the release bucket, with this dummy v0.255.255 always available. This helps ensuring that the version we release will be able to update to the next one available.
|
||||
|
||||
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing (or the updater-test location in the storage bucket).
|
||||
|
||||
```
|
||||
# Windows (PowerShell)
|
||||
& 'C:\Program Files\Zoo Design Studio\Zoo Design Studio.exe'
|
||||
|
||||
# macOS
|
||||
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
|
||||
|
||||
# Linux
|
||||
./Zoo Design Studio-{version}-{arch}-linux.AppImage
|
||||
```
|
||||
|
||||
#### 4. 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.
|
||||
|
||||
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 `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
|
||||
|
||||
If everything is well and the release is out to the public, the issue tracking the release shall be closed.
|
||||
|
||||
## Fuzzing the parser
|
||||
|
||||
Make sure you install cargo fuzz:
|
||||
|
||||
```bash
|
||||
$ cargo install cargo-fuzz
|
||||
```
|
||||
|
||||
```bash
|
||||
$ cd rust/kcl-lib
|
||||
|
||||
# list the fuzz targets
|
||||
$ cargo fuzz list
|
||||
|
||||
# run the parser fuzzer
|
||||
$ cargo +nightly fuzz run parser
|
||||
```
|
||||
|
||||
For more information on fuzzing you can check out
|
||||
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
|
||||
|
||||
## Tests
|
||||
|
||||
### Playwright tests
|
||||
|
||||
You will need a `./e2e/playwright/playwright-secrets.env` file:
|
||||
|
||||
```bash
|
||||
$ touch ./e2e/playwright/playwright-secrets.env
|
||||
$ cat ./e2e/playwright/playwright-secrets.env
|
||||
token=<dev.zoo.dev/account/api-tokens>
|
||||
snapshottoken=<zoo.dev/account/api-tokens>
|
||||
```
|
||||
or use `export` to set the environment variables `token` and `snapshottoken`.
|
||||
|
||||
#### Snapshot tests (Google Chrome on Ubuntu only)
|
||||
|
||||
Only Ubunu and Google Chrome is supported for the set of tests evaluating screenshot snapshots.
|
||||
If you don't run Ubuntu locally or in a VM, you may use a GitHub Codespace.
|
||||
```
|
||||
npm run playwright -- install chrome
|
||||
npm run test:snapshots
|
||||
```
|
||||
You may use `-- --update-snapshots` as needed.
|
||||
|
||||
#### Electron flow tests (Chromium on Ubuntu, macOS, Windows)
|
||||
|
||||
```
|
||||
npm run playwright -- install chromium
|
||||
npm run test:playwright:electron:local
|
||||
```
|
||||
You may use `-- -g "my test"` to match specific test titles, or `-- path/to/file.spec.ts` for a test file.
|
||||
|
||||
#### Debugger
|
||||
|
||||
However, if you want a debugger I recommend using VSCode and the `playwright` extension, as the above command is a cruder debugger that steps into every function call which is annoying.
|
||||
With the extension you can set a breakpoint after `waitForDefaultPlanesVisibilityChange` in order to skip app loading, then the vscode debugger's "step over" is much better for being able to stay at the right level of abstraction as you debug the code.
|
||||
|
||||
If you want to limit to a single browser use `--project="webkit"` or `firefox`, `Google Chrome`
|
||||
Or comment out browsers in `playwright.config.ts`.
|
||||
|
||||
note chromium has encoder compat issues which is why were testing against the branded 'Google Chrome'
|
||||
|
||||
You may consider using the VSCode extension, it's useful for running individual threads, but some some reason the "record a test" is locked to chromium with we can't use. A work around is to us the CI `npm run playwright codegen -b wk --load-storage ./store localhost:3000`
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
|
||||
Where `./store` should look like this
|
||||
|
||||
</summary>
|
||||
|
||||
```JSON
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "http://localhost:3000",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "store",
|
||||
"value": "{\"state\":{\"openPanes\":[\"code\"]},\"version\":0}"
|
||||
},
|
||||
{
|
||||
"name": "persistCode",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"name": "TOKEN_PERSIST_KEY",
|
||||
"value": "your-token"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
However because much of our tests involve clicking in the stream at specific locations, it's code-gen looks `await page.locator('video').click();` when really we need to use a pixel coord, so I think it's of limited use.
|
||||
|
||||
### Unit and component tests
|
||||
|
||||
If you already haven't, run the following:
|
||||
|
||||
```
|
||||
npm
|
||||
npm run build:wasm
|
||||
npm start
|
||||
```
|
||||
|
||||
and finally:
|
||||
|
||||
```
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
For individual testing:
|
||||
|
||||
```
|
||||
npm run test abstractSyntaxTree -t "unexpected closed curly brace" --silent=false
|
||||
```
|
||||
|
||||
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro) tests, in interactive mode by default.
|
||||
|
||||
### Rust tests
|
||||
|
||||
**Dependencies**
|
||||
|
||||
- `KITTYCAD_API_TOKEN`
|
||||
- `cargo-nextest`
|
||||
- `just`
|
||||
|
||||
#### Setting KITTYCAD_API_TOKEN
|
||||
|
||||
Use the production zoo.dev token, set this environment variable before running the tests
|
||||
|
||||
#### Installing cargonextest
|
||||
|
||||
```
|
||||
cd rust
|
||||
cargo search cargo-nextest
|
||||
cargo install cargo-nextest
|
||||
```
|
||||
|
||||
#### just
|
||||
|
||||
install [`just`](https://github.com/casey/just?tab=readme-ov-file#pre-built-binaries)
|
||||
|
||||
#### Running the tests
|
||||
|
||||
```bash
|
||||
# With just
|
||||
# Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set
|
||||
# Make sure you installed cargo-nextest
|
||||
# Make sure you installed just
|
||||
cd rust
|
||||
just test
|
||||
```
|
||||
|
||||
```bash
|
||||
# Without just
|
||||
# Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set
|
||||
# Make sure you installed cargo-nextest
|
||||
cd rust
|
||||
export RUST_BRACKTRACE="full" && cargo nextest run --workspace --test-threads=1
|
||||
```
|
||||
|
||||
Where `XXX` is an API token from the production engine (NOT the dev environment).
|
||||
|
||||
We recommend using [nextest](https://nexte.st/) to run the Rust tests (its faster and is used in CI). Once installed, run the tests using
|
||||
|
||||
```
|
||||
cd rust
|
||||
KITTYCAD_API_TOKEN=XXX cargo run nextest
|
||||
```
|
||||
|
||||
### Mapping CI CD jobs to local commands
|
||||
|
||||
When you see the CI CD fail on jobs you may wonder three things
|
||||
|
||||
- Do I have a bug in my code?
|
||||
- Is the test flaky?
|
||||
- Is there a bug in `main`?
|
||||
|
||||
To answer these questions the following commands will give you confidence to locate the issue.
|
||||
|
||||
#### Static Analysis
|
||||
|
||||
Part of the CI CD pipeline performs static analysis on the code. Use the following commands to mimic the CI CD jobs.
|
||||
|
||||
The following set of commands should get us closer to one and done commands to instantly retest issues.
|
||||
|
||||
```
|
||||
npm run test-setup
|
||||
```
|
||||
|
||||
> Gotcha, are packages up to date and is the wasm built?
|
||||
|
||||
```
|
||||
npm run tsc
|
||||
npm run fmt:check
|
||||
npm run lint
|
||||
npm run test:unit:local
|
||||
```
|
||||
|
||||
> Gotcha: Our unit tests have integration tests in them. You need to run a localhost server to run the unit tests.
|
||||
|
||||
#### E2E Tests
|
||||
|
||||
**Playwright Electron**
|
||||
|
||||
These E2E tests run in electron. There are tests that are skipped if they are ran in a windows, linux, or macos environment. We can use playwright tags to implement test skipping.
|
||||
|
||||
```
|
||||
npm run test:playwright:electron:local
|
||||
npm run test:playwright:electron:windows:local
|
||||
npm run test:playwright:electron:macos:local
|
||||
npm run test:playwright:electron:ubuntu:local
|
||||
```
|
||||
|
||||
> Why does it say local? The CI CD commands that run in the pipeline cannot be ran locally. A single command will not properly setup the testing environment locally.
|
||||
|
||||
#### Some notes on CI
|
||||
|
||||
The tests are broken into snapshot tests and non-snapshot tests, and they run in that order, they automatically commit new snap shots, so if you see an image commit check it was an intended change. If we have non-determinism in the snapshots such that they are always committing new images, hopefully this annoyance makes us fix them asap, if you notice this happening let Kurt know. But for the odd occasion `git reset --hard HEAD~ && git push -f` is your friend.
|
||||
|
||||
How to interpret failing playwright tests?
|
||||
If your tests fail, click through to the action and see that the tests failed on a line that includes `await page.getByTestId('loading').waitFor({ state: 'detached' })`, this means the test fail because the stream never started. It's you choice if you want to re-run the test, or ignore the failure.
|
||||
|
||||
We run on ubuntu and macos, because safari doesn't work on linux because of the dreaded "no RTCPeerConnection variable" error. But linux runs first and then macos for the same reason that we limit the number of parallel tests to 1 because we limit stream connections per user, so tests would start failing we if let them run together.
|
||||
|
||||
If something fails on CI you can download the artifact, unzip it and then open `playwright-report/data/<UUID>.zip` with https://trace.playwright.dev/ to see what happened.
|
||||
|
||||
#### Getting started writing a playwright test in our app
|
||||
|
||||
Besides following the instructions above and using the playwright docs, our app is weird because of the whole stream thing, which means our testing is weird. Because we've just figured out this stuff and therefore docs might go stale quick here's a 15min vid/tutorial
|
||||
|
||||
https://github.com/KittyCAD/modeling-app/assets/29681384/6f5e8e85-1003-4fd9-be7f-f36ce833942d
|
||||
|
||||
<details>
|
||||
|
||||
<summary>
|
||||
PS: for the debug panel, the following JSON is useful for snapping the camera
|
||||
</summary>
|
||||
|
||||
```JSON
|
||||
{"type":"modeling_cmd_req","cmd_id":"054e5472-e5e9-4071-92d7-1ce3bac61956","cmd":{"type":"default_camera_look_at","center":{"x":15,"y":0,"z":0},"up":{"x":0,"y":0,"z":1},"vantage":{"x":30,"y":30,"z":30}}}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### 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`.
|
17
Makefile
@ -15,8 +15,8 @@ ifdef WINDOWS
|
||||
CARGO ?= $(USERPROFILE)/.cargo/bin/cargo.exe
|
||||
WASM_PACK ?= $(USERPROFILE)/.cargo/bin/wasm-pack.exe
|
||||
else
|
||||
CARGO ?= ~/.cargo/bin/cargo
|
||||
WASM_PACK ?= ~/.cargo/bin/wasm-pack
|
||||
CARGO ?= $(shell which cargo || echo ~/.cargo/bin/cargo)
|
||||
WASM_PACK ?= $(shell which wasm-pack || echo ~/.cargo/bin/wasm-pack)
|
||||
endif
|
||||
|
||||
.PHONY: install
|
||||
@ -24,11 +24,6 @@ install: node_modules/.package-lock.json $(CARGO) $(WASM_PACK) ## Install depend
|
||||
|
||||
node_modules/.package-lock.json: package.json package-lock.json
|
||||
npm install
|
||||
ifdef POWERSHELL
|
||||
@ type nul > $@
|
||||
else
|
||||
@ touch $@
|
||||
endif
|
||||
|
||||
$(CARGO):
|
||||
ifdef WINDOWS
|
||||
@ -129,17 +124,17 @@ test-e2e: test-e2e-$(TARGET)
|
||||
test-e2e-web: install build-web ## Run the web e2e tests
|
||||
@ curl -fs localhost:3000 >/dev/null || ( echo "Error: localhost:3000 not available, 'make run-web' first" && exit 1 )
|
||||
ifdef E2E_GREP
|
||||
npm run chrome:test --headed --grep="$(E2E_GREP)" --max-failures=$(E2E_FAILURES)
|
||||
npm run chrome:test -- --headed --grep="$(E2E_GREP)" --max-failures=$(E2E_FAILURES)
|
||||
else
|
||||
npm run chrome:test --headed --workers='100%'
|
||||
npm run chrome:test -- --headed --workers='100%'
|
||||
endif
|
||||
|
||||
.PHONY: test-e2e-desktop
|
||||
test-e2e-desktop: install build-desktop ## Run the desktop e2e tests
|
||||
ifdef E2E_GREP
|
||||
npm run test:playwright:electron --grep="$(E2E_GREP)" --max-failures=$(E2E_FAILURES)
|
||||
npm run test:playwright:electron -- --grep="$(E2E_GREP)" --max-failures=$(E2E_FAILURES)
|
||||
else
|
||||
npm run test:playwright:electron --workers='100%'
|
||||
npm run test:playwright:electron -- --workers='100%'
|
||||
endif
|
||||
|
||||
###############################################################################
|
||||
|
473
README.md
@ -1,8 +1,8 @@
|
||||

|
||||
|
||||
## Zoo Design Studio
|
||||
# Zoo Design Studio
|
||||
|
||||
download at [zoo.dev/modeling-app/download](https://zoo.dev/modeling-app/download)
|
||||
[zoo.dev/modeling-app](https://zoo.dev/modeling-app)
|
||||
|
||||
A CAD application from the future, brought to you by the [Zoo team](https://zoo.dev).
|
||||
|
||||
@ -23,7 +23,7 @@ Design Studio is a _hybrid_ user interface for CAD modeling. You can point-and-c
|
||||
|
||||
The 3D view in Design Studio is just a video stream from our hosted geometry engine. The app sends new modeling commands to the engine via WebSockets, which returns back video frames of the view within the engine.
|
||||
|
||||
## Tools
|
||||
## Technology
|
||||
|
||||
- UI
|
||||
- [React](https://react.dev/)
|
||||
@ -38,471 +38,16 @@ The 3D view in Design Studio is just a video stream from our hosted geometry eng
|
||||
- Modeling
|
||||
- [KittyCAD TypeScript client](https://github.com/KittyCAD/kittycad.ts)
|
||||
|
||||
[Original demo video](https://drive.google.com/file/d/183_wjqGdzZ8EEZXSqZ3eDcJocYPCyOdC/view?pli=1)
|
||||
## Get Started
|
||||
|
||||
[Original demo slides](https://github.com/user-attachments/files/19010536/demo.pdf)
|
||||
We recommend downloading the latest application binary from our [releases](https://github.com/KittyCAD/modeling-app/releases) page. If you don't see your platform or architecture supported there, please file an issue.
|
||||
|
||||
## Get started
|
||||
If you'd like to try out upcoming changes sooner, you can also download those from our [nightly releases](https://zoo.dev/modeling-app/download/nightly) page.
|
||||
|
||||
We recommend downloading the latest application binary from [our Releases page](https://github.com/KittyCAD/modeling-app/releases). If you don't see your platform or architecture supported there, please file an issue.
|
||||
## Developing
|
||||
|
||||
## Running a development build
|
||||
|
||||
Install a node version manager such as [fnm](https://github.com/Schniz/fnm?tab=readme-ov-#installation).
|
||||
|
||||
On Windows, it's also recommended to [upgrade your PowerShell version](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.5#winget), we're using 7.
|
||||
|
||||
Then in the repo run the following to install and use the node version specified in `.nvmrc`. You might need to specify your processor architecture with `--arch arm64` or `--arch x64` if it's not autodetected.
|
||||
|
||||
```
|
||||
fnm install --corepack-enabled
|
||||
fnm use
|
||||
```
|
||||
|
||||
Install the NPM dependencies with:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
This project uses a lot of Rust compiled to [WASM](https://webassembly.org/) within it. We have package scripts to run rustup, see `package.json` for reference:
|
||||
|
||||
```
|
||||
# macOS/Linux
|
||||
npm run install:rust
|
||||
npm run install:wasm-pack:sh
|
||||
|
||||
# Windows
|
||||
npm run install:rust:windows
|
||||
npm run install:wasm-pack:cargo
|
||||
```
|
||||
|
||||
Then to build the WASM layer, run:
|
||||
|
||||
```
|
||||
# macOS/Linux
|
||||
npm run build:wasm
|
||||
|
||||
# Windows
|
||||
npm run build:wasm:windows
|
||||
```
|
||||
|
||||
Or if you have the `gh` cli installed and want to download the latest main wasm bundle. Note that on Windows, you need to associate .ps1 files with PowerShell, which can be done via the right click menu, selecting `C:\Program Files\PowerShell\7\pwsh.exe`, and you can install tools like `gh` via `npm run install:tools:windows`.
|
||||
|
||||
```
|
||||
# macOS/Linux
|
||||
npm run fetch:wasm
|
||||
|
||||
# Windows
|
||||
npm run fetch:wasm:windows
|
||||
```
|
||||
|
||||
That will build the WASM binary and put in the `public` dir (though gitignored).
|
||||
|
||||
Finally, to run the web app only, run:
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
If you're not a Zoo employee you won't be able to access the dev environment, you should copy everything from `.env.production` to `.env.development.local` to make it point to production instead, then when you navigate to `localhost:3000` the easiest way to sign in is to paste `localStorage.setItem('TOKEN_PERSIST_KEY', "your-token-from-https://zoo.dev/account/api-tokens")` replacing the with a real token from https://zoo.dev/account/api-tokens of course, then navigate to `localhost:3000` again. Note that navigating to `localhost:3000/signin` removes your token so you will need to set the token again.
|
||||
|
||||
### Development environment variables
|
||||
|
||||
The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `dev.zoo.dev`). There is an optional environment variable called `VITE_KC_DEV_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service.
|
||||
|
||||
### Developing in Chrome
|
||||
|
||||
Chrome is in the process of rolling out a new default which
|
||||
[blocks Third-Party Cookies](https://developer.chrome.com/en/docs/privacy-sandbox/third-party-cookie-phase-out/).
|
||||
If you're having trouble logging into the `modeling-app`, you may need to
|
||||
enable third-party cookies. You can enable third-party cookies by clicking on
|
||||
the eye with a slash through it in the URL bar, and clicking on "Enable
|
||||
Third-Party Cookies".
|
||||
|
||||
## Desktop
|
||||
|
||||
To spin up the desktop app, `npm install` and `npm run build:wasm` need to have been done before hand then:
|
||||
|
||||
```
|
||||
npm run tron:start
|
||||
```
|
||||
|
||||
This will start the application and hot-reload on changes.
|
||||
|
||||
Devtools can be opened with the usual Command-Option-I (macOS) or Ctrl-Shift-I (Linux and Windows).
|
||||
|
||||
To package the app for your platform with electron-builder, run `npm run tronb:package:dev` (or `npm run tronb:package:prod` to point to the .env.production variables).
|
||||
|
||||
## Checking out commits / Bisecting
|
||||
|
||||
Which commands from setup are one off vs. need to be run every time?
|
||||
|
||||
The following will need to be run when checking out a new commit and guarantees the build is not stale:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build:wasm
|
||||
npm start # or npm run build:local && npm run serve for slower but more production-like build
|
||||
```
|
||||
|
||||
## Before submitting a PR
|
||||
|
||||
Before you submit a contribution PR to this repo, please ensure that:
|
||||
|
||||
- There is a corresponding issue for the changes you want to make, so that discussion of approach can be had before work begins.
|
||||
- You have separated out refactoring commits from feature commits as much as possible
|
||||
- You have run all of the following commands locally:
|
||||
- `npm run fmt`
|
||||
- `npm run tsc`
|
||||
- `npm run test`
|
||||
- Here they are all together: `npm run fmt && npm run tsc && npm run test`
|
||||
|
||||
## Release a new version
|
||||
|
||||
#### 1. Create a 'Cut release $VERSION' issue
|
||||
|
||||
It will be used to document changelog discussions and release testing.
|
||||
|
||||
https://github.com/KittyCAD/modeling-app/issues/new
|
||||
|
||||
#### 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`.
|
||||
|
||||
```
|
||||
VERSION=$(./scripts/semantic-release.sh)
|
||||
git tag $VERSION
|
||||
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.
|
||||
|
||||
The workflow should be listed right away [in this list](https://github.com/KittyCAD/modeling-app/actions/workflows/build-apps.yml?query=event%3Apush)).
|
||||
|
||||
#### 3. Manually test artifacts
|
||||
|
||||
##### Release builds
|
||||
|
||||
The release builds can be found under the `out-{arch}-{platform}` zip files, at the very bottom of the `build-apps` summary page for the workflow (triggered by the tag in 2.).
|
||||
|
||||
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
|
||||
|
||||
The other `build-apps` output in the release `build-apps` workflow (triggered by 2.) is `updater-test-{arch}-{platform}`. It's a semi-automated process: for macOS, Windows, and Linux, download the corresponding updater-test artifact file, install the app, run it, expect an updater prompt to a dummy v0.255.255, install it and check that the app comes back at that version.
|
||||
|
||||
The only difference with these builds is that they point to a different update location on the release bucket, with this dummy v0.255.255 always available. This helps ensuring that the version we release will be able to update to the next one available.
|
||||
|
||||
If the prompt doesn't show up, start the app in command line to grab the electron-updater logs. This is likely an issue with the current build that needs addressing (or the updater-test location in the storage bucket).
|
||||
|
||||
```
|
||||
# Windows (PowerShell)
|
||||
& 'C:\Program Files\Zoo Design Studio\Zoo Design Studio.exe'
|
||||
|
||||
# macOS
|
||||
/Applications/Zoo\ Modeling\ App.app/Contents/MacOS/Zoo\ Modeling\ App
|
||||
|
||||
# Linux
|
||||
./Zoo Design Studio-{version}-{arch}-linux.AppImage
|
||||
```
|
||||
|
||||
#### 4. 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.
|
||||
|
||||
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 `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
|
||||
|
||||
If everything is well and the release is out to the public, the issue tracking the release shall be closed.
|
||||
|
||||
## Fuzzing the parser
|
||||
|
||||
Make sure you install cargo fuzz:
|
||||
|
||||
```bash
|
||||
$ cargo install cargo-fuzz
|
||||
```
|
||||
|
||||
```bash
|
||||
$ cd rust/kcl-lib
|
||||
|
||||
# list the fuzz targets
|
||||
$ cargo fuzz list
|
||||
|
||||
# run the parser fuzzer
|
||||
$ cargo +nightly fuzz run parser
|
||||
```
|
||||
|
||||
For more information on fuzzing you can check out
|
||||
[this guide](https://rust-fuzz.github.io/book/cargo-fuzz.html).
|
||||
|
||||
## Tests
|
||||
|
||||
### Playwright tests
|
||||
|
||||
You will need a `./e2e/playwright/playwright-secrets.env` file:
|
||||
|
||||
```bash
|
||||
$ touch ./e2e/playwright/playwright-secrets.env
|
||||
$ cat ./e2e/playwright/playwright-secrets.env
|
||||
token=<dev.zoo.dev/account/api-tokens>
|
||||
snapshottoken=<your-snapshot-token>
|
||||
```
|
||||
|
||||
For a portable way to run Playwright you'll need Docker.
|
||||
|
||||
#### Generic example
|
||||
|
||||
After that, open a terminal and run:
|
||||
|
||||
```bash
|
||||
docker run --network host --rm --init -it playwright/chrome:playwright-x.xx.x
|
||||
```
|
||||
|
||||
and in another terminal, run:
|
||||
|
||||
```bash
|
||||
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ npm run playwright test --project="Google Chrome" <test suite>
|
||||
```
|
||||
|
||||
#### Specific example
|
||||
|
||||
open a terminal and run:
|
||||
|
||||
```bash
|
||||
docker run --network host --rm --init -it playwright/chrome:playwright-1.46.0
|
||||
```
|
||||
|
||||
and in another terminal, run:
|
||||
|
||||
```bash
|
||||
PW_TEST_CONNECT_WS_ENDPOINT=ws://127.0.0.1:4444/ npm run playwright test --project="Google Chrome" e2e/playwright/command-bar-tests.spec.ts
|
||||
```
|
||||
|
||||
run a specific test change the test from `test('...` to `test.only('...`
|
||||
(note if you commit this, the tests will instantly fail without running any of the tests)
|
||||
|
||||
**Gotcha**: running the docker container with a mismatched image against your `./node_modules/playwright` will cause a failure. Make sure the versions are matched and up to date.
|
||||
|
||||
run headed
|
||||
|
||||
```
|
||||
npm run playwright test --headed
|
||||
```
|
||||
|
||||
run with step through debugger
|
||||
|
||||
```
|
||||
PWDEBUG=1 npm run playwright test
|
||||
```
|
||||
|
||||
However, if you want a debugger I recommend using VSCode and the `playwright` extension, as the above command is a cruder debugger that steps into every function call which is annoying.
|
||||
With the extension you can set a breakpoint after `waitForDefaultPlanesVisibilityChange` in order to skip app loading, then the vscode debugger's "step over" is much better for being able to stay at the right level of abstraction as you debug the code.
|
||||
|
||||
If you want to limit to a single browser use `--project="webkit"` or `firefox`, `Google Chrome`
|
||||
Or comment out browsers in `playwright.config.ts`.
|
||||
|
||||
note chromium has encoder compat issues which is why were testing against the branded 'Google Chrome'
|
||||
|
||||
You may consider using the VSCode extension, it's useful for running individual threads, but some some reason the "record a test" is locked to chromium with we can't use. A work around is to us the CI `npm run playwright codegen -b wk --load-storage ./store localhost:3000`
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
|
||||
Where `./store` should look like this
|
||||
|
||||
</summary>
|
||||
|
||||
```JSON
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "http://localhost:3000",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "store",
|
||||
"value": "{\"state\":{\"openPanes\":[\"code\"]},\"version\":0}"
|
||||
},
|
||||
{
|
||||
"name": "persistCode",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"name": "TOKEN_PERSIST_KEY",
|
||||
"value": "your-token"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
However because much of our tests involve clicking in the stream at specific locations, it's code-gen looks `await page.locator('video').click();` when really we need to use a pixel coord, so I think it's of limited use.
|
||||
|
||||
### Unit and component tests
|
||||
|
||||
If you already haven't, run the following:
|
||||
|
||||
```
|
||||
npm
|
||||
npm run build:wasm
|
||||
npm start
|
||||
```
|
||||
|
||||
and finally:
|
||||
|
||||
```
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
For individual testing:
|
||||
|
||||
```
|
||||
npm run test abstractSyntaxTree -t "unexpected closed curly brace" --silent=false
|
||||
```
|
||||
|
||||
Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro) tests, in interactive mode by default.
|
||||
|
||||
### Rust tests
|
||||
|
||||
**Dependencies**
|
||||
|
||||
- `KITTYCAD_API_TOKEN`
|
||||
- `cargo-nextest`
|
||||
- `just`
|
||||
|
||||
#### Setting KITTYCAD_API_TOKEN
|
||||
|
||||
Use the production zoo.dev token, set this environment variable before running the tests
|
||||
|
||||
#### Installing cargonextest
|
||||
|
||||
```
|
||||
cd rust
|
||||
cargo search cargo-nextest
|
||||
cargo install cargo-nextest
|
||||
```
|
||||
|
||||
#### just
|
||||
|
||||
install [`just`](https://github.com/casey/just?tab=readme-ov-file#pre-built-binaries)
|
||||
|
||||
#### Running the tests
|
||||
|
||||
```bash
|
||||
# With just
|
||||
# Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set
|
||||
# Make sure you installed cargo-nextest
|
||||
# Make sure you installed just
|
||||
cd rust
|
||||
just test
|
||||
```
|
||||
|
||||
```bash
|
||||
# Without just
|
||||
# Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set
|
||||
# Make sure you installed cargo-nextest
|
||||
cd rust
|
||||
export RUST_BRACKTRACE="full" && cargo nextest run --workspace --test-threads=1
|
||||
```
|
||||
|
||||
Where `XXX` is an API token from the production engine (NOT the dev environment).
|
||||
|
||||
We recommend using [nextest](https://nexte.st/) to run the Rust tests (its faster and is used in CI). Once installed, run the tests using
|
||||
|
||||
```
|
||||
cd rust
|
||||
KITTYCAD_API_TOKEN=XXX cargo run nextest
|
||||
```
|
||||
|
||||
### Mapping CI CD jobs to local commands
|
||||
|
||||
When you see the CI CD fail on jobs you may wonder three things
|
||||
|
||||
- Do I have a bug in my code?
|
||||
- Is the test flaky?
|
||||
- Is there a bug in `main`?
|
||||
|
||||
To answer these questions the following commands will give you confidence to locate the issue.
|
||||
|
||||
#### Static Analysis
|
||||
|
||||
Part of the CI CD pipeline performs static analysis on the code. Use the following commands to mimic the CI CD jobs.
|
||||
|
||||
The following set of commands should get us closer to one and done commands to instantly retest issues.
|
||||
|
||||
```
|
||||
npm run test-setup
|
||||
```
|
||||
|
||||
> Gotcha, are packages up to date and is the wasm built?
|
||||
|
||||
```
|
||||
npm run tsc
|
||||
npm run fmt-check
|
||||
npm run lint
|
||||
npm run test:unit:local
|
||||
```
|
||||
|
||||
> Gotcha: Our unit tests have integration tests in them. You need to run a localhost server to run the unit tests.
|
||||
|
||||
#### E2E Tests
|
||||
|
||||
**Playwright Electron**
|
||||
|
||||
These E2E tests run in electron. There are tests that are skipped if they are ran in a windows, linux, or macos environment. We can use playwright tags to implement test skipping.
|
||||
|
||||
```
|
||||
npm run test:playwright:electron:local
|
||||
npm run test:playwright:electron:windows:local
|
||||
npm run test:playwright:electron:macos:local
|
||||
npm run test:playwright:electron:ubuntu:local
|
||||
```
|
||||
|
||||
> Why does it say local? The CI CD commands that run in the pipeline cannot be ran locally. A single command will not properly setup the testing environment locally.
|
||||
|
||||
#### Some notes on CI
|
||||
|
||||
The tests are broken into snapshot tests and non-snapshot tests, and they run in that order, they automatically commit new snap shots, so if you see an image commit check it was an intended change. If we have non-determinism in the snapshots such that they are always committing new images, hopefully this annoyance makes us fix them asap, if you notice this happening let Kurt know. But for the odd occasion `git reset --hard HEAD~ && git push -f` is your friend.
|
||||
|
||||
How to interpret failing playwright tests?
|
||||
If your tests fail, click through to the action and see that the tests failed on a line that includes `await page.getByTestId('loading').waitFor({ state: 'detached' })`, this means the test fail because the stream never started. It's you choice if you want to re-run the test, or ignore the failure.
|
||||
|
||||
We run on ubuntu and macos, because safari doesn't work on linux because of the dreaded "no RTCPeerConnection variable" error. But linux runs first and then macos for the same reason that we limit the number of parallel tests to 1 because we limit stream connections per user, so tests would start failing we if let them run together.
|
||||
|
||||
If something fails on CI you can download the artifact, unzip it and then open `playwright-report/data/<UUID>.zip` with https://trace.playwright.dev/ to see what happened.
|
||||
|
||||
#### Getting started writing a playwright test in our app
|
||||
|
||||
Besides following the instructions above and using the playwright docs, our app is weird because of the whole stream thing, which means our testing is weird. Because we've just figured out this stuff and therefore docs might go stale quick here's a 15min vid/tutorial
|
||||
|
||||
https://github.com/KittyCAD/modeling-app/assets/29681384/6f5e8e85-1003-4fd9-be7f-f36ce833942d
|
||||
|
||||
<details>
|
||||
|
||||
<summary>
|
||||
PS: for the debug panel, the following JSON is useful for snapping the camera
|
||||
</summary>
|
||||
|
||||
```JSON
|
||||
{"type":"modeling_cmd_req","cmd_id":"054e5472-e5e9-4071-92d7-1ce3bac61956","cmd":{"type":"default_camera_look_at","center":{"x":15,"y":0,"z":0},"up":{"x":0,"y":0,"z":1},"vantage":{"x":30,"y":30,"z":30}}}
|
||||
```
|
||||
|
||||
</details>
|
||||
Finally, if you'd like to run a development build or contribute to the project, please visit our [contributor guide](CONTRIBUTING.md) to get started.
|
||||
|
||||
## KCL
|
||||
|
||||
For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/rust/kcl-lib).
|
||||
|
||||
### 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`.
|
||||
To contribute to the KittyCAD Language, see the [README](https://github.com/KittyCAD/modeling-app/tree/main/rust/kcl-lib) for KCL.
|
||||
|
48
biome.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.6.0/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"noUnusedVariables": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "warn"
|
||||
},
|
||||
"style": {
|
||||
"useBlockStatements": "error",
|
||||
"useShorthandArrayType": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentWidth": 2,
|
||||
"indentStyle": "space",
|
||||
"lineWidth": 80,
|
||||
"formatWithErrors": true
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"jsxQuoteStyle": "double",
|
||||
"trailingCommas": "es5",
|
||||
"semicolons": "asNeeded"
|
||||
},
|
||||
"parser": {
|
||||
"unsafeParameterDecoratorsEnabled": true
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"cssModules": true
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["**/*.json"]
|
||||
}
|
||||
}
|
@ -10,8 +10,10 @@ Draw an angled line from the current origin, constructing a line segment such th
|
||||
|
||||
```js
|
||||
angledLineThatIntersects(
|
||||
data: AngledLineThatIntersectsData,
|
||||
sketch: Sketch,
|
||||
angle: number,
|
||||
intersectTag: TagIdentifier,
|
||||
offset?: number,
|
||||
tag?: TagDeclarator,
|
||||
): Sketch
|
||||
```
|
||||
@ -21,9 +23,11 @@ angledLineThatIntersects(
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `data` | [`AngledLineThatIntersectsData`](/docs/kcl/types/AngledLineThatIntersectsData) | Data for drawing an angled line that intersects with a given line. | Yes |
|
||||
| `sketch` | [`Sketch`](/docs/kcl/types/Sketch) | | Yes |
|
||||
| [`tag`](/docs/kcl/types/tag) | [`TagDeclarator`](/docs/kcl/types#tag-declaration) | | No |
|
||||
| `sketch` | [`Sketch`](/docs/kcl/types/Sketch) | Which sketch should this path be added to? | Yes |
|
||||
| `angle` | [`number`](/docs/kcl/types/number) | Which angle should the line be drawn at? | Yes |
|
||||
| `intersectTag` | [`TagIdentifier`](/docs/kcl/types#tag-identifier) | The tag of the line to intersect with | Yes |
|
||||
| `offset` | [`number`](/docs/kcl/types/number) | The offset from the intersecting line. Defaults to 0. | No |
|
||||
| [`tag`](/docs/kcl/types/tag) | [`TagDeclarator`](/docs/kcl/types#tag-declaration) | Create a new tag which refers to this line | No |
|
||||
|
||||
### Returns
|
||||
|
||||
@ -38,11 +42,7 @@ exampleSketch = startSketchOn(XZ)
|
||||
|> line(endAbsolute = [5, 10])
|
||||
|> line(endAbsolute = [-10, 10], tag = $lineToIntersect)
|
||||
|> line(endAbsolute = [0, 20])
|
||||
|> angledLineThatIntersects({
|
||||
angle = 80,
|
||||
intersectTag = lineToIntersect,
|
||||
offset = 10
|
||||
}, %)
|
||||
|> angledLineThatIntersects(angle = 80, intersectTag = lineToIntersect, offset = 10)
|
||||
|> close()
|
||||
|
||||
example = extrude(exampleSketch, length = 10)
|
||||
|
@ -24,8 +24,8 @@ atan2(
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `y` | [`number`](/docs/kcl/types/number) | | Yes |
|
||||
| `x` | [`number`](/docs/kcl/types/number) | | Yes |
|
||||
| `y` | [`number`](/docs/kcl/types/number) | Y | Yes |
|
||||
| `x` | [`number`](/docs/kcl/types/number) | X | Yes |
|
||||
|
||||
### Returns
|
||||
|
||||
@ -37,7 +37,7 @@ atan2(
|
||||
```js
|
||||
sketch001 = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> angledLine(angle = toDegrees(atan2(1.25, 2)), length = 20)
|
||||
|> angledLine(angle = toDegrees(atan2(y = 1.25, x = 2)), length = 20)
|
||||
|> yLine(endAbsolute = 0)
|
||||
|> close()
|
||||
|
||||
|
@ -80,7 +80,7 @@ part001 = cube([0, 0], 20)
|
||||
// We tag the chamfer to reference it later.
|
||||
|> chamfer(length = 10, tags = [getOppositeEdge(line1)], tag = $chamfer1)
|
||||
|
||||
sketch001 = startSketchOn(part001, chamfer1)
|
||||
sketch001 = startSketchOn(part001, face = chamfer1)
|
||||
|> startProfileAt([10, 10], %)
|
||||
|> line(end = [2, 0])
|
||||
|> line(end = [0, 2])
|
||||
|
@ -9,6 +9,8 @@ layout: manual
|
||||
|
||||
### `std`
|
||||
|
||||
- [`END`](/docs/kcl/consts/std-END)
|
||||
- [`START`](/docs/kcl/consts/std-START)
|
||||
- [`X`](/docs/kcl/consts/std-X)
|
||||
- [`XY`](/docs/kcl/consts/std-XY)
|
||||
- [`XZ`](/docs/kcl/consts/std-XZ)
|
||||
|
15
docs/kcl/consts/std-END.md
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
title: "std::END"
|
||||
excerpt: "Identifies the ending face of an extrusion. I.e., the new face created by an extrusion."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Identifies the ending face of an extrusion. I.e., the new face created by an extrusion.
|
||||
|
||||
|
||||
|
||||
```js
|
||||
std::END: string = 'end'
|
||||
```
|
||||
|
||||
|
15
docs/kcl/consts/std-START.md
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
title: "std::START"
|
||||
excerpt: "Identifies the starting face of an extrusion. I.e., the face which is extruded."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Identifies the starting face of an extrusion. I.e., the face which is extruded.
|
||||
|
||||
|
||||
|
||||
```js
|
||||
std::START: string = 'start'
|
||||
```
|
||||
|
||||
|
@ -69,11 +69,11 @@ case = startSketchOn(-XZ)
|
||||
|> close()
|
||||
|> extrude(length = 65)
|
||||
|
||||
thing1 = startSketchOn(case, 'end')
|
||||
thing1 = startSketchOn(case, face = END)
|
||||
|> circle(center = [-size / 2, -size / 2], radius = 25)
|
||||
|> extrude(length = 50)
|
||||
|
||||
thing2 = startSketchOn(case, 'end')
|
||||
thing2 = startSketchOn(case, face = END)
|
||||
|> circle(center = [size / 2, -size / 2], radius = 25)
|
||||
|> extrude(length = 50)
|
||||
|
||||
|
@ -24,12 +24,14 @@ layout: manual
|
||||
* **std**
|
||||
* [`Axis2d`](kcl/types/Axis2d)
|
||||
* [`Axis3d`](kcl/types/Axis3d)
|
||||
* [`END`](kcl/consts/std-END)
|
||||
* [`Edge`](kcl/types/Edge)
|
||||
* [`Face`](kcl/types/Face)
|
||||
* [`Helix`](kcl/types/Helix)
|
||||
* [`Plane`](kcl/types/Plane)
|
||||
* [`Point2d`](kcl/types/Point2d)
|
||||
* [`Point3d`](kcl/types/Point3d)
|
||||
* [`START`](kcl/consts/std-START)
|
||||
* [`Sketch`](kcl/types/Sketch)
|
||||
* [`Solid`](kcl/types/Solid)
|
||||
* [`X`](kcl/consts/std-X)
|
||||
|
@ -37,7 +37,7 @@ line(
|
||||
```js
|
||||
triangle = startSketchOn(XZ)
|
||||
|> startProfileAt([0, 0], %)
|
||||
// The 'end' argument means it ends at exactly [10, 0].
|
||||
// The END argument means it ends at exactly [10, 0].
|
||||
// This is an absolute measurement, it is NOT relative to
|
||||
// the start of the sketch.
|
||||
|> line(endAbsolute = [10, 0])
|
||||
|
@ -109,3 +109,98 @@ Coordinate systems:
|
||||
- `zoo` (the default), forward: -Y, up: +Z, handedness: right
|
||||
- `opengl`, forward: +Z, up: +Y, handedness: right
|
||||
- `vulkan`, forward: +Z, up: -Y, handedness: left
|
||||
|
||||
### Performance
|
||||
|
||||
Parallelized foreign-file imports now let you overlap file reads, initialization,
|
||||
and rendering. To maximize throughput, you need to understand the three distinct
|
||||
stages—reading, initializing (background render start), and invocation (blocking)
|
||||
—and structure your code to defer blocking operations until the end.
|
||||
|
||||
#### Foreign Import Execution Stages
|
||||
|
||||
1. **Import (Read) Stage**
|
||||
```norun
|
||||
import "tests/inputs/cube.step" as cube
|
||||
```
|
||||
- Reads the file from disk and makes its API available.
|
||||
- **Does _not_** start Engine rendering or block your script.
|
||||
|
||||
2. **Initialization (Background Render) Stage**
|
||||
```norun
|
||||
import "tests/inputs/cube.step" as cube
|
||||
|
||||
myCube = cube // <- This line starts background rendering
|
||||
```
|
||||
- Invoking the imported symbol (assignment or plain call) triggers Engine rendering _in the background_.
|
||||
- This kick‑starts the render pipeline but doesn’t block—you can continue other work while the Engine processes the model.
|
||||
|
||||
3. **Invocation (Blocking) Stage**
|
||||
```norun
|
||||
import "tests/inputs/cube.step" as cube
|
||||
|
||||
myCube = cube
|
||||
|
||||
myCube
|
||||
|> translate(z=10) // <- This line blocks
|
||||
```
|
||||
- Any method call (e.g., `translate`, `scale`, `rotate`) waits for the background render to finish before applying transformations.
|
||||
- This is the only point where your script will block.
|
||||
|
||||
> **Nuance:** Foreign imports differ from pure KCL modules—calling the same import symbol multiple times (e.g., `screw` twice) starts background rendering twice.
|
||||
|
||||
#### Best Practices
|
||||
|
||||
##### 1. Defer Blocking Calls
|
||||
Initialize early but delay all transformations until after your heavy computation:
|
||||
```norun
|
||||
import "tests/inputs/cube.step" as cube // 1) Read
|
||||
|
||||
myCube = cube // 2) Background render starts
|
||||
|
||||
|
||||
// --- perform other operations and calculations or setup here ---
|
||||
|
||||
|
||||
myCube
|
||||
|> translate(z=10) // 3) Blocks only here
|
||||
```
|
||||
|
||||
##### 2. Encapsulate Imports in Modules
|
||||
Keep `main.kcl` free of reads and initialization; wrap them:
|
||||
|
||||
```norun
|
||||
// imports.kcl
|
||||
import "tests/inputs/cube.step" as cube // Read only
|
||||
|
||||
|
||||
export myCube = cube // Kick off rendering
|
||||
```
|
||||
|
||||
```norun
|
||||
// main.kcl
|
||||
import myCube from "imports.kcl" // Import the initialized object
|
||||
|
||||
|
||||
// ... computations ...
|
||||
|
||||
|
||||
myCube
|
||||
|> translate(z=10) // Blocking call at the end
|
||||
```
|
||||
|
||||
##### 3. Avoid Immediate Method Calls
|
||||
|
||||
```norun
|
||||
import "tests/inputs/cube.step" as cube
|
||||
|
||||
cube
|
||||
|> translate(z=10) // Blocks immediately, negating parallelism
|
||||
```
|
||||
|
||||
Both calling methods right on `cube` immediately or leaving an implicit import without assignment introduce blocking.
|
||||
|
||||
#### Future Improvements
|
||||
|
||||
Upcoming releases will auto‑analyze dependencies and only block when truly necessary. Until then, explicit deferral and modular wrapping give you the best performance.
|
||||
|
||||
|
@ -20,7 +20,7 @@ offsetPlane(
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `plane` | [`PlaneData`](/docs/kcl/types/PlaneData) | The plane (e.g. 'XY') which this new plane is created from. | Yes |
|
||||
| `plane` | [`PlaneData`](/docs/kcl/types/PlaneData) | The plane (e.g. XY) which this new plane is created from. | Yes |
|
||||
| `offset` | [`number`](/docs/kcl/types/number) | Distance from the standard plane this new plane will be created at. | Yes |
|
||||
|
||||
### Returns
|
||||
|
@ -61,11 +61,11 @@ case = startSketchOn(XY)
|
||||
|> close(%)
|
||||
|> extrude(length = 65)
|
||||
|
||||
thing1 = startSketchOn(case, 'end')
|
||||
thing1 = startSketchOn(case, face = END)
|
||||
|> circle(center = [-size / 2, -size / 2], radius = 25)
|
||||
|> extrude(length = 50)
|
||||
|
||||
thing2 = startSketchOn(case, 'end')
|
||||
thing2 = startSketchOn(case, face = END)
|
||||
|> circle(center = [size / 2, -size / 2], radius = 25)
|
||||
|> extrude(length = 50)
|
||||
|
||||
@ -92,7 +92,7 @@ case = startSketchOn(XY)
|
||||
|> close(%)
|
||||
|> extrude(length = 65)
|
||||
|
||||
thing1 = startSketchOn(case, 'end')
|
||||
thing1 = startSketchOn(case, face = END)
|
||||
|> circle(center = [-size / 2, -size / 2], radius = 25)
|
||||
|> extrude(length = 50)
|
||||
|
||||
|
@ -53,7 +53,7 @@ rotate(
|
||||
|
||||
### Returns
|
||||
|
||||
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid or an imported geometry.
|
||||
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid, sketch, or an imported geometry.
|
||||
|
||||
|
||||
### Examples
|
||||
|
@ -37,7 +37,7 @@ scale(
|
||||
|
||||
### Returns
|
||||
|
||||
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid or an imported geometry.
|
||||
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid, sketch, or an imported geometry.
|
||||
|
||||
|
||||
### Examples
|
||||
|
@ -18,8 +18,8 @@ The point is if you want to export the result of a sketch on a face, you only ne
|
||||
|
||||
```js
|
||||
startSketchOn(
|
||||
data: SketchData,
|
||||
tag?: FaceTag,
|
||||
planeOrSolid: SketchData,
|
||||
face?: FaceTag,
|
||||
): SketchSurface
|
||||
```
|
||||
|
||||
@ -28,8 +28,8 @@ startSketchOn(
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `data` | [`SketchData`](/docs/kcl/types/SketchData) | Data for start sketch on. You can start a sketch on a plane or an solid. | Yes |
|
||||
| [`tag`](/docs/kcl/types/tag) | [`FaceTag`](/docs/kcl/types/FaceTag) | A tag for a face. | No |
|
||||
| `planeOrSolid` | [`SketchData`](/docs/kcl/types/SketchData) | The plane or solid to sketch on | Yes |
|
||||
| `face` | [`FaceTag`](/docs/kcl/types/FaceTag) | Identify a face of a solid if a solid is specified as the input argument (`plane_or_solid`) | No |
|
||||
|
||||
### Returns
|
||||
|
||||
@ -48,7 +48,7 @@ exampleSketch = startSketchOn(XY)
|
||||
|
||||
example = extrude(exampleSketch, length = 5)
|
||||
|
||||
exampleSketch002 = startSketchOn(example, 'end')
|
||||
exampleSketch002 = startSketchOn(example, face = END)
|
||||
|> startProfileAt([1, 1], %)
|
||||
|> line(end = [8, 0])
|
||||
|> line(end = [0, 8])
|
||||
@ -57,7 +57,7 @@ exampleSketch002 = startSketchOn(example, 'end')
|
||||
|
||||
example002 = extrude(exampleSketch002, length = 5)
|
||||
|
||||
exampleSketch003 = startSketchOn(example002, 'end')
|
||||
exampleSketch003 = startSketchOn(example002, face = END)
|
||||
|> startProfileAt([2, 2], %)
|
||||
|> line(end = [6, 0])
|
||||
|> line(end = [0, 6])
|
||||
@ -82,7 +82,7 @@ exampleSketch = startSketchOn(XY)
|
||||
|
||||
example = extrude(exampleSketch, length = 5, tagEnd = $end01)
|
||||
|
||||
exampleSketch002 = startSketchOn(example, end01)
|
||||
exampleSketch002 = startSketchOn(example, face = end01)
|
||||
|> startProfileAt([1, 1], %)
|
||||
|> line(end = [8, 0])
|
||||
|> line(end = [0, 8])
|
||||
@ -91,7 +91,7 @@ exampleSketch002 = startSketchOn(example, end01)
|
||||
|
||||
example002 = extrude(exampleSketch002, length = 5, tagEnd = $end02)
|
||||
|
||||
exampleSketch003 = startSketchOn(example002, end02)
|
||||
exampleSketch003 = startSketchOn(example002, face = end02)
|
||||
|> startProfileAt([2, 2], %)
|
||||
|> line(end = [6, 0])
|
||||
|> line(end = [0, 6])
|
||||
@ -113,7 +113,7 @@ exampleSketch = startSketchOn(XY)
|
||||
|
||||
example = extrude(exampleSketch, length = 10)
|
||||
|
||||
exampleSketch002 = startSketchOn(example, sketchingFace)
|
||||
exampleSketch002 = startSketchOn(example, face = sketchingFace)
|
||||
|> startProfileAt([1, 1], %)
|
||||
|> line(end = [8, 0])
|
||||
|> line(end = [0, 8])
|
||||
@ -122,7 +122,7 @@ exampleSketch002 = startSketchOn(example, sketchingFace)
|
||||
|
||||
example002 = extrude(exampleSketch002, length = 10)
|
||||
|
||||
exampleSketch003 = startSketchOn(example002, sketchingFace002)
|
||||
exampleSketch003 = startSketchOn(example002, face = sketchingFace002)
|
||||
|> startProfileAt([-8, 12], %)
|
||||
|> line(end = [0, 6])
|
||||
|> line(end = [6, 0])
|
||||
@ -148,7 +148,7 @@ exampleSketch = startSketchOn(XY)
|
||||
|
||||
example = revolve(exampleSketch, axis = Y, angle = 180)
|
||||
|
||||
exampleSketch002 = startSketchOn(example, 'end')
|
||||
exampleSketch002 = startSketchOn(example, face = END)
|
||||
|> startProfileAt([4.5, -5], %)
|
||||
|> line(end = [0, 5])
|
||||
|> line(end = [5, 0])
|
||||
@ -182,7 +182,7 @@ example = revolve(
|
||||
tagEnd = $end01,
|
||||
)
|
||||
|
||||
exampleSketch002 = startSketchOn(example, end01)
|
||||
exampleSketch002 = startSketchOn(example, face = end01)
|
||||
|> startProfileAt([4.5, -5], %)
|
||||
|> line(end = [0, 5])
|
||||
|> line(end = [5, 0])
|
||||
@ -196,12 +196,10 @@ example002 = extrude(exampleSketch002, length = 5)
|
||||
|
||||
```js
|
||||
a1 = startSketchOn({
|
||||
plane = {
|
||||
origin = { x = 0, y = 0, z = 0 },
|
||||
xAxis = { x = 1, y = 0, z = 0 },
|
||||
yAxis = { x = 0, y = 1, z = 0 },
|
||||
zAxis = { x = 0, y = 0, z = 1 }
|
||||
}
|
||||
origin = { x = 0, y = 0, z = 0 },
|
||||
xAxis = { x = 1, y = 0, z = 0 },
|
||||
yAxis = { x = 0, y = 1, z = 0 },
|
||||
zAxis = { x = 0, y = 0, z = 1 }
|
||||
})
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> line(end = [100.0, 0])
|
||||
|
@ -9,7 +9,7 @@ Compute the cosine of a number (in radians).
|
||||
|
||||
|
||||
```js
|
||||
cos(@num: number(rad)): number(_)
|
||||
cos(@num: number(Angle)): number(_)
|
||||
```
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ cos(@num: number(rad)): number(_)
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `num` | `number(rad)` | | Yes |
|
||||
| `num` | `number(Angle)` | | Yes |
|
||||
|
||||
### Returns
|
||||
|
||||
|
@ -11,9 +11,9 @@ cartesian (x/y/z grid) coordinates.
|
||||
|
||||
```js
|
||||
polar(
|
||||
angle: number(deg),
|
||||
length: number(mm),
|
||||
): [number(mm); 2]
|
||||
angle: number(Angle),
|
||||
length: number(Length),
|
||||
): Point2d
|
||||
```
|
||||
|
||||
|
||||
@ -21,12 +21,12 @@ polar(
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `angle` | `number(deg)` | | Yes |
|
||||
| `length` | `number(mm)` | | Yes |
|
||||
| `angle` | `number(Angle)` | | Yes |
|
||||
| `length` | `number(Length)` | | Yes |
|
||||
|
||||
### Returns
|
||||
|
||||
`[number(mm); 2]`
|
||||
[`Point2d`](/docs/kcl/types/Point2d)
|
||||
|
||||
|
||||
### Examples
|
||||
|
@ -9,7 +9,7 @@ Compute the sine of a number (in radians).
|
||||
|
||||
|
||||
```js
|
||||
sin(@num: number(rad)): number(_)
|
||||
sin(@num: number(Angle)): number(_)
|
||||
```
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ sin(@num: number(rad)): number(_)
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `num` | `number(rad)` | | Yes |
|
||||
| `num` | `number(Angle)` | | Yes |
|
||||
|
||||
### Returns
|
||||
|
||||
|
@ -9,7 +9,7 @@ Compute the tangent of a number (in radians).
|
||||
|
||||
|
||||
```js
|
||||
tan(@num: number(rad)): number(_)
|
||||
tan(@num: number(Angle)): number(_)
|
||||
```
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ tan(@num: number(rad)): number(_)
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `num` | `number(rad)` | | Yes |
|
||||
| `num` | `number(Angle)` | | Yes |
|
||||
|
||||
### Returns
|
||||
|
||||
|
@ -21,10 +21,10 @@ revolved around the same axis.
|
||||
revolve(
|
||||
@sketches: [Sketch; 1+],
|
||||
axis: Axis2d | Edge,
|
||||
angle?: number(deg),
|
||||
tolerance?: number(mm),
|
||||
angle?: number(Angle),
|
||||
tolerance?: number(Length),
|
||||
symmetric?: bool,
|
||||
bidirectionalAngle?: number(deg),
|
||||
bidirectionalAngle?: number(Angle),
|
||||
tagStart?: tag,
|
||||
tagEnd?: tag,
|
||||
): Solid
|
||||
@ -37,10 +37,10 @@ revolve(
|
||||
|----------|------|-------------|----------|
|
||||
| `sketches` | `[Sketch; 1+]` | The sketch or set of sketches that should be revolved | Yes |
|
||||
| `axis` | `Axis2d | Edge` | Axis of revolution. | Yes |
|
||||
| `angle` | `number(deg)` | Angle to revolve (in degrees). Default is 360. | No |
|
||||
| `tolerance` | `number(mm)` | Tolerance for the revolve operation. | No |
|
||||
| `angle` | `number(Angle)` | Angle to revolve (in degrees). Default is 360. | No |
|
||||
| `tolerance` | `number(Length)` | Tolerance for the revolve operation. | No |
|
||||
| `symmetric` | [`bool`](/docs/kcl/types/bool) | If true, the extrusion will happen symmetrically around the sketch. Otherwise, the extrusion will happen on only one side of the sketch. | No |
|
||||
| `bidirectionalAngle` | `number(deg)` | If specified, will also revolve in the opposite direction to 'angle' to the specified angle. If 'symmetric' is true, this value is ignored. | No |
|
||||
| `bidirectionalAngle` | `number(Angle)` | If specified, will also revolve in the opposite direction to 'angle' to the specified angle. If 'symmetric' is true, this value is ignored. | No |
|
||||
| `tagStart` | [`tag`](/docs/kcl/types/tag) | A named tag for the face at the start of the revolve, i.e. the original sketch. | No |
|
||||
| `tagEnd` | [`tag`](/docs/kcl/types/tag) | A named tag for the face at the end of the revolve. | No |
|
||||
|
||||
@ -108,7 +108,7 @@ part001 = startSketchOn(XY)
|
||||
|> close()
|
||||
|> revolve(axis = Y, angle = 180)
|
||||
|
||||
part002 = startSketchOn(part001, 'end')
|
||||
part002 = startSketchOn(part001, face = END)
|
||||
|> startProfileAt([4.5, -5], %)
|
||||
|> line(end = [0, 5])
|
||||
|> line(end = [5, 0])
|
||||
@ -128,7 +128,7 @@ box = startSketchOn(XY)
|
||||
|> close()
|
||||
|> extrude(length = 20)
|
||||
|
||||
sketch001 = startSketchOn(box, "END")
|
||||
sketch001 = startSketchOn(box, face = END)
|
||||
|> circle( center = [10,10], radius = 4 )
|
||||
|> revolve(
|
||||
angle = -90,
|
||||
@ -147,7 +147,7 @@ box = startSketchOn(XY)
|
||||
|> close()
|
||||
|> extrude(length = 20)
|
||||
|
||||
sketch001 = startSketchOn(box, "END")
|
||||
sketch001 = startSketchOn(box, face = END)
|
||||
|> circle( center = [10,10], radius = 4 )
|
||||
|> revolve(
|
||||
angle = 90,
|
||||
@ -166,7 +166,7 @@ box = startSketchOn(XY)
|
||||
|> close()
|
||||
|> extrude(length = 20)
|
||||
|
||||
sketch001 = startSketchOn(box, "END")
|
||||
sketch001 = startSketchOn(box, face = END)
|
||||
|> circle( center = [10,10], radius = 4 )
|
||||
|> revolve(
|
||||
angle = 90,
|
||||
|
@ -13,7 +13,7 @@ the provided (x, y) origin point.
|
||||
circle(
|
||||
@sketch_or_surface: Sketch | Plane | Face,
|
||||
center: Point2d,
|
||||
radius: number,
|
||||
radius: number(Length),
|
||||
tag?: tag,
|
||||
): Sketch
|
||||
```
|
||||
@ -25,7 +25,7 @@ circle(
|
||||
|----------|------|-------------|----------|
|
||||
| `sketch_or_surface` | [`Sketch`](/docs/kcl/types/Sketch) OR [`Plane`](/docs/kcl/types/Plane) OR [`Face`](/docs/kcl/types/Face) | Sketch to extend, or plane or surface to sketch on. | Yes |
|
||||
| `center` | [`Point2d`](/docs/kcl/types/Point2d) | The center of the circle. | Yes |
|
||||
| `radius` | [`number`](/docs/kcl/types/number) | The radius of the circle. | Yes |
|
||||
| `radius` | `number(Length)` | The radius of the circle. | Yes |
|
||||
| [`tag`](/docs/kcl/types/tag) | [`tag`](/docs/kcl/types/tag) | Create a new tag which refers to this circle. | No |
|
||||
|
||||
### Returns
|
||||
|
@ -110,7 +110,7 @@ sketch0011 = startSketchOn(XY)
|
||||
|> mirror2d( axis = X )
|
||||
|> extrude(length = 10)
|
||||
|
||||
sketch002 = startSketchOn(sketch0011, 'END')
|
||||
sketch002 = startSketchOn(sketch0011, face = END)
|
||||
|> circle( center = [-0.01, 1.58], radius = 1.2 )
|
||||
|> extrude(length = 1.2)
|
||||
|
||||
|
12007
docs/kcl/std.json
@ -33,7 +33,7 @@ translate(
|
||||
|
||||
### Returns
|
||||
|
||||
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid or an imported geometry.
|
||||
[`SolidOrSketchOrImportedGeometry`](/docs/kcl/types/SolidOrSketchOrImportedGeometry) - Data for a solid, sketch, or an imported geometry.
|
||||
|
||||
|
||||
### Examples
|
||||
|
@ -145,7 +145,7 @@ This helps keep your code neat and avoid unnecessary declarations.
|
||||
Say you have a long pipeline of sketch functions, like this:
|
||||
|
||||
```norun
|
||||
startSketchOn('XZ')
|
||||
startSketchOn(XZ)
|
||||
|> line(%, end = [3, 4])
|
||||
|> line(%, end = [10, 10])
|
||||
|> line(%, end = [-13, -14])
|
||||
@ -160,7 +160,7 @@ means that `|> line(%, end = [3, 4])` and `|> line(end = [3, 4])` are equivalent
|
||||
could be rewritten as
|
||||
|
||||
```norun
|
||||
startSketchOn('XZ')
|
||||
startSketchOn(XZ)
|
||||
|> line(end = [3, 4])
|
||||
|> line(end = [10, 10])
|
||||
|> line(end = [-13, -14])
|
||||
@ -182,7 +182,7 @@ The syntax for declaring a tag is `$myTag` you would use it in the following
|
||||
way:
|
||||
|
||||
```norun
|
||||
startSketchOn('XZ')
|
||||
startSketchOn(XZ)
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001)
|
||||
|> angledLine(
|
||||
@ -217,7 +217,7 @@ However if the code was written like this:
|
||||
|
||||
```norun
|
||||
fn rect(origin) {
|
||||
return startSketchOn('XZ')
|
||||
return startSketchOn(XZ)
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001)
|
||||
|> angledLine(
|
||||
@ -227,7 +227,7 @@ fn rect(origin) {
|
||||
)
|
||||
|> angledLine(
|
||||
angle = segAng(rectangleSegmentA001),
|
||||
length = -segLen(rectangleSegmentA001)
|
||||
length = -segLen(rectangleSegmentA001),
|
||||
tag = $rectangleSegmentC001,
|
||||
)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
@ -247,7 +247,7 @@ For example the following code works.
|
||||
|
||||
```norun
|
||||
fn rect(origin) {
|
||||
return startSketchOn('XZ')
|
||||
return startSketchOn(XZ)
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine(angle = 0, length = 191.26, tag = $rectangleSegmentA001)
|
||||
|> angledLine(
|
||||
@ -257,7 +257,7 @@ fn rect(origin) {
|
||||
)
|
||||
|> angledLine(
|
||||
angle = segAng(rectangleSegmentA001),
|
||||
length = -segLen(rectangleSegmentA001)
|
||||
length = -segLen(rectangleSegmentA001),
|
||||
tag = $rectangleSegmentC001,
|
||||
)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
@ -268,11 +268,8 @@ rect([0, 0])
|
||||
myRect = rect([20, 0])
|
||||
|
||||
myRect
|
||||
|> extrude(10, %)
|
||||
|> fillet(
|
||||
radius = 0.5,
|
||||
tags = [myRect.tags.rectangleSegmentA001]
|
||||
)
|
||||
|> extrude(length = 10)
|
||||
|> fillet(radius = 0.5, tags = [myRect.tags.rectangleSegmentA001])
|
||||
```
|
||||
|
||||
See how we use the tag `rectangleSegmentA001` in the `fillet` function outside
|
||||
|
@ -1,46 +0,0 @@
|
||||
---
|
||||
title: "AngledLineData"
|
||||
excerpt: "Data to draw an angled line."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data to draw an angled line.
|
||||
|
||||
|
||||
|
||||
|
||||
**This schema accepts any of the following:**
|
||||
|
||||
An angle and length with explicitly named parameters
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `angle` |[`number`](/docs/kcl/types/number)| The angle of the line (in degrees). | No |
|
||||
| `length` |[`number`](/docs/kcl/types/number)| The length of the line. | No |
|
||||
|
||||
|
||||
----
|
||||
An angle and length given as a pair
|
||||
|
||||
**Type:** `array`
|
||||
|
||||
`[number, number]`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -16,8 +16,8 @@ Data for drawing an angled line that intersects with a given line.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `angle` |[`number`](/docs/kcl/types/number)| The angle of the line. | No |
|
||||
| `angle` |`TyF64`| The angle of the line. | No |
|
||||
| `intersectTag` |[`TagIdentifier`](/docs/kcl/types#tag-identifier)| The tag of the line to intersect with. | No |
|
||||
| `offset` |[`number`](/docs/kcl/types/number)| The offset from the intersecting line. | No |
|
||||
| `offset` |`TyF64`| The offset from the intersecting line. | No |
|
||||
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
---
|
||||
title: "AngledLineToData"
|
||||
excerpt: "Data to draw an angled line to a point."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data to draw an angled line to a point.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `angle` |[`number`](/docs/kcl/types/number)| The angle of the line. | No |
|
||||
| `to` |[`number`](/docs/kcl/types/number)| The point to draw to. | No |
|
||||
|
||||
|
@ -23,9 +23,9 @@ Angles and radius with an optional tag.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `angleStart` |[`number`](/docs/kcl/types/number) (**maximum:** 360.0) (**minimum:** -360.0)| The start angle. | No |
|
||||
| `angleEnd` |[`number`](/docs/kcl/types/number) (**maximum:** 360.0) (**minimum:** -360.0)| The end angle. | No |
|
||||
| `radius` |[`number`](/docs/kcl/types/number)| The radius. | No |
|
||||
| `angleStart` |`TyF64`| The start angle. | No |
|
||||
| `angleEnd` |`TyF64`| The end angle. | No |
|
||||
| `radius` |`TyF64`| The radius. | No |
|
||||
|
||||
|
||||
----
|
||||
@ -41,9 +41,9 @@ Center, to and radius with an optional tag.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `center` |`[number, number]`| The center. | No |
|
||||
| `to` |`[number, number]`| The to point. | No |
|
||||
| `radius` |[`number`](/docs/kcl/types/number)| The radius. | No |
|
||||
| `center` |`[, `TyF64`, `TyF64`]`| The center. | No |
|
||||
| `to` |`[, `TyF64`, `TyF64`]`| The to point. | No |
|
||||
| `radius` |`TyF64`| The radius. | No |
|
||||
|
||||
|
||||
----
|
||||
|
@ -16,7 +16,7 @@ Data to draw a three point arc (arcTo).
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `end` |`[number, number]`| End point of the arc. A point in 3D space | No |
|
||||
| `interior` |`[number, number]`| Interior point of the arc. A point in 3D space | No |
|
||||
| `end` |`[, `TyF64`, `TyF64`]`| End point of the arc. A point in 3D space | No |
|
||||
| `interior` |`[, `TyF64`, `TyF64`]`| Interior point of the arc. A point in 3D space | No |
|
||||
|
||||
|
||||
|
@ -16,8 +16,8 @@ Data to draw a bezier curve.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `to` |`[number, number]`| The to point. | No |
|
||||
| `control1` |`[number, number]`| The first control point. | No |
|
||||
| `control2` |`[number, number]`| The second control point. | No |
|
||||
| `to` |`[, `TyF64`, `TyF64`]`| The to point. | No |
|
||||
| `control1` |`[, `TyF64`, `TyF64`]`| The first control point. | No |
|
||||
| `control2` |`[, `TyF64`, `TyF64`]`| The second control point. | No |
|
||||
|
||||
|
||||
|
@ -26,7 +26,7 @@ A fillet.
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `fillet`| | No |
|
||||
| `id` |[`string`](/docs/kcl/types/string)| The id of the engine command that called this fillet. | No |
|
||||
| `radius` |[`number`](/docs/kcl/types/number)| | No |
|
||||
| `radius` |`TyF64`| | No |
|
||||
| `edgeId` |[`string`](/docs/kcl/types/string)| The engine id of the edge to fillet. | No |
|
||||
| [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| | No |
|
||||
|
||||
@ -46,7 +46,7 @@ A chamfer.
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `chamfer`| | No |
|
||||
| `id` |[`string`](/docs/kcl/types/string)| The id of the engine command that called this chamfer. | No |
|
||||
| `length` |[`number`](/docs/kcl/types/number)| | No |
|
||||
| `length` |`TyF64`| | No |
|
||||
| `edgeId` |[`string`](/docs/kcl/types/string)| The engine id of the edge to chamfer. | No |
|
||||
| [`tag`](/docs/kcl/types/tag) |[`TagDeclarator`](/docs/kcl/types#tag-declaration)| | No |
|
||||
|
||||
|
@ -130,6 +130,21 @@ A unit of length.
|
||||
| `type` |enum: `Yards`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Unknown`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
@ -178,6 +193,21 @@ A unit of angle.
|
||||
| `type` |enum: `Radians`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Unknown`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
@ -7,7 +7,7 @@ layout: manual
|
||||
A point in two dimensional space.
|
||||
|
||||
```kcl
|
||||
type Point2d = [number; 2]
|
||||
type Point2d = [number(Length); 2]
|
||||
```
|
||||
|
||||
[`Point2d`](/docs/kcl/types/Point2d) is an alias for a two-element array of [number](/docs/kcl/types/number)s. To write a value
|
||||
|
@ -7,7 +7,7 @@ layout: manual
|
||||
A point in three dimensional space.
|
||||
|
||||
```kcl
|
||||
type Point3d = [number; 3]
|
||||
type Point3d = [number(Length); 3]
|
||||
```
|
||||
|
||||
[`Point3d`](/docs/kcl/types/Point3d) is an alias for a three-element array of [number](/docs/kcl/types/number)s. To write a value
|
||||
|
@ -16,9 +16,9 @@ Data for drawing a polygon
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `radius` |[`number`](/docs/kcl/types/number)| The radius of the polygon | No |
|
||||
| `radius` |`TyF64`| The radius of the polygon | No |
|
||||
| `numSides` |`integer`| The number of sides in the polygon | No |
|
||||
| `center` |`[number, number]`| The center point of the polygon | No |
|
||||
| `center` |`[, `TyF64`, `TyF64`]`| The center point of the polygon | No |
|
||||
| `inscribed` |`boolean`| Whether the polygon is inscribed (true) or circumscribed (false) about a circle with the specified radius | No |
|
||||
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
---
|
||||
title: "SolidOrSketchOrImportedGeometry"
|
||||
excerpt: "Data for a solid or an imported geometry."
|
||||
excerpt: "Data for a solid, sketch, or an imported geometry."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data for a solid or an imported geometry.
|
||||
Data for a solid, sketch, or an imported geometry.
|
||||
|
||||
|
||||
|
||||
|
@ -16,7 +16,7 @@ Data to draw a tangential arc.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `radius` |[`number`](/docs/kcl/types/number)| Radius of the arc. Not to be confused with Raiders of the Lost Ark. | No |
|
||||
| `offset` |[`number`](/docs/kcl/types/number)| Offset of the arc, in degrees. | No |
|
||||
| `radius` |`TyF64`| Radius of the arc. Not to be confused with Raiders of the Lost Ark. | No |
|
||||
| `offset` |`TyF64`| Offset of the arc, in degrees. | No |
|
||||
|
||||
|
||||
|
@ -43,6 +43,21 @@ A unit of angle.
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Unknown`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -103,6 +103,21 @@ A unit of length.
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Unknown`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -94,11 +94,8 @@ rect([0, 0])
|
||||
myRect = rect([20, 0])
|
||||
|
||||
myRect
|
||||
|> extrude(10, %)
|
||||
|> fillet(
|
||||
radius = 0.5,
|
||||
tags = [myRect.tags.rectangleSegmentA001]
|
||||
)
|
||||
|> extrude(length = 10)
|
||||
|> fillet(radius = 0.5, tags = [myRect.tags.rectangleSegmentA001])
|
||||
```
|
||||
|
||||
See how we use the tag `rectangleSegmentA001` in the `fillet` function outside
|
||||
|
@ -66,15 +66,17 @@ async function doBasicSketch(
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(u.codeLocator)
|
||||
.toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|
||||
await expect(
|
||||
u.codeLocator
|
||||
).toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|
||||
|> xLine(length = ${commonPoints.num1})`)
|
||||
}
|
||||
await page.waitForTimeout(500)
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(u.codeLocator)
|
||||
.toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
|
||||
await expect(
|
||||
u.codeLocator
|
||||
).toHaveText(`sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
|
||||
commonPoints.startAt
|
||||
}, sketch001)
|
||||
|> xLine(length = ${commonPoints.num1})
|
||||
@ -85,8 +87,9 @@ async function doBasicSketch(
|
||||
await page.waitForTimeout(200)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(u.codeLocator)
|
||||
.toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
|
||||
await expect(
|
||||
u.codeLocator
|
||||
).toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
|
||||
commonPoints.startAt
|
||||
}, sketch001)
|
||||
|> xLine(length = ${commonPoints.num1})
|
||||
@ -142,8 +145,9 @@ async function doBasicSketch(
|
||||
|
||||
// Open the code pane.
|
||||
await u.openKclCodePanel()
|
||||
await expect(u.codeLocator)
|
||||
.toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
|
||||
await expect(
|
||||
u.codeLocator
|
||||
).toHaveText(`@settings(defaultLengthUnit = in)sketch001 = startSketchOn(XZ)profile001 = startProfileAt(${
|
||||
commonPoints.startAt
|
||||
}, sketch001)
|
||||
|> xLine(length = ${commonPoints.num1}, tag = $seg01)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { join } from 'path'
|
||||
import { bracket } from '@e2e/playwright/fixtures/bracket'
|
||||
import fsp from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from '@e2e/playwright/storageStates'
|
||||
import {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import path, { join } from 'path'
|
||||
import { KCL_DEFAULT_LENGTH } from '@src/lib/constants'
|
||||
import * as fsp from 'fs/promises'
|
||||
import path, { join } from 'path'
|
||||
|
||||
import {
|
||||
executorInputPath,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import fsp from 'fs/promises'
|
||||
import path from 'path'
|
||||
import fsp from 'fs/promises'
|
||||
|
||||
import {
|
||||
executorInputPath,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { join } from 'path'
|
||||
import { uuidv4 } from '@src/lib/utils'
|
||||
import fsp from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
import {
|
||||
TEST_COLORS,
|
||||
@ -82,7 +82,7 @@ sketch001 = startSketchOn(XY)
|
||||
.poll(() =>
|
||||
page.locator('[data-receive-command-type="scene_clear_all"]').count()
|
||||
)
|
||||
.toBe(1)
|
||||
.toBe(2)
|
||||
await expect
|
||||
.poll(() => page.locator('[data-message-type="execution-done"]').count())
|
||||
.toBe(2)
|
||||
@ -106,7 +106,7 @@ sketch001 = startSketchOn(XY)
|
||||
).toHaveCount(3)
|
||||
await expect(
|
||||
page.locator('[data-receive-command-type="scene_clear_all"]')
|
||||
).toHaveCount(1)
|
||||
).toHaveCount(2)
|
||||
})
|
||||
|
||||
test('ensure we use the cache, and do not clear on append', async ({
|
||||
@ -133,7 +133,7 @@ sketch001 = startSketchOn(XY)
|
||||
await u.openDebugPanel()
|
||||
await expect(
|
||||
page.locator('[data-receive-command-type="scene_clear_all"]')
|
||||
).toHaveCount(1)
|
||||
).toHaveCount(2)
|
||||
await expect(
|
||||
page.locator('[data-message-type="execution-done"]')
|
||||
).toHaveCount(2)
|
||||
@ -161,7 +161,7 @@ sketch001 = startSketchOn(XY)
|
||||
).toHaveCount(3)
|
||||
await expect(
|
||||
page.locator('[data-receive-command-type="scene_clear_all"]')
|
||||
).toHaveCount(1)
|
||||
).toHaveCount(2)
|
||||
})
|
||||
|
||||
test('if you click the format button it formats your code', async ({
|
||||
@ -409,8 +409,9 @@ sketch_001 = startSketchOn(XY)
|
||||
// Hit alt+shift+f to format the code
|
||||
await page.keyboard.press('Alt+Shift+KeyF')
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch001 = startSketchOn(XY)
|
||||
await expect(
|
||||
page.locator('.cm-content')
|
||||
).toHaveText(`sketch001 = startSketchOn(XY)
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line(end = [20, 0])
|
||||
|> line(end = [0, 20])
|
||||
@ -462,8 +463,9 @@ sketch_001 = startSketchOn(XY)
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch_001 = startSketchOn(XY)
|
||||
await expect(
|
||||
page.locator('.cm-content')
|
||||
).toHaveText(`sketch_001 = startSketchOn(XY)
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line(end = [20, 0])
|
||||
|> line(end = [0, 20])
|
||||
@ -731,7 +733,7 @@ sketch_001 = startSketchOn(XY)
|
||||
|> close()
|
||||
|> extrude(length = 10)
|
||||
|
||||
sketch001 = startSketchOn(box, revolveAxis)
|
||||
sketch001 = startSketchOn(box, face = revolveAxis)
|
||||
|> startProfileAt([5, 10], %)
|
||||
|> line(end = [0, -10])
|
||||
|> line(end = [2, 0])
|
||||
@ -978,8 +980,9 @@ sketch001 = startSketchOn(XZ)
|
||||
await page.keyboard.up('Control')
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch001 = startSketchOn(XZ)
|
||||
await expect(
|
||||
page.locator('.cm-content')
|
||||
).toHaveText(`sketch001 = startSketchOn(XZ)
|
||||
|> startProfileAt([4.61, -14.01], %)
|
||||
|> line(end = [12.73, -0.09])
|
||||
|> tangentialArc(endAbsolute = [24.95, -5.38])
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as fsp from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import * as fsp from 'fs/promises'
|
||||
|
||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||
|
||||
@ -44,7 +44,7 @@ const FEATURE_TREE_SKETCH_CODE = `sketch001 = startSketchOn(XZ)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close(%)
|
||||
extrude001 = extrude(sketch001, length = 10)
|
||||
sketch002 = startSketchOn(extrude001, rectangleSegmentB001)
|
||||
sketch002 = startSketchOn(extrude001, face = rectangleSegmentB001)
|
||||
|> circle(
|
||||
center = [-1, 2],
|
||||
radius = .5
|
||||
@ -194,7 +194,7 @@ test.describe('Feature Tree pane', () => {
|
||||
highlightedCode: '',
|
||||
diagnostics: [],
|
||||
activeLines: [
|
||||
'sketch002=startSketchOn(extrude001,rectangleSegmentB001)',
|
||||
'sketch002=startSketchOn(extrude001,face=rectangleSegmentB001)',
|
||||
],
|
||||
})
|
||||
await toolbar.exitSketchBtn.click()
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { FILE_EXT } from '@src/lib/constants'
|
||||
import * as fs from 'fs'
|
||||
import * as fsp from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { FILE_EXT } from '@src/lib/constants'
|
||||
import * as fsp from 'fs/promises'
|
||||
|
||||
import {
|
||||
createProject,
|
||||
|
@ -1,9 +1,9 @@
|
||||
import type { Locator, Page, Request, Route, TestInfo } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import type { Locator, Page, Request, Route, TestInfo } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
type CmdBarSerialised =
|
||||
export type CmdBarSerialised =
|
||||
| {
|
||||
stage: 'commandBarClosed'
|
||||
}
|
||||
@ -178,6 +178,13 @@ export class CmdBarFixture {
|
||||
return this.page.getByRole('option', options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the Create new variable button for kcl input
|
||||
*/
|
||||
createNewVariable = async () => {
|
||||
await this.page.getByRole('button', { name: 'Create new variable' }).click()
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a snapshot of the request sent to the text-to-cad API endpoint
|
||||
* and saves it to a file named after the current test.
|
||||
|
@ -7,11 +7,11 @@ import type {
|
||||
} from '@playwright/test'
|
||||
import { _electron as electron } from '@playwright/test'
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'path'
|
||||
import { SETTINGS_FILE_NAME } from '@src/lib/constants'
|
||||
import type { DeepPartial } from '@src/lib/types'
|
||||
import fsp from 'fs/promises'
|
||||
import fs from 'node:fs'
|
||||
import path from 'path'
|
||||
|
||||
import type { Settings } from '@rust/kcl-lib/bindings/Settings'
|
||||
|
||||
|
@ -44,6 +44,7 @@ export class SceneFixture {
|
||||
public page: Page
|
||||
public streamWrapper!: Locator
|
||||
public networkToggleConnected!: Locator
|
||||
public engineConnectionsSpinner!: Locator
|
||||
public startEditSketchBtn!: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
@ -52,6 +53,7 @@ export class SceneFixture {
|
||||
this.networkToggleConnected = page
|
||||
.getByTestId('network-toggle-ok')
|
||||
.or(page.getByTestId('network-toggle-other'))
|
||||
this.engineConnectionsSpinner = page.getByTestId(`loading-engine`)
|
||||
this.startEditSketchBtn = page
|
||||
.getByRole('button', { name: 'Start Sketch' })
|
||||
.or(page.getByRole('button', { name: 'Edit Sketch' }))
|
||||
@ -228,6 +230,7 @@ export class SceneFixture {
|
||||
connectionEstablished = async () => {
|
||||
const timeout = 30000
|
||||
await expect(this.networkToggleConnected).toBeVisible({ timeout })
|
||||
await expect(this.engineConnectionsSpinner).not.toBeVisible()
|
||||
}
|
||||
|
||||
settled = async (cmdBar: CmdBarFixture) => {
|
||||
@ -235,6 +238,7 @@ export class SceneFixture {
|
||||
|
||||
await expect(this.startEditSketchBtn).not.toBeDisabled({ timeout: 15_000 })
|
||||
await expect(this.startEditSketchBtn).toBeVisible()
|
||||
await expect(this.engineConnectionsSpinner).not.toBeVisible()
|
||||
|
||||
await cmdBar.openCmdBar()
|
||||
await cmdBar.chooseCommand('Settings · app · show debug panel')
|
||||
@ -253,6 +257,14 @@ export class SceneFixture {
|
||||
await expectPixelColor(this.page, colour, coords, diff)
|
||||
}
|
||||
|
||||
expectPixelColorNotToBe = async (
|
||||
colour: [number, number, number] | [number, number, number][],
|
||||
coords: { x: number; y: number },
|
||||
diff: number
|
||||
) => {
|
||||
await expectPixelColorNotToBe(this.page, colour, coords, diff)
|
||||
}
|
||||
|
||||
get gizmo() {
|
||||
return this.page.locator('[aria-label*=gizmo]')
|
||||
}
|
||||
@ -274,37 +286,69 @@ function isColourArray(
|
||||
return isArray(colour[0])
|
||||
}
|
||||
|
||||
export async function expectPixelColor(
|
||||
type PixelColorMatchMode = 'matches' | 'differs'
|
||||
|
||||
export async function checkPixelColor(
|
||||
page: Page,
|
||||
colour: [number, number, number] | [number, number, number][],
|
||||
coords: { x: number; y: number },
|
||||
diff: number
|
||||
diff: number,
|
||||
mode: PixelColorMatchMode
|
||||
) {
|
||||
let finalValue = colour
|
||||
const isMatchMode = mode === 'matches'
|
||||
const actionText = isMatchMode ? 'expecting' : 'not expecting'
|
||||
const functionName = isMatchMode
|
||||
? 'ExpectPixelColor'
|
||||
: 'ExpectPixelColourNotToBe'
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const pixel = (await getPixelRGBs(page)(coords, 1))[0]
|
||||
if (!pixel) return null
|
||||
finalValue = pixel
|
||||
|
||||
let matches
|
||||
if (!isColourArray(colour)) {
|
||||
return pixel.every(
|
||||
matches = pixel.every(
|
||||
(channel, index) => Math.abs(channel - colour[index]) < diff
|
||||
)
|
||||
} else {
|
||||
matches = colour.some((c) =>
|
||||
c.every((channel, index) => Math.abs(pixel[index] - channel) < diff)
|
||||
)
|
||||
}
|
||||
return colour.some((c) =>
|
||||
c.every((channel, index) => Math.abs(pixel[index] - channel) < diff)
|
||||
)
|
||||
|
||||
return isMatchMode ? matches : !matches
|
||||
},
|
||||
{ timeout: 10_000 }
|
||||
)
|
||||
.toBeTruthy()
|
||||
.catch((cause) => {
|
||||
throw new Error(
|
||||
`ExpectPixelColor: point ${JSON.stringify(
|
||||
`${functionName}: point ${JSON.stringify(
|
||||
coords
|
||||
)} was expecting ${colour} but got ${finalValue}`,
|
||||
)} was ${actionText} ${colour} but got ${finalValue}`,
|
||||
{ cause }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export async function expectPixelColor(
|
||||
page: Page,
|
||||
colour: [number, number, number] | [number, number, number][],
|
||||
coords: { x: number; y: number },
|
||||
diff: number
|
||||
) {
|
||||
await checkPixelColor(page, colour, coords, diff, 'matches')
|
||||
}
|
||||
|
||||
export async function expectPixelColorNotToBe(
|
||||
page: Page,
|
||||
colour: [number, number, number] | [number, number, number][],
|
||||
coords: { x: number; y: number },
|
||||
diff: number
|
||||
) {
|
||||
await checkPixelColor(page, colour, coords, diff, 'differs')
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ export class ToolbarFixture {
|
||||
offsetPlaneButton!: Locator
|
||||
helixButton!: Locator
|
||||
startSketchBtn!: Locator
|
||||
insertButton!: Locator
|
||||
lineBtn!: Locator
|
||||
tangentialArcBtn!: Locator
|
||||
circleBtn!: Locator
|
||||
@ -44,7 +45,7 @@ export class ToolbarFixture {
|
||||
featureTreePane!: Locator
|
||||
gizmo!: Locator
|
||||
gizmoDisabled!: Locator
|
||||
insertButton!: Locator
|
||||
loadButton!: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
@ -59,6 +60,7 @@ export class ToolbarFixture {
|
||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||
this.helixButton = page.getByTestId('helix')
|
||||
this.startSketchBtn = page.getByTestId('sketch')
|
||||
this.insertButton = page.getByTestId('insert')
|
||||
this.lineBtn = page.getByTestId('line')
|
||||
this.tangentialArcBtn = page.getByTestId('tangential-arc')
|
||||
this.circleBtn = page.getByTestId('circle-center')
|
||||
@ -68,6 +70,7 @@ export class ToolbarFixture {
|
||||
this.fileTreeBtn = page.locator('[id="files-button-holder"]')
|
||||
this.createFileBtn = page.getByTestId('create-file-button')
|
||||
this.treeInputField = page.getByTestId('tree-input-field')
|
||||
this.loadButton = page.getByTestId('load-external-model-pane-button')
|
||||
|
||||
this.filePane = page.locator('#files-pane')
|
||||
this.featureTreePane = page.locator('#feature-tree-pane')
|
||||
@ -79,8 +82,6 @@ export class ToolbarFixture {
|
||||
// element or two different elements can represent these states.
|
||||
this.gizmo = page.getByTestId('gizmo')
|
||||
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
|
||||
|
||||
this.insertButton = page.getByTestId('insert-pane-button')
|
||||
}
|
||||
|
||||
get logoLink() {
|
||||
@ -204,6 +205,11 @@ export class ToolbarFixture {
|
||||
).toBeVisible()
|
||||
await this.page.getByTestId('dropdown-three-point-arc').click()
|
||||
}
|
||||
selectLine = async () => {
|
||||
await this.page
|
||||
.getByRole('button', { name: 'line Line', exact: true })
|
||||
.click()
|
||||
}
|
||||
|
||||
async closePane(paneId: SidebarType) {
|
||||
return closePane(this.page, paneId + SIDEBAR_BUTTON_SUFFIX)
|
||||
|
@ -1,9 +1,9 @@
|
||||
import path from 'path'
|
||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||
import * as fsp from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
test.describe('Import UI tests', () => {
|
||||
test('shows toast when trying to sketch on imported face', async ({
|
||||
test('shows toast when trying to sketch on imported face, and hovering over imported geometry should NOT highlight any code', async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
@ -44,8 +44,8 @@ profile001 = startProfileAt([-134.53, -56.17], sketch001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
|
||||
|> close()
|
||||
extrude001 = extrude(profile001, length = 100)
|
||||
sketch003 = startSketchOn(extrude001, seg02)
|
||||
sketch002 = startSketchOn(extrude001, seg01)`
|
||||
sketch003 = startSketchOn(extrude001, face = seg02)
|
||||
sketch002 = startSketchOn(extrude001, face = seg01)`
|
||||
)
|
||||
})
|
||||
|
||||
@ -93,7 +93,6 @@ sketch002 = startSketchOn(extrude001, seg01)`
|
||||
await toolbar.startSketchPlaneSelection()
|
||||
|
||||
// Click on a face from the imported model
|
||||
// await new Promise(() => {})
|
||||
await importedFaceClick()
|
||||
|
||||
// Verify toast appears with correct content
|
||||
|
@ -19,8 +19,9 @@ class MyAPIReporter implements Reporter {
|
||||
target: process.env.TARGET || null,
|
||||
platform: process.env.RUNNER_OS || process.platform,
|
||||
// Extra test and result data
|
||||
annotations: test.annotations.map((a) => a.type),
|
||||
retries: result.retry,
|
||||
annotations: test.annotations.map((a) => a.type), // e.g. 'fail' or 'fixme'
|
||||
retry: result.retry,
|
||||
tags: test.tags, // e.g. '@snapshot' or '@skipWin'
|
||||
// Extra environment variables
|
||||
CI_COMMIT_SHA: process.env.CI_COMMIT_SHA || null,
|
||||
CI_PR_NUMBER: process.env.CI_PR_NUMBER || null,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import fsp from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import fsp from 'fs/promises'
|
||||
|
||||
import { executorInputPath } from '@e2e/playwright/test-utils'
|
||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { join } from 'path'
|
||||
import { PROJECT_SETTINGS_FILE_NAME } from '@src/lib/constants'
|
||||
import * as fsp from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
import type { NamedView } from '@rust/kcl-lib/bindings/NamedView'
|
||||
|
||||
import {
|
||||
createProject,
|
||||
orRunWhenFullSuiteEnabled,
|
||||
perProjectSettingsToToml,
|
||||
runningOnMac,
|
||||
runningOnWindows,
|
||||
tomlToPerProjectSettings,
|
||||
} from '@e2e/playwright/test-utils'
|
||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||
@ -63,9 +62,7 @@ function tomlStringOverWriteNamedViewUuids(toml: string): string {
|
||||
}
|
||||
|
||||
test.describe('Named view tests', () => {
|
||||
if (runningOnMac()) {
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
}
|
||||
test.skip(runningOnWindows(), 'Windows line endings break snapshot matching')
|
||||
test('Verify project.toml is not created', async ({ page }, testInfo) => {
|
||||
// Create project and load it
|
||||
const projectName = 'named-views'
|
||||
@ -127,7 +124,7 @@ test.describe('Named view tests', () => {
|
||||
|
||||
// Write the entire tomlString to a snapshot.
|
||||
// There are many key/value pairs to check this is a safer match.
|
||||
expect(tomlString).toMatchSnapshot('verify-named-view-gets-created')
|
||||
expect(tomlString).toMatchSnapshot('verify-named-view-gets-created.toml')
|
||||
}).toPass()
|
||||
})
|
||||
test('Verify named view gets deleted', async ({
|
||||
@ -173,7 +170,7 @@ test.describe('Named view tests', () => {
|
||||
|
||||
// Write the entire tomlString to a snapshot.
|
||||
// There are many key/value pairs to check this is a safer match.
|
||||
expect(tomlString).toMatchSnapshot('verify-named-view-gets-created')
|
||||
expect(tomlString).toMatchSnapshot('verify-named-view-gets-created.toml')
|
||||
}).toPass()
|
||||
|
||||
// Delete a named view
|
||||
@ -193,7 +190,7 @@ test.describe('Named view tests', () => {
|
||||
|
||||
// Write the entire tomlString to a snapshot.
|
||||
// There are many key/value pairs to check this is a safer match.
|
||||
expect(tomlString).toMatchSnapshot('verify-named-view-gets-deleted')
|
||||
expect(tomlString).toMatchSnapshot('verify-named-view-gets-deleted.toml')
|
||||
}).toPass()
|
||||
})
|
||||
test('Verify named view gets loaded', async ({
|
||||
@ -239,7 +236,7 @@ test.describe('Named view tests', () => {
|
||||
|
||||
// Write the entire tomlString to a snapshot.
|
||||
// There are many key/value pairs to check this is a safer match.
|
||||
expect(tomlString).toMatchSnapshot('verify-named-view-gets-created')
|
||||
expect(tomlString).toMatchSnapshot('verify-named-view-gets-created.toml')
|
||||
}).toPass()
|
||||
|
||||
// Create a load a named view
|
||||
@ -308,7 +305,9 @@ test.describe('Named view tests', () => {
|
||||
|
||||
// Write the entire tomlString to a snapshot.
|
||||
// There are many key/value pairs to check this is a safer match.
|
||||
expect(tomlString).toMatchSnapshot('verify-two-named-view-gets-created')
|
||||
expect(tomlString).toMatchSnapshot(
|
||||
'verify-two-named-view-gets-created.toml'
|
||||
)
|
||||
}).toPass()
|
||||
})
|
||||
})
|
||||
|
@ -1,16 +0,0 @@
|
||||
[settings]
|
||||
modeling = { }
|
||||
text_editor = { }
|
||||
command_bar = { }
|
||||
|
||||
[settings.app.named_views.0656fb1a-9640-473e-b334-591dc70c0138]
|
||||
name = "uuid1"
|
||||
eye_offset = 1_378.0059
|
||||
fov_y = 45
|
||||
is_ortho = false
|
||||
ortho_scale_enabled = true
|
||||
ortho_scale_factor = 1.6
|
||||
pivot_position = [ 0, 0, 0 ]
|
||||
pivot_rotation = [ 0.5380994, 0.0, 0.0, 0.8428814 ]
|
||||
world_coord_system = "right_handed_up_z"
|
||||
version = 1
|
@ -1,28 +0,0 @@
|
||||
[settings]
|
||||
modeling = { }
|
||||
text_editor = { }
|
||||
command_bar = { }
|
||||
|
||||
[settings.app.named_views.0656fb1a-9640-473e-b334-591dc70c0138]
|
||||
name = "uuid1"
|
||||
eye_offset = 1_378.0059
|
||||
fov_y = 45
|
||||
is_ortho = false
|
||||
ortho_scale_enabled = true
|
||||
ortho_scale_factor = 1.6
|
||||
pivot_position = [ 0, 0, 0 ]
|
||||
pivot_rotation = [ 0.5380994, 0.0, 0.0, 0.8428814 ]
|
||||
world_coord_system = "right_handed_up_z"
|
||||
version = 1
|
||||
|
||||
[settings.app.named_views.c810cf04-c6cc-4a4a-8b11-17bf445dcab7]
|
||||
name = "uuid2"
|
||||
eye_offset = 1_378.0059
|
||||
fov_y = 45
|
||||
is_ortho = false
|
||||
ortho_scale_enabled = true
|
||||
ortho_scale_factor = 1.6
|
||||
pivot_position = [ 1_826.5239, 0.0, 0.0 ]
|
||||
pivot_rotation = [ 0.5380994, 0.0, 0.0, 0.8428814 ]
|
||||
world_coord_system = "right_handed_up_z"
|
||||
version = 1
|
@ -534,7 +534,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
||||
const expected = 'Open project'
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
test('Modeling.File.Load a sample model', async ({
|
||||
test('Modeling.File.Load external model', async ({
|
||||
tronApp,
|
||||
cmdBar,
|
||||
page,
|
||||
@ -555,10 +555,10 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
||||
throw new Error('app or app.applicationMenu is missing')
|
||||
}
|
||||
const openProject = app.applicationMenu.getMenuItemById(
|
||||
'File.Load a sample model'
|
||||
'File.Load external model'
|
||||
)
|
||||
if (!openProject) {
|
||||
throw new Error('File.Load a sample model')
|
||||
throw new Error('File.Load external model')
|
||||
}
|
||||
openProject.click()
|
||||
})
|
||||
@ -568,44 +568,7 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
||||
const actual = await cmdBar.cmdBarElement
|
||||
.getByTestId('command-name')
|
||||
.textContent()
|
||||
const expected = 'Open sample'
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
test('Modeling.File.Insert from project file', async ({
|
||||
tronApp,
|
||||
cmdBar,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
}) => {
|
||||
if (!tronApp) {
|
||||
throwTronAppMissing()
|
||||
return
|
||||
}
|
||||
await homePage.goToModelingScene()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// Run electron snippet to find the Menu!
|
||||
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
|
||||
await tronApp.electron.evaluate(async ({ app }) => {
|
||||
if (!app || !app.applicationMenu) {
|
||||
throw new Error('app or app.applicationMenu is missing')
|
||||
}
|
||||
const openProject = app.applicationMenu.getMenuItemById(
|
||||
'File.Insert from project file'
|
||||
)
|
||||
if (!openProject) {
|
||||
throw new Error('File.Insert from project file')
|
||||
}
|
||||
openProject.click()
|
||||
})
|
||||
// Check that the command bar is opened
|
||||
await expect(cmdBar.cmdBarElement).toBeVisible()
|
||||
// Check the placeholder project name exists
|
||||
const actual = await cmdBar.cmdBarElement
|
||||
.getByTestId('command-name')
|
||||
.textContent()
|
||||
const expected = 'Insert'
|
||||
const expected = 'Load external model'
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
test('Modeling.File.Export current part', async ({
|
||||
@ -2159,6 +2122,44 @@ test.describe('Native file menu', { tag: ['@electron'] }, () => {
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
|
||||
test('Modeling.Design.Insert from project file', async ({
|
||||
tronApp,
|
||||
cmdBar,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
}) => {
|
||||
if (!tronApp) {
|
||||
throwTronAppMissing()
|
||||
return
|
||||
}
|
||||
await homePage.goToModelingScene()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// Run electron snippet to find the Menu!
|
||||
await page.waitForTimeout(100) // wait for createModelingPageMenu() to run
|
||||
await tronApp.electron.evaluate(async ({ app }) => {
|
||||
if (!app || !app.applicationMenu) {
|
||||
throw new Error('app or app.applicationMenu is missing')
|
||||
}
|
||||
const openProject = app.applicationMenu.getMenuItemById(
|
||||
'Design.Insert from project file'
|
||||
)
|
||||
if (!openProject) {
|
||||
throw new Error('Design.Insert from project file')
|
||||
}
|
||||
openProject.click()
|
||||
})
|
||||
// Check that the command bar is opened
|
||||
await expect(cmdBar.cmdBarElement).toBeVisible()
|
||||
// Check the placeholder project name exists
|
||||
const actual = await cmdBar.cmdBarElement
|
||||
.getByTestId('command-name')
|
||||
.textContent()
|
||||
const expected = 'Insert'
|
||||
expect(actual).toBe(expected)
|
||||
})
|
||||
|
||||
test('Modeling.Design.Create with Zoo Text-To-CAD', async ({
|
||||
tronApp,
|
||||
cmdBar,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { join } from 'path'
|
||||
import { bracket } from '@e2e/playwright/fixtures/bracket'
|
||||
import { onboardingPaths } from '@src/routes/Onboarding/paths'
|
||||
import fsp from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
import { expectPixelColor } from '@e2e/playwright/fixtures/sceneFixture'
|
||||
import {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as fsp from 'fs/promises'
|
||||
import path from 'path'
|
||||
import * as fsp from 'fs/promises'
|
||||
|
||||
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
|
||||
import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
|
||||
@ -169,6 +169,180 @@ test.describe('Point-and-click assemblies tests', () => {
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`Insert the bracket part into an assembly and transform it`,
|
||||
{ tag: ['@electron'] },
|
||||
async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
tronApp,
|
||||
}) => {
|
||||
if (!tronApp) {
|
||||
fail()
|
||||
}
|
||||
|
||||
const midPoint = { x: 500, y: 250 }
|
||||
const moreToTheRightPoint = { x: 900, y: 250 }
|
||||
const bgColor: [number, number, number] = [30, 30, 30]
|
||||
const partColor: [number, number, number] = [100, 100, 100]
|
||||
const tolerance = 30
|
||||
const u = await getUtils(page)
|
||||
const gizmo = page.locator('[aria-label*=gizmo]')
|
||||
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
|
||||
|
||||
await test.step('Setup parts and expect empty assembly scene', async () => {
|
||||
const projectName = 'assembly'
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const bracketDir = path.join(dir, projectName)
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await Promise.all([
|
||||
fsp.copyFile(
|
||||
path.join('public', 'kcl-samples', 'bracket', 'main.kcl'),
|
||||
path.join(bracketDir, 'bracket.kcl')
|
||||
),
|
||||
fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''),
|
||||
])
|
||||
})
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.openProject(projectName)
|
||||
await scene.settled(cmdBar)
|
||||
await toolbar.closePane('code')
|
||||
})
|
||||
|
||||
await test.step('Insert kcl as module', async () => {
|
||||
await insertPartIntoAssembly(
|
||||
'bracket.kcl',
|
||||
'bracket',
|
||||
toolbar,
|
||||
cmdBar,
|
||||
page
|
||||
)
|
||||
await toolbar.openPane('code')
|
||||
await editor.expectEditor.toContain(
|
||||
`
|
||||
import "bracket.kcl" as bracket
|
||||
bracket
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// Check scene for changes
|
||||
await toolbar.closePane('code')
|
||||
await u.doAndWaitForCmd(async () => {
|
||||
await gizmo.click({ button: 'right' })
|
||||
await resetCameraButton.click()
|
||||
}, 'zoom_to_fit')
|
||||
await toolbar.closePane('debug')
|
||||
await scene.expectPixelColor(partColor, midPoint, tolerance)
|
||||
await scene.expectPixelColor(bgColor, moreToTheRightPoint, tolerance)
|
||||
})
|
||||
|
||||
await test.step('Set translate on module', async () => {
|
||||
await toolbar.openPane('feature-tree')
|
||||
|
||||
const op = await toolbar.getFeatureTreeOperation('bracket', 0)
|
||||
await op.click({ button: 'right' })
|
||||
await page.getByTestId('context-menu-set-translate').click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'x',
|
||||
currentArgValue: '0',
|
||||
headerArguments: {
|
||||
X: '',
|
||||
Y: '',
|
||||
Z: '',
|
||||
},
|
||||
highlightedHeaderArg: 'x',
|
||||
commandName: 'Translate',
|
||||
})
|
||||
await page.keyboard.insertText('5')
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.keyboard.insertText('0.1')
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.keyboard.insertText('0.2')
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
X: '5',
|
||||
Y: '0.1',
|
||||
Z: '0.2',
|
||||
},
|
||||
commandName: 'Translate',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await toolbar.closePane('feature-tree')
|
||||
await toolbar.openPane('code')
|
||||
await editor.expectEditor.toContain(
|
||||
`
|
||||
bracket
|
||||
|> translate(x = 5, y = 0.1, z = 0.2)
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
// Expect translated part in the scene
|
||||
await scene.expectPixelColor(bgColor, midPoint, tolerance)
|
||||
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
|
||||
})
|
||||
|
||||
await test.step('Set rotate on module', async () => {
|
||||
await toolbar.closePane('code')
|
||||
await toolbar.openPane('feature-tree')
|
||||
|
||||
const op = await toolbar.getFeatureTreeOperation('bracket', 0)
|
||||
await op.click({ button: 'right' })
|
||||
await page.getByTestId('context-menu-set-rotate').click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'roll',
|
||||
currentArgValue: '0',
|
||||
headerArguments: {
|
||||
Roll: '',
|
||||
Pitch: '',
|
||||
Yaw: '',
|
||||
},
|
||||
highlightedHeaderArg: 'roll',
|
||||
commandName: 'Rotate',
|
||||
})
|
||||
await page.keyboard.insertText('0.1')
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.keyboard.insertText('0.2')
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.keyboard.insertText('0.3')
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Roll: '0.1',
|
||||
Pitch: '0.2',
|
||||
Yaw: '0.3',
|
||||
},
|
||||
commandName: 'Rotate',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await toolbar.closePane('feature-tree')
|
||||
await toolbar.openPane('code')
|
||||
await editor.expectEditor.toContain(
|
||||
`
|
||||
bracket
|
||||
|> translate(x = 5, y = 0.1, z = 0.2)
|
||||
|> rotate(roll = 0.1, pitch = 0.2, yaw = 0.3)
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
// Expect no change in the scene as the rotations are tiny
|
||||
await scene.expectPixelColor(bgColor, midPoint, tolerance)
|
||||
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`Insert foreign parts into assembly as whole module import`,
|
||||
{ tag: ['@electron'] },
|
||||
@ -231,10 +405,6 @@ test.describe('Point-and-click assemblies tests', () => {
|
||||
)
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// TODO: remove this once #5780 is fixed
|
||||
await page.reload()
|
||||
|
||||
await scene.settled(cmdBar)
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor(partColor, partPoint, tolerance)
|
||||
@ -279,10 +449,6 @@ test.describe('Point-and-click assemblies tests', () => {
|
||||
)
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// TODO: remove this once #5780 is fixed
|
||||
await page.reload()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor(partColor, partPoint, tolerance)
|
||||
|
@ -1,11 +1,14 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import type { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
|
||||
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
|
||||
import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
|
||||
import { orRunWhenFullSuiteEnabled } from '@e2e/playwright/test-utils'
|
||||
import {
|
||||
orRunWhenFullSuiteEnabled,
|
||||
runningOnWindows,
|
||||
} from '@e2e/playwright/test-utils'
|
||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||
|
||||
// test file is for testing point an click code gen functionality that's not sketch mode related
|
||||
@ -220,7 +223,7 @@ test.describe('Point-and-click tests', () => {
|
||||
)`,
|
||||
|
||||
afterChamferSelectSnippet:
|
||||
'sketch002 = startSketchOn(extrude001, seg03)',
|
||||
'sketch002 = startSketchOn(extrude001, face = seg03)',
|
||||
afterRectangle1stClickSnippet:
|
||||
'startProfileAt([205.96, 254.59], sketch002)',
|
||||
afterRectangle2ndClickSnippet: `angledLine(angle=0,length=11.39,tag=$rectangleSegmentA002)
|
||||
@ -244,7 +247,7 @@ test.describe('Point-and-click tests', () => {
|
||||
)`,
|
||||
|
||||
afterChamferSelectSnippet:
|
||||
'sketch003 = startSketchOn(extrude001, seg04)',
|
||||
'sketch003 = startSketchOn(extrude001, face = seg04)',
|
||||
afterRectangle1stClickSnippet:
|
||||
'startProfileAt([-209.64, 255.28], sketch003)',
|
||||
afterRectangle2ndClickSnippet: `angledLine(angle=0,length=11.56,tag=$rectangleSegmentA003)
|
||||
@ -266,7 +269,7 @@ test.describe('Point-and-click tests', () => {
|
||||
]
|
||||
)`,
|
||||
afterChamferSelectSnippet:
|
||||
'sketch004 = startSketchOn(extrude001, seg05)',
|
||||
'sketch004 = startSketchOn(extrude001, face = seg05)',
|
||||
afterRectangle1stClickSnippet:
|
||||
'startProfileAt([82.57, 322.96], sketch004)',
|
||||
afterRectangle2ndClickSnippet: `angledLine(angle=0,length=11.16,tag=$rectangleSegmentA004)
|
||||
@ -284,7 +287,7 @@ test.describe('Point-and-click tests', () => {
|
||||
beforeChamferSnippetEnd:
|
||||
'|> chamfer(length = 30, tags = [getNextAdjacentEdge(yo)])',
|
||||
afterChamferSelectSnippet:
|
||||
'sketch005 = startSketchOn(extrude001, seg06)',
|
||||
'sketch005 = startSketchOn(extrude001, face = seg06)',
|
||||
afterRectangle1stClickSnippet:
|
||||
'startProfileAt([-23.43, 19.69], sketch005)',
|
||||
afterRectangle2ndClickSnippet: `angledLine(angle=0,length=9.1,tag=$rectangleSegmentA005)
|
||||
@ -309,28 +312,28 @@ extrude001 = extrude(sketch001, length = 100)
|
||||
|> chamfer(length = 30, tags = [seg01], tag = $seg04)
|
||||
|> chamfer(length = 30, tags = [getNextAdjacentEdge(seg02)], tag = $seg05)
|
||||
|> chamfer(length = 30, tags = [getNextAdjacentEdge(yo)], tag = $seg06)
|
||||
sketch005 = startSketchOn(extrude001, seg06)
|
||||
sketch005 = startSketchOn(extrude001, face = seg06)
|
||||
profile004=startProfileAt([-23.43,19.69], sketch005)
|
||||
|> angledLine(angle = 0, length = 9.1, tag = $rectangleSegmentA005)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA005) - 90, length = 84.07)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA005), length = -segLen(rectangleSegmentA005))
|
||||
|> line(endAbsolute=[profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
sketch004 = startSketchOn(extrude001, seg05)
|
||||
sketch004 = startSketchOn(extrude001, face = seg05)
|
||||
profile003 = startProfileAt([82.57, 322.96], sketch004)
|
||||
|> angledLine(angle = 0, length = 11.16, tag = $rectangleSegmentA004)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA004) - 90, length = 103.07)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA004), length = -segLen(rectangleSegmentA004))
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
sketch003 = startSketchOn(extrude001, seg04)
|
||||
sketch003 = startSketchOn(extrude001, face = seg04)
|
||||
profile002 = startProfileAt([-209.64, 255.28], sketch003)
|
||||
|> angledLine(angle = 0, length = 11.56, tag = $rectangleSegmentA003)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA003) - 90, length = 106.84)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA003), length = -segLen(rectangleSegmentA003))
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
sketch002 = startSketchOn(extrude001, seg03)
|
||||
sketch002 = startSketchOn(extrude001, face = seg03)
|
||||
profile001 = startProfileAt([205.96, 254.59], sketch002)
|
||||
|> angledLine(angle = 0, length = 11.39, tag = $rectangleSegmentA002)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA002) - 90, length = 105.26)
|
||||
@ -383,7 +386,7 @@ profile001 = startProfileAt([205.96, 254.59], sketch002)
|
||||
])`,
|
||||
beforeChamferSnippetEnd: ')',
|
||||
afterChamferSelectSnippet:
|
||||
'sketch002 = startSketchOn(extrude001, seg03)',
|
||||
'sketch002 = startSketchOn(extrude001, face = seg03)',
|
||||
afterRectangle1stClickSnippet:
|
||||
'startProfileAt([205.96, 254.59], sketch002)',
|
||||
afterRectangle2ndClickSnippet: `angledLine(angle=0,length=11.39,tag=$rectangleSegmentA002)
|
||||
@ -416,7 +419,7 @@ chamf = chamfer(
|
||||
getNextAdjacentEdge(seg02)
|
||||
],
|
||||
)
|
||||
sketch002 = startSketchOn(extrude001, seg03)
|
||||
sketch002 = startSketchOn(extrude001, face = seg03)
|
||||
profile001 = startProfileAt([205.96, 254.59], sketch002)
|
||||
|> angledLine(angle = 0, length = 11.39, tag = $rectangleSegmentA002)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA002) - 90, length = 105.26)
|
||||
@ -1046,7 +1049,7 @@ openSketch = startSketchOn(XY)
|
||||
}) => {
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 620, y: 257 }
|
||||
const expectedOutput = `helix001 = helix( axis = X, radius = 5, length = 5, revolutions = 1, angleStart = 360, ccw = false,)`
|
||||
const expectedOutput = `helix001 = helix( axis = X, radius = 5, length = 5, revolutions = 1, angleStart = 270, ccw = false,)`
|
||||
const expectedLine = `axis=X,`
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
@ -1072,6 +1075,23 @@ openSketch = startSketchOn(XY)
|
||||
await expect.poll(() => page.getByText('Axis').count()).toBe(6)
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'angleStart',
|
||||
highlightedHeaderArg: 'angleStart',
|
||||
currentArgValue: '360',
|
||||
headerArguments: {
|
||||
Mode: 'Axis',
|
||||
Axis: 'X',
|
||||
Revolutions: '1',
|
||||
AngleStart: '',
|
||||
Length: '',
|
||||
Radius: '',
|
||||
CounterClockWise: '',
|
||||
},
|
||||
commandName: 'Helix',
|
||||
})
|
||||
await cmdBar.currentArgumentInput.locator('.cm-content').fill('270')
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
@ -1080,7 +1100,7 @@ openSketch = startSketchOn(XY)
|
||||
headerArguments: {
|
||||
Mode: 'Axis',
|
||||
Axis: 'X',
|
||||
AngleStart: '360',
|
||||
AngleStart: '270',
|
||||
Revolutions: '1',
|
||||
Length: '5',
|
||||
Radius: '5',
|
||||
@ -1115,7 +1135,7 @@ openSketch = startSketchOn(XY)
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Axis: 'X',
|
||||
AngleStart: '360',
|
||||
AngleStart: '270',
|
||||
Revolutions: '1',
|
||||
Radius: '5',
|
||||
Length: initialInput,
|
||||
@ -1131,7 +1151,7 @@ openSketch = startSketchOn(XY)
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Axis: 'X',
|
||||
AngleStart: '360',
|
||||
AngleStart: '270',
|
||||
Revolutions: '1',
|
||||
Radius: '5',
|
||||
Length: newInput,
|
||||
@ -2935,9 +2955,9 @@ extrude001 = extrude(sketch001, length = 30)
|
||||
const testPoint = { x: 575, y: 200 }
|
||||
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const shellDeclaration =
|
||||
"shell001 = shell(extrude001, faces = ['end'], thickness = 5)"
|
||||
'shell001 = shell(extrude001, faces = [END], thickness = 5)'
|
||||
const editedShellDeclaration =
|
||||
"shell001 = shell(extrude001, faces = ['end'], thickness = 2)"
|
||||
'shell001 = shell(extrude001, faces = [END], thickness = 2)'
|
||||
|
||||
await test.step(`Look for the grey of the shape`, async () => {
|
||||
await scene.expectPixelColor([127, 127, 127], testPoint, 15)
|
||||
@ -3079,9 +3099,9 @@ extrude001 = extrude(sketch001, length = 40)
|
||||
const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70)
|
||||
const mutatedCode = 'xLine(length = -40, tag = $seg01)'
|
||||
const shellDeclaration =
|
||||
"shell001 = shell(extrude001, faces = ['end', seg01], thickness = 5)"
|
||||
'shell001 = shell(extrude001, faces = [END, seg01], thickness = 5)'
|
||||
const editedShellDeclaration =
|
||||
"shell001 = shell(extrude001, faces = ['end', seg01], thickness = 1)"
|
||||
'shell001 = shell(extrude001, faces = [END, seg01], thickness = 1)'
|
||||
|
||||
await test.step(`Look for the grey of the shape`, async () => {
|
||||
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
|
||||
@ -3188,7 +3208,7 @@ sketch001 = startSketchOn(XZ)
|
||||
|> circle(center = [0, 0], radius = 100)
|
||||
|> extrude(length = 100)
|
||||
|
||||
sketch002 = startSketchOn(sketch001, 'END')
|
||||
sketch002 = startSketchOn(sketch001, face = 'END')
|
||||
|> circle(center = [0, 0], radius = 50)
|
||||
|> extrude(length = 50)
|
||||
`,
|
||||
@ -3197,7 +3217,7 @@ sketch001 = startSketchOn(XZ)
|
||||
|> circle(center = [0, 0], radius = 100)
|
||||
extrude001 = extrude(sketch001, length = 100)
|
||||
|
||||
sketch002 = startSketchOn(extrude001, 'END')
|
||||
sketch002 = startSketchOn(extrude001, face = 'END')
|
||||
|> circle(center = [0, 0], radius = 50)
|
||||
extrude002 = extrude(sketch002, length = 50)
|
||||
`,
|
||||
@ -3224,7 +3244,7 @@ extrude002 = extrude(sketch002, length = 50)
|
||||
const testPoint = { x: 580, y: 320 }
|
||||
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const shellTarget = hasExtrudesInPipe ? 'sketch002' : 'extrude002'
|
||||
const shellDeclaration = `shell001 = shell(${shellTarget}, faces = ['end'], thickness = 5)`
|
||||
const shellDeclaration = `shell001 = shell(${shellTarget}, faces = [END], thickness = 5)`
|
||||
|
||||
await test.step(`Look for the grey of the shape`, async () => {
|
||||
await scene.expectPixelColor([113, 113, 113], testPoint, 15)
|
||||
@ -3299,7 +3319,7 @@ profile001 = startProfileAt([-20, 20], sketch001)
|
||||
`
|
||||
const extrudeCode = `extrude001 = extrude(profile001, length = 40)
|
||||
`
|
||||
const shellCode = `shell001 = shell(extrude001, faces = ['end'], thickness = 5)
|
||||
const shellCode = `shell001 = shell(extrude001, faces = [END], thickness = 5)
|
||||
`
|
||||
const initialCode = sketchCode + extrudeCode + shellCode
|
||||
await context.addInitScript((initialCode) => {
|
||||
@ -3447,7 +3467,7 @@ tag=$rectangleSegmentC001,
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = 200)
|
||||
sketch002 = startSketchOn(extrude001, rectangleSegmentA001)
|
||||
sketch002 = startSketchOn(extrude001, face = rectangleSegmentA001)
|
||||
|> startProfileAt([-66.77, 84.81], %)
|
||||
|> angledLine(angle = 180, length = 27.08, tag = $rectangleSegmentA002)
|
||||
|> angledLine(
|
||||
@ -3527,6 +3547,9 @@ tag=$rectangleSegmentC002,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
if (runningOnWindows()) {
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
}
|
||||
const initialCode = `sketch001 = startSketchOn(XZ)
|
||||
|> startProfileAt([-102.57, 101.72], %)
|
||||
|> angledLine(angle = 0, length = 202.6, tag = $rectangleSegmentA001)
|
||||
@ -3535,7 +3558,7 @@ tag=$rectangleSegmentC002,
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = 50)
|
||||
sketch002 = startSketchOn(extrude001, rectangleSegmentA001)
|
||||
sketch002 = startSketchOn(extrude001, face = rectangleSegmentA001)
|
||||
|> circle(center = [-11.34, 10.0], radius = 8.69)
|
||||
|
||||
`
|
||||
@ -3623,7 +3646,7 @@ sketch002 = startSketchOn(extrude001, rectangleSegmentA001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = 5)
|
||||
sketch003 = startSketchOn(extrude001, 'START')
|
||||
sketch003 = startSketchOn(extrude001, face = 'START')
|
||||
|> circle(
|
||||
center = [-0.69, 0.56],
|
||||
radius = 0.28
|
||||
@ -3818,4 +3841,469 @@ extrude001 = extrude(profile001, length = 100)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const translateExtrudeCases: { variables: boolean }[] = [
|
||||
{
|
||||
variables: false,
|
||||
},
|
||||
{
|
||||
variables: true,
|
||||
},
|
||||
]
|
||||
translateExtrudeCases.map(({ variables }) => {
|
||||
test(`Set translate on extrude through right-click menu (variables: ${variables})`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn(XZ)
|
||||
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||
extrude001 = extrude(profile001, length = 1)
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const midPoint = { x: 500, y: 250 }
|
||||
const moreToTheRightPoint = { x: 800, y: 250 }
|
||||
const bgColor: [number, number, number] = [50, 50, 50]
|
||||
const partColor: [number, number, number] = [150, 150, 150]
|
||||
const tolerance = 50
|
||||
|
||||
await test.step('Confirm extrude exists with default appearance', async () => {
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor(partColor, midPoint, tolerance)
|
||||
await scene.expectPixelColor(bgColor, moreToTheRightPoint, tolerance)
|
||||
})
|
||||
|
||||
await test.step('Set translate through command bar flow', async () => {
|
||||
await toolbar.openPane('feature-tree')
|
||||
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
|
||||
await op.click({ button: 'right' })
|
||||
await page.getByTestId('context-menu-set-translate').click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'x',
|
||||
currentArgValue: '0',
|
||||
headerArguments: {
|
||||
X: '',
|
||||
Y: '',
|
||||
Z: '',
|
||||
},
|
||||
highlightedHeaderArg: 'x',
|
||||
commandName: 'Translate',
|
||||
})
|
||||
await page.keyboard.insertText('3')
|
||||
if (variables) {
|
||||
await cmdBar.createNewVariable()
|
||||
}
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.keyboard.insertText('0.1')
|
||||
if (variables) {
|
||||
await cmdBar.createNewVariable()
|
||||
}
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.keyboard.insertText('0.2')
|
||||
if (variables) {
|
||||
await cmdBar.createNewVariable()
|
||||
}
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
X: '3',
|
||||
Y: '0.1',
|
||||
Z: '0.2',
|
||||
},
|
||||
commandName: 'Translate',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await toolbar.closePane('feature-tree')
|
||||
})
|
||||
|
||||
await test.step('Confirm code and scene have changed', async () => {
|
||||
await toolbar.openPane('code')
|
||||
if (variables) {
|
||||
await editor.expectEditor.toContain(
|
||||
`
|
||||
z001 = 0.2
|
||||
y001 = 0.1
|
||||
x001 = 3
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||
extrude001 = extrude(profile001, length = 1)
|
||||
|> translate(x = x001, y = y001, z = z001)
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
} else {
|
||||
await editor.expectEditor.toContain(
|
||||
`
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||
extrude001 = extrude(profile001, length = 1)
|
||||
|> translate(x = 3, y = 0.1, z = 0.2)
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
}
|
||||
await scene.expectPixelColor(bgColor, midPoint, tolerance)
|
||||
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
|
||||
})
|
||||
|
||||
await test.step('Edit translate', async () => {
|
||||
await toolbar.openPane('feature-tree')
|
||||
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
|
||||
await op.click({ button: 'right' })
|
||||
await page.getByTestId('context-menu-set-translate').click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'z',
|
||||
currentArgValue: variables ? 'z001' : '0.2',
|
||||
headerArguments: {
|
||||
X: '3',
|
||||
Y: '0.1',
|
||||
Z: '0.2',
|
||||
},
|
||||
highlightedHeaderArg: 'z',
|
||||
commandName: 'Translate',
|
||||
})
|
||||
await page.keyboard.insertText('0.3')
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
X: '3',
|
||||
Y: '0.1',
|
||||
Z: '0.3',
|
||||
},
|
||||
commandName: 'Translate',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await toolbar.closePane('feature-tree')
|
||||
await toolbar.openPane('code')
|
||||
await editor.expectEditor.toContain(`z = 0.3`)
|
||||
// Expect almost no change in scene
|
||||
await scene.expectPixelColor(bgColor, midPoint, tolerance)
|
||||
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const rotateExtrudeCases: { variables: boolean }[] = [
|
||||
{
|
||||
variables: false,
|
||||
},
|
||||
{
|
||||
variables: true,
|
||||
},
|
||||
]
|
||||
rotateExtrudeCases.map(({ variables }) => {
|
||||
test(`Set rotate on extrude through right-click menu (variables: ${variables})`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn(XZ)
|
||||
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||
extrude001 = extrude(profile001, length = 1)
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
await test.step('Set rotate through command bar flow', async () => {
|
||||
await toolbar.openPane('feature-tree')
|
||||
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
|
||||
await op.click({ button: 'right' })
|
||||
await page.getByTestId('context-menu-set-rotate').click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'roll',
|
||||
currentArgValue: '0',
|
||||
headerArguments: {
|
||||
Roll: '',
|
||||
Pitch: '',
|
||||
Yaw: '',
|
||||
},
|
||||
highlightedHeaderArg: 'roll',
|
||||
commandName: 'Rotate',
|
||||
})
|
||||
await page.keyboard.insertText('1.1')
|
||||
if (variables) {
|
||||
await cmdBar.createNewVariable()
|
||||
}
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.keyboard.insertText('1.2')
|
||||
if (variables) {
|
||||
await cmdBar.createNewVariable()
|
||||
}
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.keyboard.insertText('1.3')
|
||||
if (variables) {
|
||||
await cmdBar.createNewVariable()
|
||||
}
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Roll: '1.1',
|
||||
Pitch: '1.2',
|
||||
Yaw: '1.3',
|
||||
},
|
||||
commandName: 'Rotate',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await toolbar.closePane('feature-tree')
|
||||
})
|
||||
|
||||
await test.step('Confirm code and scene have changed', async () => {
|
||||
await toolbar.openPane('code')
|
||||
if (variables) {
|
||||
await editor.expectEditor.toContain(
|
||||
`
|
||||
yaw001 = 1.3
|
||||
pitch001 = 1.2
|
||||
roll001 = 1.1
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||
extrude001 = extrude(profile001, length = 1)
|
||||
|> rotate(roll = roll001, pitch = pitch001, yaw = yaw001)
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
} else {
|
||||
await editor.expectEditor.toContain(
|
||||
`
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||
extrude001 = extrude(profile001, length = 1)
|
||||
|> rotate(roll = 1.1, pitch = 1.2, yaw = 1.3)
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
await test.step('Edit rotate', async () => {
|
||||
await toolbar.openPane('feature-tree')
|
||||
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
|
||||
await op.click({ button: 'right' })
|
||||
await page.getByTestId('context-menu-set-rotate').click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'yaw',
|
||||
currentArgValue: variables ? 'yaw001' : '1.3',
|
||||
headerArguments: {
|
||||
Roll: '1.1',
|
||||
Pitch: '1.2',
|
||||
Yaw: '1.3',
|
||||
},
|
||||
highlightedHeaderArg: 'yaw',
|
||||
commandName: 'Rotate',
|
||||
})
|
||||
await page.keyboard.insertText('13')
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Roll: '1.1',
|
||||
Pitch: '1.2',
|
||||
Yaw: '13',
|
||||
},
|
||||
commandName: 'Rotate',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await toolbar.closePane('feature-tree')
|
||||
await toolbar.openPane('code')
|
||||
await editor.expectEditor.toContain(`yaw = 13`)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Set translate and rotate on extrude through selection`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn(XZ)
|
||||
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||
extrude001 = extrude(profile001, length = 1)
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const midPoint = { x: 500, y: 250 }
|
||||
const moreToTheRightPoint = { x: 800, y: 250 }
|
||||
const bgColor: [number, number, number] = [50, 50, 50]
|
||||
const partColor: [number, number, number] = [150, 150, 150]
|
||||
const tolerance = 50
|
||||
const [clickMidPoint] = scene.makeMouseHelpers(midPoint.x, midPoint.y)
|
||||
const [clickMoreToTheRightPoint] = scene.makeMouseHelpers(
|
||||
moreToTheRightPoint.x,
|
||||
moreToTheRightPoint.y
|
||||
)
|
||||
|
||||
await test.step('Confirm extrude exists with default appearance', async () => {
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor(partColor, midPoint, tolerance)
|
||||
await scene.expectPixelColor(bgColor, moreToTheRightPoint, tolerance)
|
||||
})
|
||||
|
||||
await test.step('Set translate through command bar flow', async () => {
|
||||
await cmdBar.openCmdBar()
|
||||
await cmdBar.chooseCommand('Translate')
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
X: '',
|
||||
Y: '',
|
||||
Z: '',
|
||||
},
|
||||
highlightedHeaderArg: 'selection',
|
||||
commandName: 'Translate',
|
||||
})
|
||||
await clickMidPoint()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'x',
|
||||
currentArgValue: '0',
|
||||
headerArguments: {
|
||||
Selection: '1 path',
|
||||
X: '',
|
||||
Y: '',
|
||||
Z: '',
|
||||
},
|
||||
highlightedHeaderArg: 'x',
|
||||
commandName: 'Translate',
|
||||
})
|
||||
await page.keyboard.insertText('2')
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Selection: '1 path',
|
||||
X: '2',
|
||||
Y: '0',
|
||||
Z: '0',
|
||||
},
|
||||
commandName: 'Translate',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step('Confirm code and scene have changed', async () => {
|
||||
await toolbar.openPane('code')
|
||||
await editor.expectEditor.toContain(
|
||||
`
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||
extrude001 = extrude(profile001, length = 1)
|
||||
|> translate(x = 2, y = 0, z = 0)
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
await scene.expectPixelColor(bgColor, midPoint, tolerance)
|
||||
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
|
||||
})
|
||||
|
||||
await test.step('Set rotate through command bar flow', async () => {
|
||||
// clear selection
|
||||
await clickMidPoint()
|
||||
await cmdBar.openCmdBar()
|
||||
await cmdBar.chooseCommand('Rotate')
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Roll: '',
|
||||
Pitch: '',
|
||||
Yaw: '',
|
||||
},
|
||||
highlightedHeaderArg: 'selection',
|
||||
commandName: 'Rotate',
|
||||
})
|
||||
await clickMoreToTheRightPoint()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'roll',
|
||||
currentArgValue: '0',
|
||||
headerArguments: {
|
||||
Selection: '1 path',
|
||||
Roll: '',
|
||||
Pitch: '',
|
||||
Yaw: '',
|
||||
},
|
||||
highlightedHeaderArg: 'roll',
|
||||
commandName: 'Rotate',
|
||||
})
|
||||
await page.keyboard.insertText('0.1')
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.keyboard.insertText('0.2')
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.keyboard.insertText('0.3')
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Selection: '1 path',
|
||||
Roll: '0.1',
|
||||
Pitch: '0.2',
|
||||
Yaw: '0.3',
|
||||
},
|
||||
commandName: 'Rotate',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step('Confirm code has changed', async () => {
|
||||
await toolbar.openPane('code')
|
||||
await editor.expectEditor.toContain(
|
||||
`
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||
extrude001 = extrude(profile001, length = 1)
|
||||
|> translate(x = 2, y = 0, z = 0)
|
||||
|> rotate(roll = 0.1, pitch = 0.2, yaw = 0.3)
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
// No change here since the angles are super small
|
||||
await scene.expectPixelColor(bgColor, midPoint, tolerance)
|
||||
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { DEFAULT_PROJECT_KCL_FILE } from '@src/lib/constants'
|
||||
import fs from 'fs'
|
||||
import fsp from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { DEFAULT_PROJECT_KCL_FILE } from '@src/lib/constants'
|
||||
import fsp from 'fs/promises'
|
||||
|
||||
import type { Paths } from '@e2e/playwright/test-utils'
|
||||
import {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||
import { orRunWhenFullSuiteEnabled } from '@e2e/playwright/test-utils'
|
||||
|
||||
/* eslint-disable jest/no-conditional-expect */
|
||||
|
||||
@ -57,6 +58,7 @@ test.describe('edit with AI example snapshots', () => {
|
||||
`change colour`,
|
||||
{ tag: '@snapshot' },
|
||||
async ({ context, homePage, cmdBar, editor, page, scene }) => {
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
await context.addInitScript((file) => {
|
||||
localStorage.setItem('persistCode', file)
|
||||
}, file)
|
||||
|
@ -1,8 +1,8 @@
|
||||
import path from 'path'
|
||||
import { bracket } from '@e2e/playwright/fixtures/bracket'
|
||||
import type { Page } from '@playwright/test'
|
||||
import { reportRejection } from '@src/lib/trap'
|
||||
import * as fsp from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from '@e2e/playwright/storageStates'
|
||||
import type { TestColor } from '@e2e/playwright/test-utils'
|
||||
@ -304,8 +304,9 @@ extrude001 = extrude(sketch001, length = 50)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.press('ArrowLeft')
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toContainText(`exampleSketch = startSketchOn("XZ")
|
||||
await expect(
|
||||
page.locator('.cm-content')
|
||||
).toContainText(`exampleSketch = startSketchOn("XZ")
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> angledLine(angle = 50, length = 45 )
|
||||
|> yLine(endAbsolute = 0)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import { roundOff, uuidv4 } from '@src/lib/utils'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import type { Page } from '@playwright/test'
|
||||
import { roundOff, uuidv4 } from '@src/lib/utils'
|
||||
|
||||
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
|
||||
import type { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture'
|
||||
@ -161,7 +161,8 @@ sketch001 = startSketchOn(XZ)
|
||||
// click to add segment
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
|
||||
await expect
|
||||
.poll(u.normalisedEditorCode, { timeout: 1000 })
|
||||
.toBe(`@settings(defaultLengthUnit = in)
|
||||
|
||||
|
||||
@ -403,8 +404,9 @@ sketch001 = startProfileAt([12.34, -12.34], sketch002)
|
||||
await u.openKclCodePanel()
|
||||
|
||||
// expect the code to have changed
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch001 = startSketchOn(XZ)
|
||||
await expect(
|
||||
page.locator('.cm-content')
|
||||
).toHaveText(`sketch001 = startSketchOn(XZ)
|
||||
|> startProfileAt([6.44, -12.07], %)
|
||||
|> line(end = [14.72, 1.97])
|
||||
|> tangentialArc(endAbsolute = [26.92, -3.32])
|
||||
@ -1077,7 +1079,7 @@ profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
|
||||
// The click should generate a new sketch starting on the end face of extrude001
|
||||
// signified by the implicit 'END' tag for that solid.
|
||||
await page.waitForTimeout(800)
|
||||
await page.getByText(`END')`).click()
|
||||
await page.getByText(`END)`).click()
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type(' |>', { delay: 100 })
|
||||
@ -1099,7 +1101,7 @@ profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = 5)
|
||||
sketch002 = startSketchOn(extrude001, 'END')
|
||||
sketch002 = startSketchOn(extrude001, face = END)
|
||||
|>
|
||||
`.replace(/\s/g, '')
|
||||
)
|
||||
@ -1363,73 +1365,6 @@ profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Sketch mode should be toleratant to syntax errors', () => {
|
||||
test(
|
||||
'adding a syntax error, recovers after fixing',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ page, homePage, context, scene, editor, toolbar }) => {
|
||||
const file = await fs.readFile(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'../../',
|
||||
'./rust/kcl-lib/e2e/executor/inputs/e2e-can-sketch-on-chamfer.kcl'
|
||||
),
|
||||
'utf-8'
|
||||
)
|
||||
await context.addInitScript((file) => {
|
||||
localStorage.setItem('persistCode', file)
|
||||
}, file)
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
const [objClick] = scene.makeMouseHelpers(600, 250)
|
||||
const arrowHeadLocation = { x: 706, y: 129 } as const
|
||||
const arrowHeadWhite = TEST_COLORS.WHITE
|
||||
const backgroundGray: [number, number, number] = [28, 28, 28]
|
||||
const verifyArrowHeadColor = async (c: [number, number, number]) =>
|
||||
scene.expectPixelColor(c, arrowHeadLocation, 15)
|
||||
|
||||
await test.step('check chamfer selection changes cursor positon', async () => {
|
||||
await expect(async () => {
|
||||
// sometimes initial click doesn't register
|
||||
await objClick()
|
||||
await editor.expectActiveLinesToBe([
|
||||
'|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]',
|
||||
])
|
||||
}).toPass({ timeout: 15_000, intervals: [500] })
|
||||
})
|
||||
|
||||
await test.step('enter sketch and sanity check segments have been drawn', async () => {
|
||||
await toolbar.editSketch()
|
||||
// this checks sketch segments have been drawn
|
||||
await verifyArrowHeadColor(arrowHeadWhite)
|
||||
})
|
||||
|
||||
await test.step('Make typo and check the segments have Disappeared and there is a syntax error', async () => {
|
||||
await editor.replaceCode('line(endAbsolute = [pro', 'badBadBadFn([pro')
|
||||
await editor.expectState({
|
||||
activeLines: [],
|
||||
diagnostics: ['memoryitemkey`badBadBadFn`isnotdefined'],
|
||||
highlightedCode: '',
|
||||
})
|
||||
// this checks sketch segments have failed to be drawn
|
||||
await verifyArrowHeadColor(backgroundGray)
|
||||
})
|
||||
|
||||
await test.step('', async () => {
|
||||
await editor.replaceCode('badBadBadFn([pro', 'line(endAbsolute = [pro')
|
||||
await editor.expectState({
|
||||
activeLines: [],
|
||||
diagnostics: [],
|
||||
highlightedCode: '',
|
||||
})
|
||||
// this checks sketch segments have been drawn
|
||||
await verifyArrowHeadColor(arrowHeadWhite)
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test.describe(`Sketching with offset planes`, () => {
|
||||
test(`Can select an offset plane to sketch on`, async ({
|
||||
context,
|
||||
@ -1623,6 +1558,7 @@ profile002 = startProfileAt([117.2, 56.08], sketch001)
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`@settings(defaultLengthUnit = in)
|
||||
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile002 = startProfileAt([40.68, 87.67], sketch001)
|
||||
|> xLine(length = 239.17)
|
||||
@ -2634,7 +2570,7 @@ profile001 = startProfileAt([6.71, -3.66], sketch001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(profile001, length = 20)
|
||||
sketch002 = startSketchOn(extrude001, seg01)
|
||||
sketch002 = startSketchOn(extrude001, face = seg01)
|
||||
profile002 = startProfileAt([0.75, 13.46], sketch002)
|
||||
|> line(end = [4.52, 3.79])
|
||||
|> line(end = [5.98, -2.81])
|
||||
@ -3034,4 +2970,412 @@ test.describe('Redirecting to home page and back to the original file should cle
|
||||
await homePage.openProject('testDefault')
|
||||
await expect(page.getByText('323.49')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('Straight line snapping to previous tangent', async ({
|
||||
page,
|
||||
homePage,
|
||||
toolbar,
|
||||
scene,
|
||||
cmdBar,
|
||||
context,
|
||||
editor,
|
||||
}) => {
|
||||
await context.addInitScript(() => {
|
||||
localStorage.setItem('persistCode', `@settings(defaultLengthUnit = mm)`)
|
||||
})
|
||||
|
||||
const viewportSize = { width: 1200, height: 900 }
|
||||
await page.setBodyDimensions(viewportSize)
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
// wait until scene is ready to be interacted with
|
||||
await scene.connectionEstablished()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
await page.getByRole('button', { name: 'Start Sketch' }).click()
|
||||
|
||||
// select an axis plane
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
// Needed as we don't yet have a way to get a signal from the engine that the camera has animated to the sketch plane
|
||||
await page.waitForTimeout(3000)
|
||||
|
||||
const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 }
|
||||
const { click00r } = getMovementUtils({ center, page })
|
||||
|
||||
// Draw line
|
||||
await click00r(0, 0)
|
||||
await click00r(200, -200)
|
||||
|
||||
// Draw arc
|
||||
await toolbar.tangentialArcBtn.click()
|
||||
await click00r(0, 0)
|
||||
await click00r(100, 100)
|
||||
|
||||
// Switch back to line
|
||||
await toolbar.selectLine()
|
||||
await click00r(0, 0)
|
||||
await click00r(-100, 100)
|
||||
|
||||
// Draw a 3 point arc
|
||||
await toolbar.selectThreePointArc()
|
||||
await click00r(0, 0)
|
||||
await click00r(0, 100)
|
||||
await click00r(100, 0)
|
||||
|
||||
// draw a line to opposite tangnet direction of previous arc
|
||||
await toolbar.selectLine()
|
||||
await click00r(0, 0)
|
||||
await click00r(-200, 200)
|
||||
|
||||
await editor.expectEditor.toContain(
|
||||
`@settings(defaultLengthUnit = mm)
|
||||
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile001 = startProfileAt([0, 0], sketch001)
|
||||
|> line(end = [191.39, 191.39])
|
||||
|> tangentialArc(endAbsolute = [287.08, 95.69], tag = $seg01)
|
||||
|> angledLine(angle = tangentToEnd(seg01), length = 135.34)
|
||||
|> arcTo({
|
||||
interior = [191.39, -95.69],
|
||||
end = [287.08, -95.69]
|
||||
}, %, $seg02)
|
||||
|> angledLine(angle = tangentToEnd(seg02) + turns::HALF_TURN, length = 270.67)
|
||||
`.replaceAll('\n', '')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('manual edits during sketch mode', () => {
|
||||
test('Can edit sketch through feature tree with variable modifications', async ({
|
||||
page,
|
||||
context,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `myVar1 = 5
|
||||
myVar2 = 6
|
||||
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile001 = startProfileAt([106.68, 89.77], sketch001)
|
||||
|> line(end = [132.34, 157.8])
|
||||
|> line(end = [67.65, -460.55], tag = $seg01)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(profile001, length = 500)
|
||||
sketch002 = startSketchOn(extrude001, face = seg01)
|
||||
profile002 = startProfileAt([83.39, 329.15], sketch002)
|
||||
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|
||||
|> angledLine(length = 156.54, angle = -28)
|
||||
|> angledLine(
|
||||
angle = -151,
|
||||
length = 116.27,
|
||||
)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
profile003 = startProfileAt([-201.08, 254.17], sketch002)
|
||||
|> line(end = [103.55, 33.32])
|
||||
|> line(end = [48.8, -153.54])`
|
||||
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await scene.connectionEstablished()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
await test.step('Open feature tree and edit second sketch', async () => {
|
||||
await toolbar.openFeatureTreePane()
|
||||
const sketchButton = await toolbar.getFeatureTreeOperation('Sketch', 1)
|
||||
await sketchButton.dblclick()
|
||||
await page.waitForTimeout(700) // Wait for engine animation
|
||||
})
|
||||
|
||||
await test.step('Add new variable and wait for re-execution', async () => {
|
||||
await page.waitForTimeout(500) // wait for deferred execution
|
||||
await editor.replaceCode('myVar2 = 6', 'myVar2 = 6\nmyVar3 = 7')
|
||||
await page.waitForTimeout(2000) // wait for deferred execution
|
||||
})
|
||||
|
||||
const handle1Location = { x: 843, y: 235 }
|
||||
await test.step('Edit sketch by dragging handle', async () => {
|
||||
await page.waitForTimeout(500)
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
await editor.expectEditor.toContain('length = 156.54, angle = -28')
|
||||
await page.mouse.move(handle1Location.x, handle1Location.y)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(
|
||||
handle1Location.x + 50,
|
||||
handle1Location.y + 50,
|
||||
{
|
||||
steps: 5,
|
||||
}
|
||||
)
|
||||
await page.mouse.up()
|
||||
await editor.expectEditor.toContain('length = 231.59, angle = -34')
|
||||
return true
|
||||
},
|
||||
{ timeout: 10_000 }
|
||||
)
|
||||
.toBeTruthy()
|
||||
})
|
||||
|
||||
await test.step('Delete variables and wait for re-execution', async () => {
|
||||
await page.waitForTimeout(500)
|
||||
await editor.replaceCode('myVar3 = 7', '')
|
||||
await page.waitForTimeout(50)
|
||||
await editor.replaceCode('myVar2 = 6', '')
|
||||
await page.waitForTimeout(2000) // Wait for deferred execution
|
||||
})
|
||||
|
||||
const handle2Location = { x: 872, y: 273 }
|
||||
await test.step('Edit sketch again', async () => {
|
||||
await editor.expectEditor.toContain('length = 231.59, angle = -34')
|
||||
await page.waitForTimeout(500)
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
await page.mouse.move(handle2Location.x, handle2Location.y)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(handle2Location.x, handle2Location.y - 50, {
|
||||
steps: 5,
|
||||
})
|
||||
await page.mouse.up()
|
||||
await editor.expectEditor.toContain('length = 167.36, angle = -14')
|
||||
return true
|
||||
},
|
||||
{ timeout: 10_000 }
|
||||
)
|
||||
.toBeTruthy()
|
||||
})
|
||||
|
||||
await test.step('add whole other sketch before current sketch', async () => {
|
||||
await page.waitForTimeout(500)
|
||||
await editor.replaceCode(
|
||||
`myVar1 = 5`,
|
||||
`myVar1 = 5
|
||||
sketch003 = startSketchOn(XY)
|
||||
profile004 = circle(sketch003, center = [143.91, 136.89], radius = 71.63)`
|
||||
)
|
||||
await page.waitForTimeout(2000) // Wait for deferred execution
|
||||
})
|
||||
|
||||
const handle3Location = { x: 844, y: 212 }
|
||||
await test.step('edit sketch again', async () => {
|
||||
await page.waitForTimeout(500) // Wait for deferred execution
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
await editor.expectEditor.toContain('length = 167.36, angle = -14')
|
||||
await page.mouse.move(handle3Location.x, handle3Location.y)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(handle3Location.x, handle3Location.y + 110, {
|
||||
steps: 5,
|
||||
})
|
||||
await page.mouse.up()
|
||||
await editor.expectEditor.toContain('length = 219.2, angle = -56')
|
||||
return true
|
||||
},
|
||||
{ timeout: 10_000 }
|
||||
)
|
||||
.toBeTruthy()
|
||||
})
|
||||
|
||||
// exit sketch and assert whole code
|
||||
await test.step('Exit sketch and assert code', async () => {
|
||||
await toolbar.exitSketch()
|
||||
await editor.expectEditor.toContain(
|
||||
`myVar1 = 5
|
||||
sketch003 = startSketchOn(XY)
|
||||
profile004 = circle(sketch003, center = [143.91, 136.89], radius = 71.63)
|
||||
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile001 = startProfileAt([106.68, 89.77], sketch001)
|
||||
|> line(end = [132.34, 157.8])
|
||||
|> line(end = [67.65, -460.55], tag = $seg01)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(profile001, length = 500)
|
||||
sketch002 = startSketchOn(extrude001, face = seg01)
|
||||
profile002 = startProfileAt([83.39, 329.15], sketch002)
|
||||
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|
||||
|> angledLine(length = 219.2, angle = -56)
|
||||
|> angledLine(angle = -151, length = 116.27)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
profile003 = startProfileAt([-201.08, 254.17], sketch002)
|
||||
|> line(end = [103.55, 33.32])
|
||||
|> line(end = [48.8, -153.54])
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
await editor.expectState({
|
||||
activeLines: [],
|
||||
diagnostics: [],
|
||||
highlightedCode: '',
|
||||
})
|
||||
})
|
||||
})
|
||||
test('Will exit out of sketch mode for some incompatible edits', async ({
|
||||
page,
|
||||
context,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `myVar1 = 5
|
||||
myVar2 = 6
|
||||
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile001 = startProfileAt([106.68, 89.77], sketch001)
|
||||
|> line(end = [132.34, 157.8])
|
||||
|> line(end = [67.65, -460.55], tag = $seg01)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(profile001, length = 500)
|
||||
sketch002 = startSketchOn(extrude001, face = seg01)
|
||||
profile002 = startProfileAt([83.39, 329.15], sketch002)
|
||||
|> angledLine(angle = 0, length = 119.61, tag = $rectangleSegmentA001)
|
||||
|> angledLine(length = 156.54, angle = -28)
|
||||
|> angledLine(
|
||||
angle = -151,
|
||||
length = 116.27,
|
||||
)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
profile003 = startProfileAt([-201.08, 254.17], sketch002)
|
||||
|> line(end = [103.55, 33.32])
|
||||
|> line(end = [48.8, -153.54])`
|
||||
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await scene.connectionEstablished()
|
||||
await scene.settled(cmdBar)
|
||||
const expectSketchOriginToBeDrawn = async () => {
|
||||
await scene.expectPixelColor(TEST_COLORS.WHITE, { x: 672, y: 193 }, 15)
|
||||
}
|
||||
|
||||
await test.step('Open feature tree and edit second sketch', async () => {
|
||||
await toolbar.openFeatureTreePane()
|
||||
const sketchButton = await toolbar.getFeatureTreeOperation('Sketch', 1)
|
||||
await sketchButton.dblclick()
|
||||
await page.waitForTimeout(700) // Wait for engine animation
|
||||
await expectSketchOriginToBeDrawn()
|
||||
})
|
||||
|
||||
await test.step('rename variable of current sketch, sketch002 to changeSketchNamePartWayThrough', async () => {
|
||||
await editor.replaceCode('sketch002', 'changeSketchNamePartWayThrough')
|
||||
await page.waitForTimeout(100)
|
||||
// three times to rename the declaration and it's use
|
||||
await editor.replaceCode('sketch002', 'changeSketchNamePartWayThrough')
|
||||
await page.waitForTimeout(100)
|
||||
await editor.replaceCode('sketch002', 'changeSketchNamePartWayThrough')
|
||||
await expect(
|
||||
page.getByText('Unable to maintain sketch mode')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
test(
|
||||
'adding a syntax error, recovers after fixing',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ page, homePage, context, scene, editor, toolbar, cmdBar }) => {
|
||||
const file = await fs.readFile(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
'../../',
|
||||
'./rust/kcl-lib/e2e/executor/inputs/e2e-can-sketch-on-chamfer.kcl'
|
||||
),
|
||||
'utf-8'
|
||||
)
|
||||
await context.addInitScript((file) => {
|
||||
localStorage.setItem('persistCode', file)
|
||||
}, file)
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
const [objClick] = scene.makeMouseHelpers(600, 250)
|
||||
const arrowHeadLocation = { x: 706, y: 129 } as const
|
||||
const arrowHeadWhite = TEST_COLORS.WHITE
|
||||
const backgroundGray: [number, number, number] = [28, 28, 28]
|
||||
const verifyArrowHeadColor = async (c: [number, number, number]) =>
|
||||
scene.expectPixelColor(c, arrowHeadLocation, 15)
|
||||
|
||||
// wait for scene to load
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
await test.step('check chamfer selection changes cursor positon', async () => {
|
||||
await expect(async () => {
|
||||
// sometimes initial click doesn't register
|
||||
await objClick()
|
||||
await editor.expectActiveLinesToBe([
|
||||
'|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]',
|
||||
])
|
||||
}).toPass({ timeout: 15_000, intervals: [500] })
|
||||
})
|
||||
|
||||
await test.step('enter sketch and sanity check segments have been drawn', async () => {
|
||||
await toolbar.editSketch()
|
||||
// this checks sketch segments have been drawn
|
||||
await verifyArrowHeadColor(arrowHeadWhite)
|
||||
})
|
||||
|
||||
await test.step('Make typo and check the segments have Disappeared and there is a syntax error', async () => {
|
||||
await editor.replaceCode(
|
||||
'line(endAbsolute = [pro',
|
||||
'badBadBadFn(endAbsolute = [pro'
|
||||
)
|
||||
await editor.expectState({
|
||||
activeLines: [],
|
||||
diagnostics: ['memoryitemkey`badBadBadFn`isnotdefined'],
|
||||
highlightedCode: '',
|
||||
})
|
||||
await expect(
|
||||
page.getByText(
|
||||
"Error in kcl script, sketch cannot be drawn until it's fixed"
|
||||
)
|
||||
).toBeVisible()
|
||||
// this checks sketch segments have failed to be drawn
|
||||
await verifyArrowHeadColor(backgroundGray)
|
||||
})
|
||||
|
||||
await test.step('', async () => {
|
||||
await editor.replaceCode(
|
||||
'badBadBadFn(endAbsolute = [pro',
|
||||
'line(endAbsolute = [pro'
|
||||
)
|
||||
await editor.expectState({
|
||||
activeLines: [],
|
||||
diagnostics: [],
|
||||
highlightedCode: '',
|
||||
})
|
||||
// this checks sketch segments have been drawn
|
||||
await verifyArrowHeadColor(arrowHeadWhite)
|
||||
})
|
||||
|
||||
await test.step('make a change to the code and expect pixel color to change', async () => {
|
||||
// defends against a regression where sketch would duplicate in the scene
|
||||
// https://github.com/KittyCAD/modeling-app/issues/6345
|
||||
await editor.replaceCode(
|
||||
'startProfileAt([75.8, 317.2',
|
||||
'startProfileAt([75.8, 217.2'
|
||||
)
|
||||
// expect not white anymore
|
||||
await scene.expectPixelColorNotToBe(
|
||||
TEST_COLORS.WHITE,
|
||||
arrowHeadLocation,
|
||||
15
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { spawn } from 'child_process'
|
||||
import path from 'path'
|
||||
import type { Models } from '@kittycad/lib'
|
||||
import { KCL_DEFAULT_LENGTH } from '@src/lib/constants'
|
||||
import { spawn } from 'child_process'
|
||||
import fsp from 'fs/promises'
|
||||
import JSZip from 'jszip'
|
||||
import path from 'path'
|
||||
|
||||
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
|
||||
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
|
||||
@ -13,6 +13,8 @@ import type { Paths } from '@e2e/playwright/test-utils'
|
||||
import {
|
||||
doExport,
|
||||
getUtils,
|
||||
headerMasks,
|
||||
networkingMasks,
|
||||
orRunWhenFullSuiteEnabled,
|
||||
settingsToToml,
|
||||
} from '@e2e/playwright/test-utils'
|
||||
@ -77,11 +79,7 @@ part001 = startSketchOn(-XZ)
|
||||
)
|
||||
|> xLine(endAbsolute = totalLen, tag = $seg03)
|
||||
|> yLine(length = -armThick, tag = $seg01)
|
||||
|> angledLineThatIntersects({
|
||||
angle = turns::HALF_TURN,
|
||||
offset = -armThick,
|
||||
intersectTag = seg04
|
||||
}, %)
|
||||
|> angledLineThatIntersects(angle = turns::HALF_TURN, offset = -armThick, intersectTag = seg04)
|
||||
|> angledLine(angle = segAng(seg04, %) + 180, endAbsoluteY = turns::ZERO)
|
||||
|> angledLine(
|
||||
angle = -bottomAng,
|
||||
@ -90,11 +88,7 @@ part001 = startSketchOn(-XZ)
|
||||
)
|
||||
|> xLine(length = endAbsolute = segEndX(seg03) + 0)
|
||||
|> yLine(length = -segLen(seg01, %))
|
||||
|> angledLineThatIntersects({
|
||||
angle = turns::HALF_TURN,
|
||||
offset = -armThick,
|
||||
intersectTag = seg02
|
||||
}, %)
|
||||
|> angledLineThatIntersects(angle = turns::HALF_TURN, offset = -armThick, intersectTag = seg02)
|
||||
|> angledLine(angle = segAng(seg02, %) + 180, endAbsoluteY = -baseHeight)
|
||||
|> xLine(endAbsolute = turns::ZERO)
|
||||
|> close()
|
||||
@ -374,7 +368,7 @@ const extrudeDefaultPlane = async (
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
await u.openKclCodePanel()
|
||||
}
|
||||
@ -383,8 +377,7 @@ test.describe(
|
||||
'extrude on default planes should be stable',
|
||||
{ tag: '@snapshot' },
|
||||
() => {
|
||||
// FIXME: Skip on macos its being weird.
|
||||
test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
|
||||
test('XY', async ({ page, context, cmdBar, scene }) => {
|
||||
await extrudeDefaultPlane(context, page, cmdBar, scene, 'XY')
|
||||
@ -416,6 +409,7 @@ test(
|
||||
'Draft segments should look right',
|
||||
{ tag: '@snapshot' },
|
||||
async ({ page, scene, toolbar }) => {
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
@ -462,7 +456,7 @@ test(
|
||||
await page.waitForTimeout(500)
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
|
||||
const lineEndClick = () =>
|
||||
@ -491,7 +485,7 @@ test(
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
await endOfTangentClk()
|
||||
|
||||
@ -501,7 +495,7 @@ test(
|
||||
await threePointArcMidPointMv()
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
await threePointArcMidPointClk()
|
||||
await page.waitForTimeout(100)
|
||||
@ -510,7 +504,7 @@ test(
|
||||
await page.waitForTimeout(500)
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
|
||||
await threePointArcEndPointClk()
|
||||
@ -530,7 +524,7 @@ test(
|
||||
await page.waitForTimeout(500)
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
await arcEndClk()
|
||||
}
|
||||
@ -540,8 +534,7 @@ test(
|
||||
'Draft rectangles should look right',
|
||||
{ tag: '@snapshot' },
|
||||
async ({ page, context, cmdBar, scene }) => {
|
||||
// FIXME: Skip on macos its being weird.
|
||||
test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
@ -580,7 +573,7 @@ test(
|
||||
// Ensure the draft rectangle looks the same as it usually does
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
}
|
||||
)
|
||||
@ -623,7 +616,7 @@ test(
|
||||
// Ensure the draft rectangle looks the same as it usually does
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`sketch001 = startSketchOn(XZ)profile001 = circle(sketch001, center = [366.89, -62.01], radius = 1)`
|
||||
@ -635,8 +628,7 @@ test.describe(
|
||||
'Client side scene scale should match engine scale',
|
||||
{ tag: '@snapshot' },
|
||||
() => {
|
||||
// FIXME: Skip on macos its being weird.
|
||||
test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
|
||||
test('Inch scale', async ({ page, cmdBar, scene }) => {
|
||||
const u = await getUtils(page)
|
||||
@ -696,7 +688,7 @@ test.describe(
|
||||
// screen shot should show the sketch
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
|
||||
await u.doAndWaitForImageDiff(
|
||||
@ -709,7 +701,7 @@ test.describe(
|
||||
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
})
|
||||
|
||||
@ -789,6 +781,7 @@ test.describe(
|
||||
// screen shot should show the sketch
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
|
||||
// exit sketch
|
||||
@ -802,6 +795,7 @@ test.describe(
|
||||
// second screen shot should look almost identical, i.e. scale should be the same.
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -811,8 +805,7 @@ test(
|
||||
'Sketch on face with none z-up',
|
||||
{ tag: '@snapshot' },
|
||||
async ({ page, context, cmdBar, scene }) => {
|
||||
// FIXME: Skip on macos its being weird.
|
||||
test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async (KCL_DEFAULT_LENGTH) => {
|
||||
@ -824,7 +817,7 @@ test(
|
||||
|> line(end = [11.91, -10.42])
|
||||
|> close()
|
||||
|> extrude(length = ${KCL_DEFAULT_LENGTH})
|
||||
part002 = startSketchOn(part001, seg01)
|
||||
part002 = startSketchOn(part001, face = seg01)
|
||||
|> startProfileAt([8, 8], %)
|
||||
|> line(end = [4.68, 3.05])
|
||||
|> line(end = [0, -7.79])
|
||||
@ -863,7 +856,7 @@ part002 = startSketchOn(part001, seg01)
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
}
|
||||
)
|
||||
@ -872,8 +865,7 @@ test(
|
||||
'Zoom to fit on load - solid 2d',
|
||||
{ tag: '@snapshot' },
|
||||
async ({ page, context, cmdBar, scene }) => {
|
||||
// FIXME: Skip on macos its being weird.
|
||||
test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
@ -902,7 +894,7 @@ test(
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
}
|
||||
)
|
||||
@ -911,8 +903,7 @@ test(
|
||||
'Zoom to fit on load - solid 3d',
|
||||
{ tag: '@snapshot' },
|
||||
async ({ page, context, cmdBar, scene }) => {
|
||||
// FIXME: Skip on macos its being weird.
|
||||
test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
@ -942,7 +933,7 @@ test(
|
||||
|
||||
await expect(page).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
}
|
||||
)
|
||||
@ -953,13 +944,9 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
||||
cmdBar,
|
||||
scene,
|
||||
}) => {
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
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('/')
|
||||
@ -1012,18 +999,14 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
||||
|
||||
await expect(stream).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask,
|
||||
mask: [...headerMasks(page), ...networkingMasks(page)],
|
||||
})
|
||||
})
|
||||
|
||||
test('Grid turned off', async ({ page, cmdBar, scene }) => {
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
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('/')
|
||||
@ -1038,11 +1021,12 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
||||
|
||||
await expect(stream).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask,
|
||||
mask: [...headerMasks(page), ...networkingMasks(page)],
|
||||
})
|
||||
})
|
||||
|
||||
test('Grid turned on', async ({ page, context, cmdBar, scene }) => {
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
await context.addInitScript(
|
||||
async ({ settingsKey, settings }) => {
|
||||
localStorage.setItem(settingsKey, settings)
|
||||
@ -1063,11 +1047,6 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
||||
|
||||
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('/')
|
||||
@ -1082,7 +1061,7 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
||||
|
||||
await expect(stream).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask,
|
||||
mask: [...headerMasks(page), ...networkingMasks(page)],
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1151,11 +1130,13 @@ test('theme persists', async ({ page, context }) => {
|
||||
|
||||
await expect(page, 'expect screenshot to have light theme').toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('code color goober', { tag: '@snapshot' }, () => {
|
||||
test('code color goober', async ({ page, context, scene, cmdBar }) => {
|
||||
test.fixme(orRunWhenFullSuiteEnabled())
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
@ -1166,9 +1147,9 @@ test.describe('code color goober', { tag: '@snapshot' }, () => {
|
||||
sweepPath = startSketchOn(XZ)
|
||||
|> startProfileAt([0.05, 0.05], %)
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc({ offset = 90, radius = 5 }, %)
|
||||
|> tangentialArc(angle = 90, radius = 5)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc({ offset = -90, radius = 5 }, %)
|
||||
|> tangentialArc(angle = -90, radius = 5)
|
||||
|> line(end = [0, 7])
|
||||
|
||||
sweepSketch = startSketchOn(XY)
|
||||
@ -1195,7 +1176,7 @@ sweepSketch = startSketchOn(XY)
|
||||
|
||||
await expect(page, 'expect small color widget').toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
})
|
||||
|
||||
@ -1215,9 +1196,9 @@ sweepSketch = startSketchOn(XY)
|
||||
sweepPath = startSketchOn(XZ)
|
||||
|> startProfileAt([0.05, 0.05], %)
|
||||
|> line(end = [0, 7])
|
||||
|> tangentialArc({ offset = 90, radius = 5 }, %)
|
||||
|> tangentialArc(angle = 90, radius = 5)
|
||||
|> line(end = [-3, 0])
|
||||
|> tangentialArc({ offset = -90, radius = 5 }, %)
|
||||
|> tangentialArc(angle = -90, radius = 5)
|
||||
|> line(end = [0, 7])
|
||||
|
||||
sweepSketch = startSketchOn(XY)
|
||||
@ -1252,7 +1233,7 @@ sweepSketch = startSketchOn(XY)
|
||||
'expect small color widget to have window open'
|
||||
).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
mask: networkingMasks(page),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 42 KiB |