Compare commits
100 Commits
v0.29.0
...
jtran/offs
Author | SHA1 | Date | |
---|---|---|---|
39e838a015 | |||
96652a0c48 | |||
04e586d07b | |||
fe5f574a77 | |||
e787495ad0 | |||
8bb9be7a5e | |||
00892464e8 | |||
05ed2a3367 | |||
10cc5bce59 | |||
a32f150fc1 | |||
ac60082e67 | |||
d44dc1b21a | |||
813962ea4c | |||
738443a6ab | |||
4b6bbbe2c5 | |||
6ff8addc8b | |||
da05c38b9e | |||
191b9b71fd | |||
05163fdded | |||
7ed26e21c6 | |||
c668d40efc | |||
f38c6b90b7 | |||
7bc8bae0ec | |||
3804aca27e | |||
b127680f2f | |||
b7de8e60cf | |||
058fccb5e1 | |||
00e97257ae | |||
aeb656d176 | |||
ac49ebd6e0 | |||
b40f03ad25 | |||
a8ad86e645 | |||
87f50cd5e9 | |||
0400e6228e | |||
26f150fd6c | |||
3049f405f5 | |||
53d40301dc | |||
671c01e36f | |||
e80151979b | |||
668e2afb99 | |||
548c664db0 | |||
d3a3f4410c | |||
22eb343171 | |||
f2cfa4d5cf | |||
3f1f40eeba | |||
ff2d161606 | |||
210c78029d | |||
e27840219b | |||
c943a3f192 | |||
6aa588f09f | |||
59a6333aad | |||
403f1507ae | |||
eac7b83504 | |||
667500d1b9 | |||
b15aac9f48 | |||
54153aa646 | |||
943cf21d34 | |||
5a6728c45a | |||
ff2103d493 | |||
2dfa8f2176 | |||
29ed330326 | |||
ca2cc825a6 | |||
83fe1b7ce0 | |||
157b76cc78 | |||
cf957d880e | |||
dfc3d19677 | |||
dd370a9365 | |||
2274d6459c | |||
32ce857119 | |||
88b51da417 | |||
30d365aeb3 | |||
7af62399ac | |||
441d957228 | |||
9e57034873 | |||
eb96d6539c | |||
513c76ecc8 | |||
51d9449280 | |||
6366bc4766 | |||
7a21918223 | |||
8072f1db63 | |||
18e1855fa9 | |||
7be53c7d4a | |||
2bf20988ef | |||
1495cc6d18 | |||
f876e6ca3c | |||
60a0c811ab | |||
cab0c1e6a1 | |||
417d720b22 | |||
77293952c0 | |||
ea3d604b73 | |||
023a659491 | |||
dd3a2b14f9 | |||
424b409cc1 | |||
82a58e69c2 | |||
776b420031 | |||
1087d4223b | |||
089d6df889 | |||
efb067af58 | |||
2aa27eab01 | |||
9c47ac5b57 |
29
.github/workflows/build-apps.yml
vendored
@ -165,7 +165,6 @@ jobs:
|
||||
- name: Build the app (release)
|
||||
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
|
||||
env:
|
||||
PUBLISH_FOR_PULL_REQUEST: true
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
@ -173,7 +172,6 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||
run: yarn electron-builder --config --publish always
|
||||
|
||||
@ -229,7 +227,6 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
CSC_FOR_PULL_REQUEST: true
|
||||
WINDOWS_CERTIFICATE_THUMBPRINT: ${{ secrets.WINDOWS_CERTIFICATE_THUMBPRINT }}
|
||||
run: yarn electron-builder --config --publish always
|
||||
|
||||
@ -362,6 +359,17 @@ jobs:
|
||||
- name: List artifacts
|
||||
run: "ls -R out"
|
||||
|
||||
- name: Set more complete nightly release notes
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
run: |
|
||||
# Note: preferred going this way instead of a full clone in the checkout step,
|
||||
# see https://github.com/actions/checkout/issues/1471
|
||||
git fetch --prune --unshallow --tags
|
||||
export TAG="nightly-${VERSION}"
|
||||
export PREVIOUS_TAG=$(git describe --tags --match="nightly-v[0-9]*" --abbrev=0)
|
||||
export NOTES=$(./scripts/get-nightly-changelog.sh)
|
||||
yarn files:set-notes
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
uses: 'google-github-actions/auth@v2.1.7'
|
||||
@ -382,3 +390,18 @@ jobs:
|
||||
glob: '*'
|
||||
parent: false
|
||||
destination: 'dl.kittycad.io/releases/modeling-app/nightly'
|
||||
|
||||
- name: Invalidate bucket cache on latest*.yml and last_download.json files
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
run: yarn files:invalidate-bucket:nightly
|
||||
|
||||
- name: Tag nightly commit
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { VERSION } = process.env
|
||||
const { owner, repo } = context.repo
|
||||
const { sha } = context
|
||||
const ref = `refs/tags/nightly-${VERSION}`
|
||||
github.rest.git.createRef({ owner, repo, sha, ref })
|
||||
|
22
.github/workflows/cargo-test.yml
vendored
@ -2,28 +2,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'src/wasm-lib/**.rs'
|
||||
- 'src/wasm-lib/**.hbs'
|
||||
- 'src/wasm-lib/**.gen'
|
||||
- 'src/wasm-lib/**.snap'
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- 'src/wasm-lib/**.kcl'
|
||||
- .github/workflows/cargo-test.yml
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/wasm-lib/**.rs'
|
||||
- 'src/wasm-lib/**.hbs'
|
||||
- 'src/wasm-lib/**.gen'
|
||||
- 'src/wasm-lib/**.snap'
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- 'src/wasm-lib/**.kcl'
|
||||
- .github/workflows/cargo-test.yml
|
||||
workflow_dispatch:
|
||||
permissions: read-all
|
||||
concurrency:
|
||||
@ -71,7 +51,7 @@ jobs:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
RUST_MIN_STACK: 10485760000
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{secrets.CODECOV_TOKEN}}
|
||||
fail_ci_if_error: true
|
||||
|
6
.github/workflows/publish-apps-release.yml
vendored
@ -126,11 +126,7 @@ jobs:
|
||||
destination: 'dl.kittycad.io/releases/modeling-app'
|
||||
|
||||
- name: Invalidate bucket cache on latest*.yml and last_download.json files
|
||||
run: |
|
||||
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/last_download.json" --async
|
||||
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-linux-arm64.yml" --async
|
||||
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest-mac.yml" --async
|
||||
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="/releases/modeling-app/latest.yml" --async
|
||||
run: yarn files:invalidate-bucket
|
||||
|
||||
- name: Upload release files to Github
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
|
1
.gitignore
vendored
@ -61,6 +61,7 @@ Mac_App_Distribution.provisionprofile
|
||||
*.tsbuildinfo
|
||||
src/wasm-lib/pkg
|
||||
|
||||
.eslintcache
|
||||
venv
|
||||
.vite/
|
||||
|
||||
|
43
INSTALL.md
Normal file
@ -0,0 +1,43 @@
|
||||
# Setting Up Zoo Modeling App
|
||||
|
||||
Compared to other CAD software, getting Zoo Modeling App up and running is quick and straightforward across platforms. It's about 100MB to download and is quick to install.
|
||||
|
||||
## Windows
|
||||
|
||||
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for Windows and for your processor type.
|
||||
|
||||
2. Once downloaded, run the installer `Zoo Modeling App-{version}-{arch}-win.exe` which should take a few seconds.
|
||||
|
||||
3. The installation happens at `C:\Program Files\Zoo Modeling App`. A shortcut in the start menu is also created so you can run the app easily by clicking on it.
|
||||
|
||||
## macOS
|
||||
|
||||
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for macOS and for your processor type.
|
||||
|
||||
2. Once downloaded, open the disk image `Zoo Modeling App-{version}-{arch}-mac.dmg` and drag the applications to your `Applications` directory.
|
||||
|
||||
3. You can then open your `Applications` directory and double-click on `Zoo Modeling App` to open.
|
||||
|
||||
|
||||
## Linux
|
||||
|
||||
1. Download the [Zoo Modeling App installer](https://zoo.dev/modeling-app/download) for Linux and for your processor type.
|
||||
|
||||
2. Install the dependencies needed to run the [AppImage format](https://appimage.org/).
|
||||
- On Ubuntu, install the FUSE library with these commands in a terminal.
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install libfuse2
|
||||
```
|
||||
- Optionally, follow [these steps](https://github.com/probonopd/go-appimage/blob/master/src/appimaged/README.md#initial-setup) to install `appimaged`. It is a daemon that makes interacting with AppImage files more seamless.
|
||||
- Once installed, copy the downloaded `Zoo Modeling App-{version}-{arch}-linux.AppImage` to the directory of your choice, for instance `~/Applications`.
|
||||
|
||||
- `appimaged` should automatically find it and make it executable. If not, run:
|
||||
```bash
|
||||
chmod a+x ~/Applications/Zoo\ Modeling\ App-{version}-{arch}-linux.AppImage
|
||||
```
|
||||
|
||||
3. You can double-click on the AppImage to run it, or in a terminal with this command:
|
||||
```bash
|
||||
~/Applications/Zoo\ Modeling\ App-{version}-{arch}-linux.AppImage
|
||||
```
|
@ -99,7 +99,7 @@ yarn tron:start
|
||||
|
||||
This will start the application and hot-reload on changes.
|
||||
|
||||
Devtools can be opened with the usual Cmd/Ctrl-Shift-I.
|
||||
Devtools can be opened with the usual Cmd-Opt-I (Mac) or Ctrl-Shift-I (Linux and Windows).
|
||||
|
||||
To build, run `yarn tron:package`.
|
||||
|
||||
|
@ -22,3 +22,5 @@ once fixed in engine will just start working here with no language changes.
|
||||
|
||||
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
|
||||
chamfer cases work currently.
|
||||
|
||||
- **Appearance**: Changing the appearance on a loft does not work.
|
||||
|
239
docs/kcl/appearance.md
Normal file
49
docs/kcl/atan2.md
Normal file
@ -19,6 +19,7 @@ layout: manual
|
||||
* [`angledLineThatIntersects`](kcl/angledLineThatIntersects)
|
||||
* [`angledLineToX`](kcl/angledLineToX)
|
||||
* [`angledLineToY`](kcl/angledLineToY)
|
||||
* [`appearance`](kcl/appearance)
|
||||
* [`arc`](kcl/arc)
|
||||
* [`arcTo`](kcl/arcTo)
|
||||
* [`asin`](kcl/asin)
|
||||
@ -29,6 +30,7 @@ layout: manual
|
||||
* [`assertLessThan`](kcl/assertLessThan)
|
||||
* [`assertLessThanOrEq`](kcl/assertLessThanOrEq)
|
||||
* [`atan`](kcl/atan)
|
||||
* [`atan2`](kcl/atan2)
|
||||
* [`bezierCurve`](kcl/bezierCurve)
|
||||
* [`ceil`](kcl/ceil)
|
||||
* [`chamfer`](kcl/chamfer)
|
||||
@ -101,6 +103,7 @@ layout: manual
|
||||
* [`startProfileAt`](kcl/startProfileAt)
|
||||
* [`startSketchAt`](kcl/startSketchAt)
|
||||
* [`startSketchOn`](kcl/startSketchOn)
|
||||
* [`sweep`](kcl/sweep)
|
||||
* [`tan`](kcl/tan)
|
||||
* [`tangentToEnd`](kcl/tangentToEnd)
|
||||
* [`tangentialArc`](kcl/tangentialArc)
|
||||
|
@ -45,7 +45,7 @@ circles = map([1..3], drawCircle)
|
||||
```js
|
||||
r = 10 // radius
|
||||
// Call `map`, using an anonymous function instead of a named one.
|
||||
circles = map([1..3], (id) {
|
||||
circles = map([1..3], fn(id) {
|
||||
return startSketchOn("XY")
|
||||
|> circle({ center = [id * 2 * r, 0], radius = r }, %)
|
||||
})
|
||||
|
@ -12,7 +12,7 @@ to other modules.
|
||||
|
||||
```
|
||||
// util.kcl
|
||||
export fn increment = (x) => {
|
||||
export fn increment(x) {
|
||||
return x + 1
|
||||
}
|
||||
```
|
||||
@ -37,11 +37,11 @@ Multiple functions can be exported in a file.
|
||||
|
||||
```
|
||||
// util.kcl
|
||||
export fn increment = (x) => {
|
||||
export fn increment(x) {
|
||||
return x + 1
|
||||
}
|
||||
|
||||
export fn decrement = (x) => {
|
||||
export fn decrement(x) {
|
||||
return x - 1
|
||||
}
|
||||
```
|
||||
|
@ -43,7 +43,7 @@ fn sum(arr) {
|
||||
|
||||
/* The above is basically like this pseudo-code:
|
||||
fn sum(arr):
|
||||
let sumSoFar = 0
|
||||
sumSoFar = 0
|
||||
for i in arr:
|
||||
sumSoFar = add(sumSoFar, i)
|
||||
return sumSoFar */
|
||||
@ -61,7 +61,7 @@ assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
|
||||
// an anonymous `add` function as its parameter, instead of declaring a
|
||||
// named function outside.
|
||||
arr = [1, 2, 3]
|
||||
sum = reduce(arr, 0, (i, result_so_far) {
|
||||
sum = reduce(arr, 0, fn(i, result_so_far) {
|
||||
return i + result_so_far
|
||||
})
|
||||
|
||||
@ -84,7 +84,7 @@ fn decagon(radius) {
|
||||
// Use a `reduce` to draw the remaining decagon sides.
|
||||
// For each number in the array 1..10, run the given function,
|
||||
// which takes a partially-sketched decagon and adds one more edge to it.
|
||||
fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) {
|
||||
fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {
|
||||
// Draw one edge of the decagon.
|
||||
x = cos(stepAngle * i) * radius
|
||||
y = sin(stepAngle * i) * radius
|
||||
@ -96,14 +96,14 @@ fn decagon(radius) {
|
||||
|
||||
/* The `decagon` above is basically like this pseudo-code:
|
||||
fn decagon(radius):
|
||||
let stepAngle = (1/10) * tau()
|
||||
let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
|
||||
stepAngle = (1/10) * tau()
|
||||
startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])
|
||||
|
||||
// Here's the reduce part.
|
||||
let partialDecagon = startOfDecagonSketch
|
||||
partialDecagon = startOfDecagonSketch
|
||||
for i in [1..10]:
|
||||
let x = cos(stepAngle * i) * radius
|
||||
let y = sin(stepAngle * i) * radius
|
||||
x = cos(stepAngle * i) * radius
|
||||
y = sin(stepAngle * i) * radius
|
||||
partialDecagon = lineTo([x, y], partialDecagon)
|
||||
fullDecagon = partialDecagon // it's now full
|
||||
return fullDecagon */
|
||||
|
65044
docs/kcl/std.json
55
docs/kcl/sweep.md
Normal file
@ -54,7 +54,7 @@ We also have support for defining your own functions. Functions can take in any
|
||||
type of argument. Below is an example of the syntax:
|
||||
|
||||
```
|
||||
fn myFn = (x) => {
|
||||
fn myFn(x) {
|
||||
return x
|
||||
}
|
||||
```
|
||||
@ -118,7 +118,7 @@ use the tag `rectangleSegmentA001` in any function or expression in the file.
|
||||
However if the code was written like this:
|
||||
|
||||
```
|
||||
fn rect = (origin) => {
|
||||
fn rect(origin) {
|
||||
return startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|
||||
@ -146,7 +146,7 @@ Tags are accessible through the sketch group they are declared in.
|
||||
For example the following code works.
|
||||
|
||||
```
|
||||
fn rect = (origin) => {
|
||||
fn rect(origin) {
|
||||
return startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|
||||
|
23
docs/kcl/types/AppearanceData.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
title: "AppearanceData"
|
||||
excerpt: "Data for appearance."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data for appearance.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `color` |`string`| Color of the new material, a hex string like "#ff0000". | No |
|
||||
| `metalness` |`number` (**maximum:** 100.0)| Metalness of the new material, a percentage like 95.7. | No |
|
||||
| `roughness` |`number` (**maximum:** 100.0)| Roughness of the new material, a percentage like 95.7. | No |
|
||||
|
||||
|
@ -12,5 +12,10 @@ KCL value for an optional parameter which was not given an argument. (remember,
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `digest` |`[, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`, `integer`]`| | No |
|
||||
|
||||
|
||||
|
23
docs/kcl/types/SweepData.md
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
title: "SweepData"
|
||||
excerpt: "Data for a sweep."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data for a sweep.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No |
|
||||
| `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
|
||||
| `tolerance` |`number`| Tolerance for the sweep operation. | No |
|
||||
|
||||
|
@ -57,23 +57,26 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||
await expect(u.codeLocator).toContainText(
|
||||
`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
|
||||
)
|
||||
}
|
||||
await page.waitForTimeout(500)
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
await expect(u.codeLocator)
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|
||||
|> xLine(${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')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
await expect(u.codeLocator)
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
|
||||
commonPoints.startAt
|
||||
}, sketch001)
|
||||
|> xLine(${commonPoints.num1}, %)
|
||||
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
||||
} else {
|
||||
@ -82,8 +85,10 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
||||
await page.waitForTimeout(200)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
if (openPanes.includes('code')) {
|
||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
await expect(u.codeLocator)
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
|
||||
commonPoints.startAt
|
||||
}, sketch001)
|
||||
|> xLine(${commonPoints.num1}, %)
|
||||
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||
|> xLine(${commonPoints.num2 * -1}, %)`)
|
||||
@ -140,8 +145,10 @@ async function doBasicSketch(page: Page, openPanes: string[]) {
|
||||
|
||||
// Open the code pane.
|
||||
await u.openKclCodePanel()
|
||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
await expect(u.codeLocator)
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
|
||||
commonPoints.startAt
|
||||
}, sketch001)
|
||||
|> xLine(${commonPoints.num1}, %, $seg01)
|
||||
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||
|> xLine(-segLen(seg01), %)`)
|
||||
|
@ -44,8 +44,7 @@ test.describe('Can create sketches on all planes and their back sides', () => {
|
||||
},
|
||||
}
|
||||
|
||||
const code = `sketch001 = startSketchOn('${plane}')
|
||||
|> startProfileAt([0.9, -1.22], %)`
|
||||
const code = `sketch001 = startSketchOn('${plane}')profile001 = startProfileAt([0.9, -1.22], sketch001)`
|
||||
|
||||
await u.openDebugPanel()
|
||||
|
||||
|
@ -94,6 +94,51 @@ test.describe('Editor tests', () => {
|
||||
|> close(%)`)
|
||||
})
|
||||
|
||||
test('ensure we use the cache, and do not re-execute', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1000, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.codeLocator.click()
|
||||
await page.keyboard.type(`sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, 20], %)
|
||||
|> line([-20, 0], %)
|
||||
|> close(%)`)
|
||||
|
||||
// Ensure we execute the first time.
|
||||
await u.openDebugPanel()
|
||||
await expect(
|
||||
page.locator('[data-receive-command-type="scene_clear_all"]')
|
||||
).toHaveCount(2)
|
||||
await expect(
|
||||
page.locator('[data-message-type="execution-done"]')
|
||||
).toHaveCount(2)
|
||||
|
||||
// Add whitespace to the end of the code.
|
||||
await u.codeLocator.click()
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await page.keyboard.press('Home')
|
||||
await page.keyboard.type(' ')
|
||||
await page.keyboard.press('Enter')
|
||||
await page.keyboard.type(' ')
|
||||
|
||||
// Ensure we don't execute the second time.
|
||||
await u.openDebugPanel()
|
||||
// Make sure we didn't clear the scene.
|
||||
await expect(
|
||||
page.locator('[data-message-type="execution-done"]')
|
||||
).toHaveCount(3)
|
||||
await expect(
|
||||
page.locator('[data-receive-command-type="scene_clear_all"]')
|
||||
).toHaveCount(2)
|
||||
})
|
||||
|
||||
test('if you click the format button it formats your code and executes so lints are still there', async ({
|
||||
page,
|
||||
}) => {
|
||||
@ -458,8 +503,8 @@ test.describe('Editor tests', () => {
|
||||
|
||||
/* add the following code to the editor ($ error is not a valid line)
|
||||
$ error
|
||||
const topAng = 30
|
||||
const bottomAng = 25
|
||||
topAng = 30
|
||||
bottomAng = 25
|
||||
*/
|
||||
await u.codeLocator.click()
|
||||
await page.keyboard.type('$ error')
|
||||
@ -474,12 +519,14 @@ test.describe('Editor tests', () => {
|
||||
await page.keyboard.type('bottomAng = 25')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// error in guter
|
||||
// error in gutter
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
await expect(page.getByText('Unexpected token: $').first()).toBeVisible()
|
||||
await expect(
|
||||
page.getByText('Tag names must not be empty').first()
|
||||
).toBeVisible()
|
||||
|
||||
// select the line that's causing the error and delete it
|
||||
await page.getByText('$ error').click()
|
||||
@ -518,7 +565,10 @@ test.describe('Editor tests', () => {
|
||||
await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('error with 2 source ranges gets 2 diagnostics', async ({ page }) => {
|
||||
// TODO currently multiple source ranges are not supported
|
||||
test.skip('error with 2 source ranges gets 2 diagnostics', async ({
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
|
||||
type mouseParams = {
|
||||
pixelDiff?: number
|
||||
shouldDbClick?: boolean
|
||||
}
|
||||
type mouseDragToParams = mouseParams & {
|
||||
fromPoint: { x: number; y: number }
|
||||
@ -75,11 +76,16 @@ export class SceneFixture {
|
||||
if (clickParams?.pixelDiff) {
|
||||
return doAndWaitForImageDiff(
|
||||
this.page,
|
||||
() => this.page.mouse.click(x, y),
|
||||
() =>
|
||||
clickParams?.shouldDbClick
|
||||
? this.page.mouse.dblclick(x, y)
|
||||
: this.page.mouse.click(x, y),
|
||||
clickParams.pixelDiff
|
||||
)
|
||||
}
|
||||
return this.page.mouse.click(x, y)
|
||||
return clickParams?.shouldDbClick
|
||||
? this.page.mouse.dblclick(x, y)
|
||||
: this.page.mouse.click(x, y)
|
||||
},
|
||||
(moveParams?: mouseParams) => {
|
||||
if (moveParams?.pixelDiff) {
|
||||
@ -214,23 +220,7 @@ export class SceneFixture {
|
||||
coords: { x: number; y: number },
|
||||
diff: number
|
||||
) => {
|
||||
let finalValue = colour
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const pixel = (await getPixelRGBs(this.page)(coords, 1))[0]
|
||||
if (!pixel) return null
|
||||
finalValue = pixel
|
||||
return pixel.every(
|
||||
(channel, index) => Math.abs(channel - colour[index]) < diff
|
||||
)
|
||||
})
|
||||
.toBeTruthy()
|
||||
.catch((cause) => {
|
||||
throw new Error(
|
||||
`ExpectPixelColor: expecting ${colour} got ${finalValue}`,
|
||||
{ cause }
|
||||
)
|
||||
})
|
||||
await expectPixelColor(this.page, colour, coords, diff)
|
||||
}
|
||||
|
||||
get gizmo() {
|
||||
@ -246,3 +236,28 @@ export class SceneFixture {
|
||||
await buttonToTest.click()
|
||||
}
|
||||
}
|
||||
|
||||
export async function expectPixelColor(
|
||||
page: Page,
|
||||
colour: [number, number, number],
|
||||
coords: { x: number; y: number },
|
||||
diff: number
|
||||
) {
|
||||
let finalValue = colour
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const pixel = (await getPixelRGBs(page)(coords, 1))[0]
|
||||
if (!pixel) return null
|
||||
finalValue = pixel
|
||||
return pixel.every(
|
||||
(channel, index) => Math.abs(channel - colour[index]) < diff
|
||||
)
|
||||
})
|
||||
.toBeTruthy()
|
||||
.catch((cause) => {
|
||||
throw new Error(
|
||||
`ExpectPixelColor: expecting ${colour} got ${finalValue}`,
|
||||
{ cause }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -6,10 +6,15 @@ export class ToolbarFixture {
|
||||
public page: Page
|
||||
|
||||
extrudeButton!: Locator
|
||||
loftButton!: Locator
|
||||
shellButton!: Locator
|
||||
offsetPlaneButton!: Locator
|
||||
startSketchBtn!: Locator
|
||||
lineBtn!: Locator
|
||||
tangentialArcBtn!: Locator
|
||||
circleBtn!: Locator
|
||||
rectangleBtn!: Locator
|
||||
lengthConstraintBtn!: Locator
|
||||
exitSketchBtn!: Locator
|
||||
editSketchBtn!: Locator
|
||||
fileTreeBtn!: Locator
|
||||
@ -26,10 +31,15 @@ export class ToolbarFixture {
|
||||
reConstruct = (page: Page) => {
|
||||
this.page = page
|
||||
this.extrudeButton = page.getByTestId('extrude')
|
||||
this.loftButton = page.getByTestId('loft')
|
||||
this.shellButton = page.getByTestId('shell')
|
||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||
this.startSketchBtn = page.getByTestId('sketch')
|
||||
this.lineBtn = page.getByTestId('line')
|
||||
this.tangentialArcBtn = page.getByTestId('tangential-arc')
|
||||
this.circleBtn = page.getByTestId('circle-center')
|
||||
this.rectangleBtn = page.getByTestId('corner-rectangle')
|
||||
this.lengthConstraintBtn = page.getByTestId('constraint-length')
|
||||
this.exitSketchBtn = page.getByTestId('sketch-exit')
|
||||
this.editSketchBtn = page.getByText('Edit Sketch')
|
||||
this.fileTreeBtn = page.locator('[id="files-button-holder"]')
|
||||
@ -87,4 +97,13 @@ export class ToolbarFixture {
|
||||
await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
|
||||
}
|
||||
}
|
||||
selectCenterRectangle = async () => {
|
||||
await this.page
|
||||
.getByRole('button', { name: 'caret down Corner rectangle:' })
|
||||
.click()
|
||||
await expect(
|
||||
this.page.getByTestId('dropdown-center-rectangle')
|
||||
).toBeVisible()
|
||||
await this.page.getByTestId('dropdown-center-rectangle').click()
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
TEST_SETTINGS_ONBOARDING_USER_MENU,
|
||||
} from './storageStates'
|
||||
import * as TOML from '@iarna/toml'
|
||||
import { expectPixelColor } from './fixtures/sceneFixture'
|
||||
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
if (testInfo.tags.includes('@electron')) {
|
||||
@ -45,7 +46,7 @@ test.describe('Onboarding tests', () => {
|
||||
{ settingsKey: TEST_SETTINGS_KEY }
|
||||
)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
@ -54,6 +55,12 @@ test.describe('Onboarding tests', () => {
|
||||
|
||||
// *and* that the code is shown in the editor
|
||||
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
|
||||
|
||||
// Make sure the model loaded
|
||||
const XYPlanePoint = { x: 774, y: 116 } as const
|
||||
const modelColor: [number, number, number] = [45, 45, 45]
|
||||
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(8)
|
||||
})
|
||||
|
||||
test(
|
||||
@ -72,7 +79,7 @@ test.describe('Onboarding tests', () => {
|
||||
|
||||
const u = await getUtils(page)
|
||||
|
||||
const viewportSize = { width: 1200, height: 500 }
|
||||
const viewportSize = { width: 1200, height: 1000 }
|
||||
await page.setViewportSize(viewportSize)
|
||||
|
||||
await test.step(`Create a project and open to the onboarding`, async () => {
|
||||
@ -92,6 +99,14 @@ test.describe('Onboarding tests', () => {
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
'// Shelf Bracket'
|
||||
)
|
||||
|
||||
// TODO: jess make less shit
|
||||
// Make sure the model loaded
|
||||
//const XYPlanePoint = { x: 986, y: 522 } as const
|
||||
//const modelColor: [number, number, number] = [76, 76, 76]
|
||||
//await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||
|
||||
//await expectPixelColor(page, modelColor, XYPlanePoint, 8)
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
@ -108,7 +123,7 @@ test.describe('Onboarding tests', () => {
|
||||
}, initialCode)
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
// Replay the onboarding
|
||||
@ -140,6 +155,12 @@ test.describe('Onboarding tests', () => {
|
||||
return localStorage.getItem('persistCode')
|
||||
})
|
||||
).toContain('// Shelf Bracket')
|
||||
|
||||
// Make sure the model loaded
|
||||
const XYPlanePoint = { x: 986, y: 522 } as const
|
||||
const modelColor: [number, number, number] = [76, 76, 76]
|
||||
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
|
||||
})
|
||||
|
||||
test('Click through each onboarding step', async ({ page }) => {
|
||||
@ -179,6 +200,17 @@ test.describe('Onboarding tests', () => {
|
||||
// Test that the onboarding pane is gone
|
||||
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
|
||||
await expect(page.url()).not.toContain('onboarding')
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// TODO: jess to fix
|
||||
// Make sure the model loaded
|
||||
//const XYPlanePoint = { x: 774, y: 516 } as const
|
||||
// const modelColor: [number, number, number] = [129, 129, 129]
|
||||
// await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||
// await expectPixelColor(page, modelColor, XYPlanePoint, 20)
|
||||
})
|
||||
|
||||
test('Onboarding redirects and code updating', async ({ page }) => {
|
||||
@ -439,7 +471,7 @@ test(
|
||||
})
|
||||
|
||||
await test.step('Navigate into project', async () => {
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
@ -462,7 +494,15 @@ test(
|
||||
await test.step('Confirm that the onboarding has restarted', async () => {
|
||||
await expect(tutorialProjectIndicator).toBeVisible()
|
||||
await expect(tutorialModalText).toBeVisible()
|
||||
// Make sure the model loaded
|
||||
const XYPlanePoint = { x: 988, y: 523 } as const
|
||||
const modelColor: [number, number, number] = [76, 76, 76]
|
||||
|
||||
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
|
||||
await tutorialDismissButton.click()
|
||||
// Make sure model still there.
|
||||
await expectPixelColor(page, modelColor, XYPlanePoint, 8)
|
||||
})
|
||||
|
||||
await test.step('Clear code and restart onboarding from settings', async () => {
|
||||
|
@ -135,7 +135,9 @@ test.describe('verify sketch on chamfer works', () => {
|
||||
pixelDiff: 50,
|
||||
})
|
||||
await rectangle2ndClick()
|
||||
await editor.expectEditor.toContain(afterRectangle2ndClickSnippet)
|
||||
await editor.expectEditor.toContain(afterRectangle2ndClickSnippet, {
|
||||
shouldNormalise: true,
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Clean up so that `_sketchOnAChamfer` util can be called again', async () => {
|
||||
@ -177,18 +179,13 @@ test.describe('verify sketch on chamfer works', () => {
|
||||
|
||||
afterChamferSelectSnippet:
|
||||
'sketch002 = startSketchOn(extrude001, seg03)',
|
||||
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002) - 90,
|
||||
105.26
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002),
|
||||
-segLen(rectangleSegmentA002)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`,
|
||||
afterRectangle1stClickSnippet:
|
||||
'startProfileAt([205.96, 254.59], sketch002)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
|
||||
|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
|
||||
|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
|
||||
|>lineTo([profileStartX(%),profileStartY(%)],%)
|
||||
|>close(%)`,
|
||||
})
|
||||
|
||||
await sketchOnAChamfer({
|
||||
@ -209,19 +206,15 @@ test.describe('verify sketch on chamfer works', () => {
|
||||
|
||||
afterChamferSelectSnippet:
|
||||
'sketch003 = startSketchOn(extrude001, seg04)',
|
||||
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA003) - 90,
|
||||
106.84
|
||||
], %, $rectangleSegmentB002)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA003),
|
||||
-segLen(rectangleSegmentA003)
|
||||
], %, $rectangleSegmentC002)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`,
|
||||
afterRectangle1stClickSnippet:
|
||||
'startProfileAt([-209.64, 255.28], sketch003)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0,11.56],%,$rectangleSegmentA003)
|
||||
|>angledLine([segAng(rectangleSegmentA003)-90,106.84],%)
|
||||
|>angledLine([segAng(rectangleSegmentA003),-segLen(rectangleSegmentA003)],%)
|
||||
|>lineTo([profileStartX(%),profileStartY(%)],%)
|
||||
|>close(%)`,
|
||||
})
|
||||
|
||||
await sketchOnAChamfer({
|
||||
clickCoords: { x: 677, y: 87 },
|
||||
cameraPos: { x: -6200, y: 1500, z: 6200 },
|
||||
@ -234,19 +227,14 @@ test.describe('verify sketch on chamfer works', () => {
|
||||
]
|
||||
}, %)`,
|
||||
afterChamferSelectSnippet:
|
||||
'sketch003 = startSketchOn(extrude001, seg04)',
|
||||
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA003) - 90,
|
||||
106.84
|
||||
], %, $rectangleSegmentB002)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA003),
|
||||
-segLen(rectangleSegmentA003)
|
||||
], %, $rectangleSegmentC002)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`,
|
||||
'sketch004 = startSketchOn(extrude001, seg05)',
|
||||
afterRectangle1stClickSnippet:
|
||||
'startProfileAt([82.57, 322.96], sketch004)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0,11.16],%,$rectangleSegmentA004)
|
||||
|>angledLine([segAng(rectangleSegmentA004)-90,103.07],%)
|
||||
|>angledLine([segAng(rectangleSegmentA004),-segLen(rectangleSegmentA004)],%)
|
||||
|>lineTo([profileStartX(%),profileStartY(%)],%)|
|
||||
>close(%)`,
|
||||
})
|
||||
/// last one
|
||||
await sketchOnAChamfer({
|
||||
@ -259,104 +247,97 @@ test.describe('verify sketch on chamfer works', () => {
|
||||
}, %)`,
|
||||
afterChamferSelectSnippet:
|
||||
'sketch005 = startSketchOn(extrude001, seg06)',
|
||||
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
|
||||
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA005) - 90,
|
||||
84.07
|
||||
], %, $rectangleSegmentB004)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA005),
|
||||
-segLen(rectangleSegmentA005)
|
||||
], %, $rectangleSegmentC004)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`,
|
||||
afterRectangle1stClickSnippet:
|
||||
'startProfileAt([-23.43, 19.69], sketch005)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0,9.1],%,$rectangleSegmentA005)
|
||||
|>angledLine([segAng(rectangleSegmentA005)-90,84.07],%)
|
||||
|>angledLine([segAng(rectangleSegmentA005),-segLen(rectangleSegmentA005)],%)
|
||||
|>lineTo([profileStartX(%),profileStartY(%)],%)
|
||||
|>close(%)`,
|
||||
})
|
||||
|
||||
await test.step('verify at the end of the test that final code is what is expected', async () => {
|
||||
await editor.expectEditor.toContain(
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
|
||||
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|
||||
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
217.26
|
||||
], %, $seg01)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $yo)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
|
||||
|> close(%)
|
||||
extrude001 = extrude(100, sketch001)
|
||||
|> chamfer({
|
||||
length = 30,
|
||||
tags = [getOppositeEdge(seg01)]
|
||||
}, %, $seg03)
|
||||
|> chamfer({ length = 30, tags = [seg01] }, %, $seg04)
|
||||
|> chamfer({
|
||||
length = 30,
|
||||
tags = [getNextAdjacentEdge(seg02)]
|
||||
}, %, $seg05)
|
||||
|> chamfer({
|
||||
length = 30,
|
||||
tags = [getNextAdjacentEdge(yo)]
|
||||
}, %, $seg06)
|
||||
sketch005 = startSketchOn(extrude001, seg06)
|
||||
|> startProfileAt([-23.43, 19.69], %)
|
||||
|> angledLine([0, 9.1], %, $rectangleSegmentA005)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA005) - 90,
|
||||
84.07
|
||||
], %, $rectangleSegmentB004)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA005),
|
||||
-segLen(rectangleSegmentA005)
|
||||
], %, $rectangleSegmentC004)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
sketch004 = startSketchOn(extrude001, seg05)
|
||||
|> startProfileAt([82.57, 322.96], %)
|
||||
|> angledLine([0, 11.16], %, $rectangleSegmentA004)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA004) - 90,
|
||||
103.07
|
||||
], %, $rectangleSegmentB003)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA004),
|
||||
-segLen(rectangleSegmentA004)
|
||||
], %, $rectangleSegmentC003)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
sketch003 = startSketchOn(extrude001, seg04)
|
||||
|> startProfileAt([-209.64, 255.28], %)
|
||||
|> angledLine([0, 11.56], %, $rectangleSegmentA003)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA003) - 90,
|
||||
106.84
|
||||
], %, $rectangleSegmentB002)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA003),
|
||||
-segLen(rectangleSegmentA003)
|
||||
], %, $rectangleSegmentC002)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
sketch002 = startSketchOn(extrude001, seg03)
|
||||
|> startProfileAt([205.96, 254.59], %)
|
||||
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002) - 90,
|
||||
105.26
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002),
|
||||
-segLen(rectangleSegmentA002)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
`,
|
||||
|> startProfileAt([75.8, 317.2], %) // [$startCapTag, $EndCapTag]
|
||||
|> angledLine([0, 268.43], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
217.26
|
||||
], %, $seg01)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $yo)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %, $seg02)
|
||||
|> close(%)
|
||||
extrude001 = extrude(100, sketch001)
|
||||
|> chamfer({
|
||||
length = 30,
|
||||
tags = [getOppositeEdge(seg01)]
|
||||
}, %, $seg03)
|
||||
|> chamfer({ length = 30, tags = [seg01] }, %, $seg04)
|
||||
|> chamfer({
|
||||
length = 30,
|
||||
tags = [getNextAdjacentEdge(seg02)]
|
||||
}, %, $seg05)
|
||||
|> chamfer({
|
||||
length = 30,
|
||||
tags = [getNextAdjacentEdge(yo)]
|
||||
}, %, $seg06)
|
||||
sketch005 = startSketchOn(extrude001, seg06)
|
||||
profile004 = startProfileAt([-23.43, 19.69], sketch005)
|
||||
|> angledLine([0, 9.1], %, $rectangleSegmentA005)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA005) - 90,
|
||||
84.07
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA005),
|
||||
-segLen(rectangleSegmentA005)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
sketch004 = startSketchOn(extrude001, seg05)
|
||||
profile003 = startProfileAt([82.57, 322.96], sketch004)
|
||||
|> angledLine([0, 11.16], %, $rectangleSegmentA004)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA004) - 90,
|
||||
103.07
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA004),
|
||||
-segLen(rectangleSegmentA004)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
sketch003 = startSketchOn(extrude001, seg04)
|
||||
profile002 = startProfileAt([-209.64, 255.28], sketch003)
|
||||
|> angledLine([0, 11.56], %, $rectangleSegmentA003)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA003) - 90,
|
||||
106.84
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA003),
|
||||
-segLen(rectangleSegmentA003)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
sketch002 = startSketchOn(extrude001, seg03)
|
||||
profile001 = startProfileAt([205.96, 254.59], sketch002)
|
||||
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002) - 90,
|
||||
105.26
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002),
|
||||
-segLen(rectangleSegmentA002)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
`,
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
})
|
||||
@ -392,18 +373,13 @@ test.describe('verify sketch on chamfer works', () => {
|
||||
beforeChamferSnippetEnd: '}, extrude001)',
|
||||
afterChamferSelectSnippet:
|
||||
'sketch002 = startSketchOn(extrude001, seg03)',
|
||||
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002) - 90,
|
||||
105.26
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002),
|
||||
-segLen(rectangleSegmentA002)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`,
|
||||
afterRectangle1stClickSnippet:
|
||||
'startProfileAt([205.96, 254.59], sketch002)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0,11.39],%,$rectangleSegmentA002)
|
||||
|>angledLine([segAng(rectangleSegmentA002)-90,105.26],%)
|
||||
|>angledLine([segAng(rectangleSegmentA002),-segLen(rectangleSegmentA002)],%)
|
||||
|>lineTo([profileStartX(%),profileStartY(%)],%)
|
||||
|>close(%)`,
|
||||
})
|
||||
await editor.expectEditor.toContain(
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
@ -433,16 +409,16 @@ chamf = chamfer({
|
||||
]
|
||||
}, %)
|
||||
sketch002 = startSketchOn(extrude001, seg03)
|
||||
|> startProfileAt([205.96, 254.59], %)
|
||||
profile001 = startProfileAt([205.96, 254.59], sketch002)
|
||||
|> angledLine([0, 11.39], %, $rectangleSegmentA002)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002) - 90,
|
||||
105.26
|
||||
], %, $rectangleSegmentB001)
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002),
|
||||
-segLen(rectangleSegmentA002)
|
||||
], %, $rectangleSegmentC001)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
`,
|
||||
@ -504,10 +480,10 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
|
||||
|
||||
const expectedCodeSnippets = {
|
||||
sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
|
||||
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
|
||||
pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], sketch001)`,
|
||||
segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
|
||||
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
|
||||
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
|
||||
afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], sketch001)`,
|
||||
afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], sketch001)`,
|
||||
}
|
||||
|
||||
await app.initialise()
|
||||
@ -677,3 +653,259 @@ test(`Offset plane point-and-click`, async ({
|
||||
await scene.expectPixelColor([74, 74, 74], testPoint, 15)
|
||||
})
|
||||
})
|
||||
|
||||
const loftPointAndClickCases = [
|
||||
{ shouldPreselect: true },
|
||||
{ shouldPreselect: false },
|
||||
]
|
||||
loftPointAndClickCases.forEach(({ shouldPreselect }) => {
|
||||
test(`Loft point-and-click (preselected sketches: ${shouldPreselect})`, async ({
|
||||
app,
|
||||
page,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||
plane001 = offsetPlane('XZ', 50)
|
||||
sketch002 = startSketchOn(plane001)
|
||||
|> circle({ center = [0, 0], radius = 20 }, %)
|
||||
`
|
||||
await app.initialise(initialCode)
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 575, y: 200 }
|
||||
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const [clickOnSketch2] = scene.makeMouseHelpers(
|
||||
testPoint.x,
|
||||
testPoint.y + 80
|
||||
)
|
||||
const loftDeclaration = 'loft001 = loft([sketch001, sketch002])'
|
||||
|
||||
await test.step(`Look for the white of the sketch001 shape`, async () => {
|
||||
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
|
||||
})
|
||||
|
||||
async function selectSketches() {
|
||||
await clickOnSketch1()
|
||||
await page.keyboard.down('Shift')
|
||||
await clickOnSketch2()
|
||||
await app.page.waitForTimeout(500)
|
||||
await page.keyboard.up('Shift')
|
||||
}
|
||||
|
||||
if (!shouldPreselect) {
|
||||
await test.step(`Go through the command bar flow without preselected sketches`, async () => {
|
||||
await toolbar.loftButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: { Selection: '' },
|
||||
highlightedHeaderArg: 'selection',
|
||||
commandName: 'Loft',
|
||||
})
|
||||
await selectSketches()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: { Selection: '2 faces' },
|
||||
commandName: 'Loft',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
} else {
|
||||
await test.step(`Preselect the two sketches`, async () => {
|
||||
await selectSketches()
|
||||
})
|
||||
|
||||
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
|
||||
await toolbar.loftButton.click()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: { Selection: '2 faces' },
|
||||
commandName: 'Loft',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
}
|
||||
|
||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||
await editor.expectEditor.toContain(loftDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: [loftDeclaration],
|
||||
highlightedCode: '',
|
||||
})
|
||||
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const shellPointAndClickCapCases = [
|
||||
{ shouldPreselect: true },
|
||||
{ shouldPreselect: false },
|
||||
]
|
||||
shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
|
||||
test(`Shell point-and-click cap (preselected sketches: ${shouldPreselect})`, async ({
|
||||
app,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||
extrude001 = extrude(30, sketch001)
|
||||
`
|
||||
await app.initialise(initialCode)
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 575, y: 200 }
|
||||
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const shellDeclaration =
|
||||
"shell001 = shell({ faces = ['end'], thickness = 5 }, extrude001)"
|
||||
|
||||
await test.step(`Look for the grey of the shape`, async () => {
|
||||
await scene.expectPixelColor([127, 127, 127], testPoint, 15)
|
||||
})
|
||||
|
||||
if (!shouldPreselect) {
|
||||
await test.step(`Go through the command bar flow without preselected faces`, async () => {
|
||||
await toolbar.shellButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Thickness: '',
|
||||
},
|
||||
highlightedHeaderArg: 'selection',
|
||||
commandName: 'Shell',
|
||||
})
|
||||
await clickOnCap()
|
||||
await app.page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Selection: '1 cap',
|
||||
Thickness: '5',
|
||||
},
|
||||
commandName: 'Shell',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
} else {
|
||||
await test.step(`Preselect the cap`, async () => {
|
||||
await clickOnCap()
|
||||
await app.page.waitForTimeout(500)
|
||||
})
|
||||
|
||||
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
|
||||
await toolbar.shellButton.click()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Selection: '1 cap',
|
||||
Thickness: '5',
|
||||
},
|
||||
commandName: 'Shell',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
}
|
||||
|
||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||
await editor.expectEditor.toContain(shellDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: [shellDeclaration],
|
||||
highlightedCode: '',
|
||||
})
|
||||
await scene.expectPixelColor([146, 146, 146], testPoint, 15)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('Shell point-and-click wall', async ({
|
||||
app,
|
||||
page,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-20, 20], %)
|
||||
|> xLine(40, %)
|
||||
|> yLine(-60, %)
|
||||
|> xLine(-40, %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude001 = extrude(40, sketch001)
|
||||
`
|
||||
await app.initialise(initialCode)
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 580, y: 180 }
|
||||
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70)
|
||||
const mutatedCode = 'xLine(-40, %, $seg01)'
|
||||
const shellDeclaration =
|
||||
"shell001 = shell({ faces = ['end', seg01], thickness = 5}, extrude001)"
|
||||
const formattedOutLastLine = '}, extrude001)'
|
||||
|
||||
await test.step(`Look for the grey of the shape`, async () => {
|
||||
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step(`Go through the command bar flow, selecting a wall and keeping default thickness`, async () => {
|
||||
await toolbar.shellButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Thickness: '',
|
||||
},
|
||||
highlightedHeaderArg: 'selection',
|
||||
commandName: 'Shell',
|
||||
})
|
||||
await clickOnCap()
|
||||
await page.keyboard.down('Shift')
|
||||
await clickOnWall()
|
||||
await app.page.waitForTimeout(500)
|
||||
await page.keyboard.up('Shift')
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Selection: '1 cap, 1 face',
|
||||
Thickness: '5',
|
||||
},
|
||||
commandName: 'Shell',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||
await editor.expectEditor.toContain(mutatedCode)
|
||||
await editor.expectEditor.toContain(shellDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: [formattedOutLastLine],
|
||||
highlightedCode: '',
|
||||
})
|
||||
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
|
||||
})
|
||||
})
|
||||
|
@ -136,6 +136,335 @@ test(
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
const bracketDir = join(dir, 'bracket')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
)
|
||||
const errorDir = join(dir, 'broken-code')
|
||||
await fsp.mkdir(errorDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('broken-code-test.kcl'),
|
||||
join(errorDir, 'main.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const u = await getUtils(page)
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
const pointOnModel = { x: 630, y: 280 }
|
||||
|
||||
await test.step('Opening the bracket project should load the stream', async () => {
|
||||
// expect to see the text bracket
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
|
||||
await page.getByTestId('app-logo').click()
|
||||
|
||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
||||
await expect(page.getByText('broken-code')).toBeVisible()
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
await expect(page.getByText('New Project')).toBeVisible()
|
||||
})
|
||||
await test.step('opening broken code project should clear the scene and show the error', async () => {
|
||||
// Go back home.
|
||||
await expect(page.getByText('broken-code')).toBeVisible()
|
||||
|
||||
await page.getByText('broken-code').click()
|
||||
|
||||
// error in guter
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
const crypticErrorText = `Expected a tag declarator`
|
||||
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
|
||||
|
||||
// black pixel means the scene has been cleared.
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [30, 30, 30]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
const bracketDir = join(dir, 'bracket')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
)
|
||||
const emptyDir = join(dir, 'empty')
|
||||
await fsp.mkdir(emptyDir, { recursive: true })
|
||||
await fsp.writeFile(join(emptyDir, 'main.kcl'), '')
|
||||
},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const u = await getUtils(page)
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
const pointOnModel = { x: 630, y: 280 }
|
||||
|
||||
await test.step('Opening the bracket project should load the stream', async () => {
|
||||
// expect to see the text bracket
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await test.step('Clicking the logo takes us back to the projects page / home', async () => {
|
||||
await page.getByTestId('app-logo').click()
|
||||
|
||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
||||
await expect(page.getByText('empty')).toBeVisible()
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
await expect(page.getByText('New Project')).toBeVisible()
|
||||
})
|
||||
await test.step('opening empty code project should clear the scene', async () => {
|
||||
// Go back home.
|
||||
await expect(page.getByText('empty')).toBeVisible()
|
||||
|
||||
await page.getByText('empty').click()
|
||||
|
||||
// Ensure the code is empty.
|
||||
await expect(u.codeLocator).toContainText('')
|
||||
expect(u.codeLocator.innerHTML.length).toBeLessThan(2)
|
||||
|
||||
// planes colors means the scene has been cleared.
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [92, 53, 53]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'open a file in a project works and renders, open empty file, it should clear the scene',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
const bracketDir = join(dir, 'bracket')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
)
|
||||
|
||||
await fsp.writeFile(join(bracketDir, 'empty.kcl'), '')
|
||||
},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const u = await getUtils(page)
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
const pointOnModel = { x: 630, y: 280 }
|
||||
|
||||
await test.step('Opening the bracket project should load the stream', async () => {
|
||||
// expect to see the text bracket
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
await test.step('creating a empty file should clear the scene', async () => {
|
||||
// open the file pane.
|
||||
await page.getByTestId('files-pane-button').click()
|
||||
|
||||
// OPen the other file.
|
||||
const file = page.getByRole('button', { name: 'empty.kcl' })
|
||||
await expect(file).toBeVisible()
|
||||
|
||||
await file.click()
|
||||
|
||||
// planes colors means the scene has been cleared.
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [92, 53, 53]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(15)
|
||||
|
||||
// Ensure the code is empty.
|
||||
await expect(u.codeLocator).toContainText('')
|
||||
expect(u.codeLocator.innerHTML.length).toBeLessThan(2)
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'open a file in a project works and renders, open another file in the same project with errors, it should clear the scene',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
const bracketDir = join(dir, 'bracket')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
join(bracketDir, 'main.kcl')
|
||||
)
|
||||
await fsp.copyFile(
|
||||
executorInputPath('broken-code-test.kcl'),
|
||||
join(bracketDir, 'broken-code-test.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
const u = await getUtils(page)
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
const pointOnModel = { x: 630, y: 280 }
|
||||
|
||||
await test.step('Opening the bracket project should load the stream', async () => {
|
||||
// expect to see the text bracket
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
|
||||
await expect(page.getByTestId('loading')).toBeAttached()
|
||||
await expect(page.getByTestId('loading')).not.toBeAttached({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
await test.step('opening broken code file should clear the scene and show the error', async () => {
|
||||
// open the file pane.
|
||||
await page.getByTestId('files-pane-button').click()
|
||||
|
||||
// OPen the other file.
|
||||
const file = page.getByRole('button', { name: 'broken-code-test.kcl' })
|
||||
await expect(file).toBeVisible()
|
||||
|
||||
await file.click()
|
||||
|
||||
// error in guter
|
||||
await expect(page.locator('.cm-lint-marker-error')).toBeVisible()
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
const crypticErrorText = `Expected a tag declarator`
|
||||
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
|
||||
|
||||
// black pixel means the scene has been cleared.
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [30, 30, 30]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'when code with error first loads you get errors in console',
|
||||
{ tag: '@electron' },
|
||||
|
@ -550,7 +550,7 @@ sketch001 = startSketchAt([-0, -0])
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Constants and locators
|
||||
const planeColor: [number, number, number] = [170, 220, 170]
|
||||
const planeColor: [number, number, number] = [161, 220, 155]
|
||||
const bgColor: [number, number, number] = [27, 27, 27]
|
||||
const middlePixelIsColor = async (color: [number, number, number]) => {
|
||||
return u.getGreatestPixDiff({ x: 600, y: 250 }, color)
|
||||
|
@ -7,6 +7,8 @@ try {
|
||||
.split('\n')
|
||||
.filter((line) => line && line.length > 1)
|
||||
.forEach((line) => {
|
||||
// Allow line comments.
|
||||
if (line.trimStart().startsWith('#')) return
|
||||
const [key, value] = line.split('=')
|
||||
// prefer env vars over secrets file
|
||||
secrets[key] = process.env[key] || (value as any).replaceAll('"', '')
|
||||
|
@ -114,9 +114,9 @@ test.describe('Sketch tests', () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([4.61, -14.01], %)
|
||||
|> xLine(12.73, %)
|
||||
|> tangentialArcTo([24.95, -5.38], %)`
|
||||
|> startProfileAt([2.61, -4.01], %)
|
||||
|> xLine(8.73, %)
|
||||
|> tangentialArcTo([8.33, -1.31], %)`
|
||||
)
|
||||
})
|
||||
|
||||
@ -126,7 +126,7 @@ test.describe('Sketch tests', () => {
|
||||
|
||||
await expect(async () => {
|
||||
await page.mouse.click(700, 200)
|
||||
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
|
||||
await page.getByText('tangentialArcTo([8.33, -1.31], %)').click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).toBeEnabled({ timeout: 1000 })
|
||||
@ -135,7 +135,7 @@ test.describe('Sketch tests', () => {
|
||||
|
||||
await page.waitForTimeout(600) // wait for animation
|
||||
|
||||
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
|
||||
await page.getByText('tangentialArcTo([8.33, -1.31], %)').click()
|
||||
await page.keyboard.press('End')
|
||||
await page.keyboard.down('Shift')
|
||||
await page.keyboard.press('ArrowUp')
|
||||
@ -149,17 +149,21 @@ test.describe('Sketch tests', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||
await page.waitForTimeout(100)
|
||||
// click start profileAt handle to continue profile
|
||||
await page.mouse.click(702, 407)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(async () => {
|
||||
// click to add segment
|
||||
await page.mouse.click(700, 200)
|
||||
|
||||
await expect.poll(u.normalisedEditorCode, { timeout: 1000 })
|
||||
.toBe(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([12.34, -12.34], %)
|
||||
.toBe(`sketch002 = startSketchOn('XZ')
|
||||
sketch001 = startProfileAt([12.34, -12.34], sketch002)
|
||||
|> yLine(12.34, %)
|
||||
|
||||
`)
|
||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||
}).toPass({ timeout: 5_000, intervals: [1_000] })
|
||||
})
|
||||
test('Can exit selection of face', async ({ page }) => {
|
||||
// Load the app with the code panes
|
||||
@ -669,7 +673,7 @@ test.describe('Sketch tests', () => {
|
||||
await page.waitForTimeout(500) // TODO detect animation ending, or disable animation
|
||||
|
||||
await click00r(0, 0)
|
||||
codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)`
|
||||
codeStr += `profile001 = startProfileAt(${toSU([0, 0])}, sketch001)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
await click00r(50, 0)
|
||||
@ -705,7 +709,7 @@ test.describe('Sketch tests', () => {
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await click00r(30, 0)
|
||||
codeStr += ` |> startProfileAt([2.03, 0], %)`
|
||||
codeStr += `profile002 = startProfileAt([2.03, 0], sketch002)`
|
||||
await expect(u.codeLocator).toHaveText(codeStr)
|
||||
|
||||
// TODO: I couldn't use `toSU` here because of some rounding error causing
|
||||
@ -742,7 +746,9 @@ test.describe('Sketch tests', () => {
|
||||
await u.openDebugPanel()
|
||||
|
||||
const code = `sketch001 = startSketchOn('-XZ')
|
||||
|> startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(scale * 34.8)}], %)
|
||||
profile001 = startProfileAt([${roundOff(scale * 69.6)}, ${roundOff(
|
||||
scale * 34.8
|
||||
)}], sketch001)
|
||||
|> xLine(${roundOff(scale * 139.19)}, %)
|
||||
|> yLine(-${roundOff(scale * 139.2)}, %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
@ -808,11 +814,17 @@ test.describe('Sketch tests', () => {
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
prevContent = await page.locator('.cm-content').innerText()
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||
// Assert the tool was unequipped
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const text = await page.locator('.cm-content').innerText()
|
||||
return text.replace(/\s/g, '')
|
||||
})
|
||||
.toBe(code.replace(/\s/g, ''))
|
||||
|
||||
// Assert the tool stays equipped after a profile is closed (ready for the next one)
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'line Line', exact: true })
|
||||
).not.toHaveAttribute('aria-pressed', 'true')
|
||||
).toHaveAttribute('aria-pressed', 'true')
|
||||
|
||||
// exit sketch
|
||||
await u.openAndClearDebugPanel()
|
||||
@ -943,6 +955,110 @@ sketch002 = startSketchOn(extrude001, 'END')
|
||||
`.replace(/\s/g, '')
|
||||
)
|
||||
})
|
||||
|
||||
/* TODO: once we fix bug turn on.
|
||||
test('empty-scene default-planes act as expected when spaces in file', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const XYPlanePoint = { x: 774, y: 116 } as const
|
||||
const unHoveredColor: [number, number, number] = [47, 47, 93]
|
||||
expect(
|
||||
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
|
||||
).toBeLessThan(8)
|
||||
|
||||
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// color should not change for having been hovered
|
||||
expect(
|
||||
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
|
||||
).toBeLessThan(8)
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
|
||||
// Fill with spaces
|
||||
await u.codeLocator.fill(`
|
||||
`)
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
expect(
|
||||
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
|
||||
).toBeLessThan(8)
|
||||
|
||||
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// color should not change for having been hovered
|
||||
expect(
|
||||
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
|
||||
).toBeLessThan(8)
|
||||
})
|
||||
|
||||
test('empty-scene default-planes act as expected when only code comments in file', async ({
|
||||
page,
|
||||
browserName,
|
||||
}) => {
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const XYPlanePoint = { x: 774, y: 116 } as const
|
||||
const unHoveredColor: [number, number, number] = [47, 47, 93]
|
||||
expect(
|
||||
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
|
||||
).toBeLessThan(8)
|
||||
|
||||
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// color should not change for having been hovered
|
||||
expect(
|
||||
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
|
||||
).toBeLessThan(8)
|
||||
|
||||
await u.openAndClearDebugPanel()
|
||||
|
||||
// Fill with spaces
|
||||
await u.codeLocator.fill(`// this is a code comments ya nerds
|
||||
`)
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
expect(
|
||||
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
|
||||
).toBeLessThan(8)
|
||||
|
||||
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
|
||||
await page.waitForTimeout(200)
|
||||
|
||||
// color should not change for having been hovered
|
||||
expect(
|
||||
await u.getGreatestPixDiff(XYPlanePoint, unHoveredColor)
|
||||
).toBeLessThan(8)
|
||||
})*/
|
||||
|
||||
test('empty-scene default-planes act as expected', async ({
|
||||
page,
|
||||
browserName,
|
||||
@ -1026,11 +1142,17 @@ sketch002 = startSketchOn(extrude001, 'END')
|
||||
await page.mouse.click(XYPlanePoint.x, XYPlanePoint.y)
|
||||
await page.waitForTimeout(200)
|
||||
await page.mouse.click(XYPlanePoint.x + 50, XYPlanePoint.y + 50)
|
||||
await expect(u.codeLocator).toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([11.8, 9.09], %)
|
||||
|> line([3.39, -3.39], %)
|
||||
`)
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const text = await u.codeLocator.innerText()
|
||||
return text.replace(/\s/g, '')
|
||||
})
|
||||
.toBe(
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
profile001 = startProfileAt([11.8, 9.09], sketch001)
|
||||
|> line([3.39, -3.39], %)
|
||||
`.replace(/\s/g, '')
|
||||
)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
@ -1315,3 +1437,560 @@ test2.describe(`Sketching with offset planes`, () => {
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test2.describe('multi-profile sketching', () => {
|
||||
test2(
|
||||
'Can add multiple profiles to a sketch (all tool types)',
|
||||
async ({ app, scene, toolbar, editor }) => {
|
||||
await app.initialise(``)
|
||||
|
||||
const [selectXZPlane] = scene.makeMouseHelpers(650, 150)
|
||||
|
||||
const [startProfile1] = scene.makeMouseHelpers(568, 70)
|
||||
const [endLineStartTanArc] = scene.makeMouseHelpers(701, 78)
|
||||
const [endArcStartLine] = scene.makeMouseHelpers(745, 189)
|
||||
|
||||
const [startProfile2] = scene.makeMouseHelpers(782, 80)
|
||||
const [profile2Point2] = scene.makeMouseHelpers(921, 90)
|
||||
const [profile2Point3] = scene.makeMouseHelpers(953, 178)
|
||||
|
||||
const [circle1Center] = scene.makeMouseHelpers(842, 147)
|
||||
const [circle1Radius] = scene.makeMouseHelpers(870, 171)
|
||||
|
||||
const [circle2Center] = scene.makeMouseHelpers(850, 222)
|
||||
const [circle2Radius] = scene.makeMouseHelpers(843, 230)
|
||||
|
||||
const [crnRect1point1] = scene.makeMouseHelpers(583, 205)
|
||||
const [crnRect1point2] = scene.makeMouseHelpers(618, 320)
|
||||
|
||||
const [crnRect2point1] = scene.makeMouseHelpers(663, 215)
|
||||
const [crnRect2point2] = scene.makeMouseHelpers(744, 276)
|
||||
|
||||
const [cntrRect1point1] = scene.makeMouseHelpers(624, 387)
|
||||
const [cntrRect1point2] = scene.makeMouseHelpers(676, 355)
|
||||
|
||||
const [cntrRect2point1] = scene.makeMouseHelpers(785, 332)
|
||||
const [cntrRect2point2] = scene.makeMouseHelpers(808, 286)
|
||||
|
||||
await toolbar.startSketchPlaneSelection()
|
||||
await selectXZPlane()
|
||||
// timeout wait for engine animation is unavoidable
|
||||
await app.page.waitForTimeout(600)
|
||||
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
|
||||
await test.step('Create a close profile stopping mid profile to equip the tangential arc, and than back to the line tool', async () => {
|
||||
await startProfile1()
|
||||
await editor.expectEditor.toContain(
|
||||
`profile001 = startProfileAt([4.61, 12.21], sketch001)`
|
||||
)
|
||||
|
||||
await endLineStartTanArc()
|
||||
await editor.expectEditor.toContain(`|> line([9.02, -0.55], %)`)
|
||||
await toolbar.tangentialArcBtn.click()
|
||||
await app.page.waitForTimeout(100)
|
||||
await endLineStartTanArc()
|
||||
|
||||
await endArcStartLine()
|
||||
await editor.expectEditor.toContain(
|
||||
`|> tangentialArcTo([16.61, 4.14], %)`
|
||||
)
|
||||
await toolbar.lineBtn.click()
|
||||
await app.page.waitForTimeout(100)
|
||||
await endArcStartLine()
|
||||
|
||||
await app.page.mouse.click(572, 110)
|
||||
await editor.expectEditor.toContain(`|> line([-11.73, 5.35], %)`)
|
||||
await startProfile1()
|
||||
await editor.expectEditor
|
||||
.toContain(`|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`)
|
||||
await app.page.waitForTimeout(100)
|
||||
})
|
||||
|
||||
await test.step('Without unequipping from the last step, make another profile, and one that is not closed', async () => {
|
||||
await startProfile2()
|
||||
await editor.expectEditor.toContain(
|
||||
`profile002 = startProfileAt([19.12, 11.53], sketch001)`
|
||||
)
|
||||
await profile2Point2()
|
||||
await editor.expectEditor.toContain(`|> line([9.43, -0.68], %)`)
|
||||
await profile2Point3()
|
||||
await editor.expectEditor.toContain(`|> line([2.17, -5.97], %)`)
|
||||
})
|
||||
|
||||
await test.step('create two circles in a row without unequip', async () => {
|
||||
await toolbar.circleBtn.click()
|
||||
|
||||
await circle1Center()
|
||||
await app.page.waitForTimeout(100)
|
||||
await circle1Radius()
|
||||
await editor.expectEditor.toContain(
|
||||
`profile003 = circle({ center = [23.19, 6.98], radius = 2.5 }, sketch001)`
|
||||
)
|
||||
|
||||
await test.step('hover in empty space to wait for overlays to get out of the way', async () => {
|
||||
await app.page.mouse.move(951, 223)
|
||||
await app.page.waitForTimeout(1000)
|
||||
})
|
||||
|
||||
await circle2Center()
|
||||
await app.page.waitForTimeout(100)
|
||||
await circle2Radius()
|
||||
await editor.expectEditor.toContain(
|
||||
`profile004 = circle({ center = [23.74, 1.9], radius = 0.72 }, sketch001)`
|
||||
)
|
||||
})
|
||||
await test.step('create two corner rectangles in a row without unequip', async () => {
|
||||
await toolbar.rectangleBtn.click()
|
||||
|
||||
await crnRect1point1()
|
||||
await editor.expectEditor.toContain(
|
||||
`profile005 = startProfileAt([5.63, 3.05], sketch001)`
|
||||
)
|
||||
await crnRect1point2()
|
||||
await editor.expectEditor
|
||||
.toContain(`|> angledLine([0, 2.37], %, $rectangleSegmentA001)
|
||||
|> angledLine([segAng(rectangleSegmentA001) - 90, 7.8], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`)
|
||||
await app.page.waitForTimeout(100)
|
||||
|
||||
await crnRect2point1()
|
||||
await editor.expectEditor.toContain(
|
||||
`profile006 = startProfileAt([11.05, 2.37], sketch001)`
|
||||
)
|
||||
await crnRect2point2()
|
||||
await editor.expectEditor
|
||||
.toContain(`|> angledLine([0, 5.49], %, $rectangleSegmentA002)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002) - 90,
|
||||
4.14
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002),
|
||||
-segLen(rectangleSegmentA002)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`)
|
||||
})
|
||||
|
||||
await test.step('create two center rectangles in a row without unequip', async () => {
|
||||
await toolbar.selectCenterRectangle()
|
||||
|
||||
await cntrRect1point1()
|
||||
await editor.expectEditor.toContain(
|
||||
`profile007 = startProfileAt([8.41, -9.29], sketch001)`
|
||||
)
|
||||
await cntrRect1point2()
|
||||
await editor.expectEditor
|
||||
.toContain(`|> angledLine([0, 7.06], %, $rectangleSegmentA003)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA003) + 90,
|
||||
4.34
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA003),
|
||||
-segLen(rectangleSegmentA003)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`)
|
||||
await app.page.waitForTimeout(100)
|
||||
|
||||
await cntrRect2point1()
|
||||
await editor.expectEditor.toContain(
|
||||
`profile008 = startProfileAt([19.33, -5.56], sketch001)`
|
||||
)
|
||||
await cntrRect2point2()
|
||||
await editor.expectEditor
|
||||
.toContain(`|> angledLine([0, 3.12], %, $rectangleSegmentA004)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA004) + 90,
|
||||
6.24
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA004),
|
||||
-segLen(rectangleSegmentA004)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test2(
|
||||
'Can edit a sketch with multiple profiles, dragging segments to edit them, and adding one new profile',
|
||||
async ({ app, scene, toolbar, editor }) => {
|
||||
await app.initialise(`sketch001 = startSketchOn('XZ')
|
||||
profile001 = startProfileAt([6.24, 4.54], sketch001)
|
||||
|> line([-0.41, 6.99], %)
|
||||
|> line([8.61, 0.74], %)
|
||||
|> line([10.99, -5.22], %)
|
||||
profile002 = startProfileAt([11.19, 5.02], sketch001)
|
||||
|> angledLine([0, 10.78], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
4.14
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
|
||||
`)
|
||||
|
||||
const [pointOnSegment] = scene.makeMouseHelpers(590, 141)
|
||||
const [profileEnd] = scene.makeMouseHelpers(970, 105)
|
||||
const profileEndMv = scene.makeMouseHelpers(951, 101)[1]
|
||||
const [newProfileEnd] = scene.makeMouseHelpers(764, 104)
|
||||
const dragSegmentTo = scene.makeMouseHelpers(850, 104)[1]
|
||||
|
||||
const rectHandle = scene.makeMouseHelpers(901, 150)[1]
|
||||
const rectDragTo = scene.makeMouseHelpers(901, 180)[1]
|
||||
|
||||
const circleEdge = scene.makeMouseHelpers(691, 331)[1]
|
||||
const dragCircleTo = scene.makeMouseHelpers(720, 331)[1]
|
||||
|
||||
const [rectStart] = scene.makeMouseHelpers(794, 322)
|
||||
const [rectEnd] = scene.makeMouseHelpers(757, 395)
|
||||
|
||||
await test2.step('enter sketch and setup', async () => {
|
||||
await pointOnSegment({ shouldDbClick: true })
|
||||
await app.page.waitForTimeout(600)
|
||||
|
||||
await toolbar.lineBtn.click()
|
||||
await app.page.waitForTimeout(100)
|
||||
})
|
||||
|
||||
await test2.step('extend existing profile', async () => {
|
||||
await profileEnd()
|
||||
await app.page.waitForTimeout(100)
|
||||
await newProfileEnd()
|
||||
await editor.expectEditor.toContain(`|> line([-11.4, 0.71], %)`)
|
||||
await toolbar.lineBtn.click()
|
||||
await app.page.waitForTimeout(100)
|
||||
})
|
||||
|
||||
await test2.step('edit existing profile', async () => {
|
||||
await profileEndMv()
|
||||
await app.page.mouse.down()
|
||||
await dragSegmentTo()
|
||||
await app.page.mouse.up()
|
||||
await editor.expectEditor.toContain(`line([4.16, -4.51], %)`)
|
||||
})
|
||||
|
||||
await test2.step('edit existing rect', async () => {
|
||||
await rectHandle()
|
||||
await app.page.mouse.down()
|
||||
await rectDragTo()
|
||||
await app.page.mouse.up()
|
||||
await editor.expectEditor.toContain(
|
||||
`angledLine([-7, 10.2], %, $rectangleSegmentA001)`
|
||||
)
|
||||
})
|
||||
|
||||
await test2.step('edit existing circl', async () => {
|
||||
await circleEdge()
|
||||
await app.page.mouse.down()
|
||||
await dragCircleTo()
|
||||
await app.page.mouse.up()
|
||||
await editor.expectEditor.toContain(
|
||||
`profile003 = circle({ center = [6.92, -4.2], radius = 4.77 }, sketch001)`
|
||||
)
|
||||
})
|
||||
|
||||
await test2.step('add new profile', async () => {
|
||||
await toolbar.rectangleBtn.click()
|
||||
await app.page.waitForTimeout(100)
|
||||
await rectStart()
|
||||
await editor.expectEditor.toContain(
|
||||
`profile004 = startProfileAt([15.62, -3.83], sketch001)`
|
||||
)
|
||||
await app.page.waitForTimeout(100)
|
||||
await rectEnd()
|
||||
await editor.expectEditor
|
||||
.toContain(`|> angledLine([180, 1.97], %, $rectangleSegmentA002)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002) + 90,
|
||||
3.88
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002),
|
||||
-segLen(rectangleSegmentA002)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`)
|
||||
})
|
||||
}
|
||||
)
|
||||
test2(
|
||||
'Can delete a profile in the editor while is sketch mode, and sketch mode does not break, can ctrl+z to undo after constraint with variable was added',
|
||||
async ({ app, scene, toolbar, editor, cmdBar }) => {
|
||||
await app.initialise(`sketch001 = startSketchOn('XZ')
|
||||
profile001 = startProfileAt([6.24, 4.54], sketch001)
|
||||
|> line([-0.41, 6.99], %)
|
||||
|> line([8.61, 0.74], %)
|
||||
|> line([10.99, -5.22], %)
|
||||
profile002 = startProfileAt([11.19, 5.02], sketch001)
|
||||
|> angledLine([0, 10.78], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
4.14
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
profile003 = circle({ center = [6.92, -4.2], radius = 3.16 }, sketch001)
|
||||
`)
|
||||
|
||||
const [pointOnSegment] = scene.makeMouseHelpers(590, 141)
|
||||
const [segment1Click] = scene.makeMouseHelpers(616, 131)
|
||||
const sketchIsDrawnProperly = async () => {
|
||||
await test2.step(
|
||||
'check the sketch is still drawn properly',
|
||||
async () => {
|
||||
await app.page.waitForTimeout(200)
|
||||
await scene.expectPixelColor(
|
||||
[255, 255, 255],
|
||||
{ x: 617, y: 163 },
|
||||
15
|
||||
)
|
||||
await scene.expectPixelColor(
|
||||
[255, 255, 255],
|
||||
{ x: 629, y: 331 },
|
||||
15
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
await test2.step('enter sketch and setup', async () => {
|
||||
await pointOnSegment({ shouldDbClick: true })
|
||||
await app.page.waitForTimeout(600)
|
||||
|
||||
await toolbar.lineBtn.click()
|
||||
await app.page.waitForTimeout(100)
|
||||
})
|
||||
|
||||
await test2.step('select and delete code for a profile', async () => {})
|
||||
await app.page.getByText('close(%)').click()
|
||||
await app.page.keyboard.down('Shift')
|
||||
for (let i = 0; i < 11; i++) {
|
||||
await app.page.keyboard.press('ArrowUp')
|
||||
}
|
||||
await app.page.keyboard.press('Home')
|
||||
await app.page.keyboard.up('Shift')
|
||||
await app.page.keyboard.press('Backspace')
|
||||
|
||||
await sketchIsDrawnProperly()
|
||||
|
||||
await test2.step('add random new var between profiles', async () => {
|
||||
await app.page.keyboard.type('myVar = 5')
|
||||
await app.page.keyboard.press('Enter')
|
||||
await app.page.waitForTimeout(600)
|
||||
})
|
||||
|
||||
await sketchIsDrawnProperly()
|
||||
|
||||
await test2.step(
|
||||
'Adding a constraint with a variable, and than ctrl-z-ing which will remove the variable again does not break sketch mode',
|
||||
async () => {
|
||||
await expect(async () => {
|
||||
await segment1Click()
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: ['|>line([-0.41,6.99],%)'],
|
||||
highlightedCode: 'line([-0.41,6.99],%)',
|
||||
})
|
||||
}).toPass({ timeout: 5_000, intervals: [500] })
|
||||
|
||||
await toolbar.lengthConstraintBtn.click()
|
||||
await cmdBar.progressCmdBar()
|
||||
await editor.expectEditor.toContain('length001 = 7')
|
||||
|
||||
// wait for execute defer
|
||||
await app.page.waitForTimeout(600)
|
||||
await sketchIsDrawnProperly()
|
||||
|
||||
await app.page.keyboard.down('Meta')
|
||||
await app.page.keyboard.press('KeyZ')
|
||||
await app.page.keyboard.up('Meta')
|
||||
|
||||
await editor.expectEditor.not.toContain('length001 = 7')
|
||||
await sketchIsDrawnProperly()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test2(
|
||||
'can enter sketch when there is an extrude',
|
||||
async ({ app, scene, toolbar }) => {
|
||||
await app.initialise(`sketch001 = startSketchOn('XZ')
|
||||
profile001 = startProfileAt([-63.43, 193.08], sketch001)
|
||||
|> line([168.52, 149.87], %)
|
||||
|> line([190.29, -39.18], %)
|
||||
|> tangentialArcTo([319.63, 129.65], %)
|
||||
|> line([-217.65, -21.76], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
profile003 = startProfileAt([16.79, 38.24], sketch001)
|
||||
|> angledLine([0, 182.82], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
105.71
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
profile004 = circle({
|
||||
center = [280.45, 47.57],
|
||||
radius = 55.26
|
||||
}, sketch001)
|
||||
extrude002 = extrude(50, profile001)
|
||||
extrude001 = extrude(5, profile003)
|
||||
`)
|
||||
const [pointOnSegment] = scene.makeMouseHelpers(574, 207)
|
||||
|
||||
await pointOnSegment()
|
||||
await toolbar.editSketch()
|
||||
// wait for engine animation
|
||||
await app.page.waitForTimeout(600)
|
||||
|
||||
await test2.step('check the sketch is still drawn properly', async () => {
|
||||
await scene.expectPixelColor([255, 255, 255], { x: 591, y: 167 }, 15)
|
||||
await scene.expectPixelColor([255, 255, 255], { x: 638, y: 222 }, 15)
|
||||
await scene.expectPixelColor([255, 255, 255], { x: 756, y: 214 }, 15)
|
||||
})
|
||||
}
|
||||
)
|
||||
test2(
|
||||
'exit new sketch without drawing anything should not be a problem',
|
||||
async ({ app, scene, toolbar, editor, cmdBar }) => {
|
||||
await app.initialise(`myVar = 5`)
|
||||
const [selectXZPlane] = scene.makeMouseHelpers(650, 150)
|
||||
|
||||
await toolbar.startSketchPlaneSelection()
|
||||
await selectXZPlane()
|
||||
// timeout wait for engine animation is unavoidable
|
||||
await app.page.waitForTimeout(600)
|
||||
|
||||
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
|
||||
await toolbar.exitSketchBtn.click()
|
||||
|
||||
await editor.expectEditor.not.toContain(`sketch001 = startSketchOn('XZ')`)
|
||||
|
||||
await test2.step(
|
||||
"still renders code, hasn't got into a weird state",
|
||||
async () => {
|
||||
await editor.replaceCode(
|
||||
'myVar = 5',
|
||||
`myVar = 5
|
||||
sketch001 = startSketchOn('XZ')
|
||||
profile001 = circle({
|
||||
center = [12.41, 3.87],
|
||||
radius = myVar
|
||||
}, sketch001)`
|
||||
)
|
||||
|
||||
await scene.expectPixelColor([255, 255, 255], { x: 633, y: 211 }, 15)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
test2(
|
||||
'A sketch with only "startProfileAt" and no segments should still be able to be continued ',
|
||||
async ({ app, scene, toolbar, editor }) => {
|
||||
await app.initialise(`sketch001 = startSketchOn('XZ')
|
||||
profile001 = startProfileAt([85.19, 338.59], sketch001)
|
||||
|> line([213.3, -94.52], %)
|
||||
|> line([-230.09, -55.34], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
sketch002 = startSketchOn('XY')
|
||||
profile002 = startProfileAt([85.81, 52.55], sketch002)
|
||||
|
||||
`)
|
||||
const [startProfileAt] = scene.makeMouseHelpers(606, 184)
|
||||
const [nextPoint] = scene.makeMouseHelpers(763, 130)
|
||||
await app.page
|
||||
.getByText('startProfileAt([85.81, 52.55], sketch002)')
|
||||
.click()
|
||||
await toolbar.editSketch()
|
||||
// timeout wait for engine animation is unavoidable
|
||||
await app.page.waitForTimeout(600)
|
||||
|
||||
// equip line tool
|
||||
await toolbar.lineBtn.click()
|
||||
await app.page.waitForTimeout(100)
|
||||
await startProfileAt()
|
||||
await app.page.waitForTimeout(100)
|
||||
await nextPoint()
|
||||
await editor.expectEditor.toContain(`|> line([126.05, 44.12], %)`)
|
||||
}
|
||||
)
|
||||
test2(
|
||||
'old style sketch all in one pipe (with extrude) will break up to allow users to add a new profile to the same sketch',
|
||||
async ({ app, scene, toolbar, editor }) => {
|
||||
await app.initialise(`thePart = startSketchOn('XZ')
|
||||
|> startProfileAt([7.53, 10.51], %)
|
||||
|> line([12.54, 1.83], %)
|
||||
|> line([6.65, -6.91], %)
|
||||
|> line([-6.31, -8.69], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude001 = extrude(75, thePart)
|
||||
`)
|
||||
const [objClick] = scene.makeMouseHelpers(565, 343)
|
||||
const [profilePoint1] = scene.makeMouseHelpers(609, 289)
|
||||
const [profilePoint2] = scene.makeMouseHelpers(714, 389)
|
||||
|
||||
await test2.step('enter sketch and setup', async () => {
|
||||
await objClick()
|
||||
await toolbar.editSketch()
|
||||
// timeout wait for engine animation is unavoidable
|
||||
await app.page.waitForTimeout(600)
|
||||
})
|
||||
|
||||
await test2.step(
|
||||
'expect code to match initial conditions still',
|
||||
async () => {
|
||||
await editor.expectEditor.toContain(`thePart = startSketchOn('XZ')
|
||||
|> startProfileAt([7.53, 10.51], %)`)
|
||||
}
|
||||
)
|
||||
|
||||
await test2.step(
|
||||
'equiping the line tool should break up the pipe expression',
|
||||
async () => {
|
||||
await toolbar.lineBtn.click()
|
||||
await editor.expectEditor.toContain(
|
||||
`sketch001 = startSketchOn('XZ')thePart = startProfileAt([7.53, 10.51], sketch001)`
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
await test2.step(
|
||||
'can continue on to add a new profile to this sketch',
|
||||
async () => {
|
||||
await profilePoint1()
|
||||
await editor.expectEditor.toContain(
|
||||
`profile001 = startProfileAt([19.77, -7.08], sketch001)`
|
||||
)
|
||||
await profilePoint2()
|
||||
await editor.expectEditor.toContain(`|> line([19.05, -18.14], %)`)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -446,8 +446,7 @@ test(
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
code += `
|
||||
|> startProfileAt([7.19, -9.7], %)`
|
||||
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
|
||||
await expect(page.locator('.cm-content')).toHaveText(code)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
@ -469,6 +468,10 @@ test(
|
||||
.getByRole('button', { name: 'arc Tangential Arc', exact: true })
|
||||
.click()
|
||||
|
||||
// click to continue profile
|
||||
await page.mouse.move(813, 392, { steps: 10 })
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.move(startXPx + PUR * 30, 500 - PUR * 20, { steps: 10 })
|
||||
|
||||
await page.waitForTimeout(1000)
|
||||
@ -591,8 +594,7 @@ test(
|
||||
mask: [page.getByTestId('model-state-indicator')],
|
||||
})
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [14.44, -2.44], radius = 1 }, %)`
|
||||
`sketch001 = startSketchOn('XZ')profile001 = circle({ center = [14.44, -2.44], radius = 1 }, sketch001)`
|
||||
)
|
||||
}
|
||||
)
|
||||
@ -636,8 +638,7 @@ test.describe(
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
code += `
|
||||
|> startProfileAt([7.19, -9.7], %)`
|
||||
code += `profile001 = startProfileAt([7.19, -9.7], sketch001)`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
@ -655,6 +656,10 @@ test.describe(
|
||||
.click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// click to continue profile
|
||||
await page.mouse.click(813, 392)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
|
||||
code += `
|
||||
@ -741,8 +746,7 @@ test.describe(
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
code += `
|
||||
|> startProfileAt([182.59, -246.32], %)`
|
||||
code += `profile001 = startProfileAt([182.59, -246.32], sketch001)`
|
||||
await expect(u.codeLocator).toHaveText(code)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
@ -760,6 +764,10 @@ test.describe(
|
||||
.click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// click to continue profile
|
||||
await page.mouse.click(813, 392)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 30, 500 - PUR * 20)
|
||||
|
||||
code += `
|
||||
@ -950,7 +958,75 @@ test(
|
||||
|
||||
test.describe('Grid visibility', { tag: '@snapshot' }, () => {
|
||||
// FIXME: Skip on macos its being weird.
|
||||
test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
// test.skip(process.platform === 'darwin', 'Skip on macos')
|
||||
|
||||
test('Grid turned off to on via command bar', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
const stream = page.getByTestId('stream')
|
||||
const mask = [
|
||||
page.locator('#app-header'),
|
||||
page.locator('#sidebar-top-ribbon'),
|
||||
page.locator('#sidebar-bottom-ribbon'),
|
||||
]
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
await page.goto('/')
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
// wait for execution done
|
||||
await expect(
|
||||
page.locator('[data-message-type="execution-done"]')
|
||||
).toHaveCount(1)
|
||||
await u.closeDebugPanel()
|
||||
await u.closeKclCodePanel()
|
||||
// TODO: Find a way to truly know that the objects have finished
|
||||
// rendering, because an execution-done message is not sufficient.
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Open the command bar.
|
||||
await page
|
||||
.getByRole('button', { name: 'Commands', exact: false })
|
||||
.or(page.getByRole('button', { name: '⌘K' }))
|
||||
.click()
|
||||
const commandName = 'show scale grid'
|
||||
const commandOption = page.getByRole('option', {
|
||||
name: commandName,
|
||||
exact: false,
|
||||
})
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
// This selector changes after we set the setting
|
||||
await cmdSearchBar.fill(commandName)
|
||||
await expect(commandOption).toBeVisible()
|
||||
await commandOption.click()
|
||||
|
||||
const toggleInput = page.getByPlaceholder('Off')
|
||||
await expect(toggleInput).toBeVisible()
|
||||
await expect(toggleInput).toBeFocused()
|
||||
|
||||
// Select On
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await expect(page.getByRole('option', { name: 'Off' })).toHaveAttribute(
|
||||
'data-headlessui-state',
|
||||
'active selected'
|
||||
)
|
||||
await page.keyboard.press('ArrowUp')
|
||||
await expect(page.getByRole('option', { name: 'On' })).toHaveAttribute(
|
||||
'data-headlessui-state',
|
||||
'active'
|
||||
)
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Check the toast appeared
|
||||
await expect(
|
||||
page.getByText(`Set show scale grid to "true" as a user default`)
|
||||
).toBeVisible()
|
||||
|
||||
await expect(stream).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
mask,
|
||||
})
|
||||
})
|
||||
|
||||
test('Grid turned off', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
@ -1096,3 +1172,109 @@ test.fixme('theme persists', async ({ page, context }) => {
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('code color goober', { tag: '@snapshot' }, () => {
|
||||
test('code color goober', async ({ page, context }) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`// Create a pipe using a sweep.
|
||||
|
||||
// Create a path for the sweep.
|
||||
sweepPath = startSketchOn('XZ')
|
||||
|> startProfileAt([0.05, 0.05], %)
|
||||
|> line([0, 7], %)
|
||||
|> tangentialArc({ offset = 90, radius = 5 }, %)
|
||||
|> line([-3, 0], %)
|
||||
|> tangentialArc({ offset = -90, radius = 5 }, %)
|
||||
|> line([0, 7], %)
|
||||
|
||||
sweepSketch = startSketchOn('XY')
|
||||
|> startProfileAt([2, 0], %)
|
||||
|> arc({
|
||||
angleEnd = 360,
|
||||
angleStart = 0,
|
||||
radius = 2
|
||||
}, %)
|
||||
|> sweep({
|
||||
path = sweepPath,
|
||||
}, %)
|
||||
|> appearance({
|
||||
color = "#bb00ff",
|
||||
metalness = 90,
|
||||
roughness = 90
|
||||
}, %)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
|
||||
await expect(page, 'expect small color widget').toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
|
||||
test('code color goober opening window', async ({ page, context }) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`// Create a pipe using a sweep.
|
||||
|
||||
// Create a path for the sweep.
|
||||
sweepPath = startSketchOn('XZ')
|
||||
|> startProfileAt([0.05, 0.05], %)
|
||||
|> line([0, 7], %)
|
||||
|> tangentialArc({ offset = 90, radius = 5 }, %)
|
||||
|> line([-3, 0], %)
|
||||
|> tangentialArc({ offset = -90, radius = 5 }, %)
|
||||
|> line([0, 7], %)
|
||||
|
||||
sweepSketch = startSketchOn('XY')
|
||||
|> startProfileAt([2, 0], %)
|
||||
|> arc({
|
||||
angleEnd = 360,
|
||||
angleStart = 0,
|
||||
radius = 2
|
||||
}, %)
|
||||
|> sweep({
|
||||
path = sweepPath,
|
||||
}, %)
|
||||
|> appearance({
|
||||
color = "#bb00ff",
|
||||
metalness = 90,
|
||||
roughness = 90
|
||||
}, %)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 1000 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.clearAndCloseDebugPanel()
|
||||
|
||||
await expect(page.locator('.cm-css-color-picker-wrapper')).toBeVisible()
|
||||
|
||||
// Click the color widget
|
||||
await page.locator('.cm-css-color-picker-wrapper input').click()
|
||||
|
||||
await expect(
|
||||
page,
|
||||
'expect small color widget to have window open'
|
||||
).toHaveScreenshot({
|
||||
maxDiffPixels: 100,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 144 KiB |
After Width: | Height: | Size: 130 KiB |
After Width: | Height: | Size: 139 KiB |
After Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
@ -14,7 +14,7 @@ export const TEST_SETTINGS = {
|
||||
},
|
||||
modeling: {
|
||||
defaultUnit: 'in',
|
||||
mouseControls: 'KittyCAD',
|
||||
mouseControls: 'Zoo',
|
||||
cameraProjection: 'perspective',
|
||||
showDebugPanel: true,
|
||||
},
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
import { commonPoints, getUtils, setup, tearDown } from './test-utils'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
|
||||
test.beforeEach(async ({ context, page }, testInfo) => {
|
||||
await setup(context, page, testInfo)
|
||||
@ -130,17 +132,16 @@ test.describe('Test network and connection issues', () => {
|
||||
|
||||
const startXPx = 600
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
|
||||
)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|
||||
|> xLine(${commonPoints.num1}, %)`)
|
||||
|
||||
// Expect the network to be up
|
||||
@ -188,7 +189,9 @@ test.describe('Test network and connection issues', () => {
|
||||
await page.mouse.click(100, 100)
|
||||
|
||||
// select a line
|
||||
await page.getByText(`startProfileAt(${commonPoints.startAt}, %)`).click()
|
||||
await page
|
||||
.getByText(`startProfileAt(${commonPoints.startAt}, sketch001)`)
|
||||
.click()
|
||||
|
||||
// enter sketch again
|
||||
await u.doAndWaitForCmd(
|
||||
@ -202,11 +205,36 @@ test.describe('Test network and connection issues', () => {
|
||||
|
||||
await page.waitForTimeout(150)
|
||||
|
||||
const camCommand: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_look_at',
|
||||
center: { x: 109, y: 0, z: -152 },
|
||||
vantage: { x: 115, y: -505, z: -152 },
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
},
|
||||
}
|
||||
const updateCamCommand: EngineCommand = {
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'default_camera_get_settings',
|
||||
},
|
||||
}
|
||||
await u.sendCustomCmd(camCommand)
|
||||
await page.waitForTimeout(100)
|
||||
await u.sendCustomCmd(updateCamCommand)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// click to continue profile
|
||||
await page.mouse.click(1007, 400)
|
||||
await page.waitForTimeout(100)
|
||||
// Ensure we can continue sketching
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await expect.poll(u.normalisedEditorCode)
|
||||
.toBe(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([12.34, -12.34], %)
|
||||
profile001 = startProfileAt([12.34, -12.34], sketch001)
|
||||
|> xLine(12.34, %)
|
||||
|> line([-12.34, 12.34], %)
|
||||
|
||||
@ -216,7 +244,7 @@ test.describe('Test network and connection issues', () => {
|
||||
|
||||
await expect.poll(u.normalisedEditorCode)
|
||||
.toBe(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([12.34, -12.34], %)
|
||||
profile001 = startProfileAt([12.34, -12.34], sketch001)
|
||||
|> xLine(12.34, %)
|
||||
|> line([-12.34, 12.34], %)
|
||||
|> xLine(-12.34, %)
|
||||
|
@ -479,4 +479,26 @@ test.describe('Testing Camera Movement', () => {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test('Right-click opens context menu when not dragged', async ({ page }) => {
|
||||
const u = await getUtils(page)
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await test.step(`The menu should not show if we drag the mouse`, async () => {
|
||||
await page.mouse.move(900, 200)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
await page.mouse.move(900, 300)
|
||||
await page.mouse.up({ button: 'right' })
|
||||
|
||||
await expect(page.getByTestId('view-controls-menu')).not.toBeVisible()
|
||||
})
|
||||
|
||||
await test.step(`The menu should show if we don't drag the mouse`, async () => {
|
||||
await page.mouse.move(900, 200)
|
||||
await page.mouse.down({ button: 'right' })
|
||||
await page.mouse.up({ button: 'right' })
|
||||
|
||||
await expect(page.getByTestId('view-controls-menu')).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -17,16 +17,26 @@ test.describe('Testing constraints', () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, 20], %)
|
||||
|> xLine(-20, %)
|
||||
`
|
||||
|> startProfileAt([-10, -10], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, 20], %)
|
||||
|> xLine(-20, %)
|
||||
`
|
||||
)
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
// constants and locators
|
||||
const lengthValue = {
|
||||
old: '20',
|
||||
new: '25',
|
||||
}
|
||||
const cmdBarKclInput = page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.getByRole('textbox')
|
||||
const cmdBarSubmitButton = page.getByRole('button', {
|
||||
name: 'arrow right Continue',
|
||||
})
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
@ -36,26 +46,26 @@ test.describe('Testing constraints', () => {
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// Click the line of code for line.
|
||||
await page.getByText(`line([0, 20], %)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
||||
// TODO remove this and reinstate `await topHorzSegmentClick()`
|
||||
await page.getByText(`line([0, ${lengthValue.old}], %)`).click()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// enter sketch again
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(500) // wait for animation
|
||||
|
||||
const startXPx = 500
|
||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
||||
await page.keyboard.down('Shift')
|
||||
await page.mouse.click(834, 244)
|
||||
await page.keyboard.up('Shift')
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'dimension Length', exact: true })
|
||||
.click()
|
||||
await page.getByText('Add constraining value').click()
|
||||
await expect(cmdBarKclInput).toHaveText('20')
|
||||
await cmdBarKclInput.fill(lengthValue.new)
|
||||
await expect(
|
||||
page.getByText(`Can't calculate`),
|
||||
`Something went wrong with the KCL expression evaluation`
|
||||
).not.toBeVisible()
|
||||
await cmdBarSubmitButton.click()
|
||||
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`length001 = 20sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
|
||||
`length001 = ${lengthValue.new}sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
|
||||
)
|
||||
|
||||
// Make sure we didn't pop out of sketch mode.
|
||||
@ -66,7 +76,6 @@ test.describe('Testing constraints', () => {
|
||||
await page.waitForTimeout(500) // wait for animation
|
||||
|
||||
// Exit sketch
|
||||
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Exit Sketch' })
|
||||
@ -524,7 +533,7 @@ part002 = startSketchOn('XZ')
|
||||
})
|
||||
}
|
||||
})
|
||||
test.describe('Test Angle/Length constraint single selection', () => {
|
||||
test.describe('Test Angle constraint single selection', () => {
|
||||
const cases = [
|
||||
{
|
||||
testName: 'Angle - Add variable',
|
||||
@ -538,18 +547,6 @@ part002 = startSketchOn('XZ')
|
||||
constraint: 'angle',
|
||||
value: '83, 78.33',
|
||||
},
|
||||
{
|
||||
testName: 'Length - Add variable',
|
||||
addVariable: true,
|
||||
constraint: 'length',
|
||||
value: '83, length001',
|
||||
},
|
||||
{
|
||||
testName: 'Length - No variable',
|
||||
addVariable: false,
|
||||
constraint: 'length',
|
||||
value: '83, 78.33',
|
||||
},
|
||||
] as const
|
||||
for (const { testName, addVariable, value, constraint } of cases) {
|
||||
test(`${testName}`, async ({ page }) => {
|
||||
@ -608,6 +605,90 @@ part002 = startSketchOn('XZ')
|
||||
})
|
||||
}
|
||||
})
|
||||
test.describe('Test Length constraint single selection', () => {
|
||||
const cases = [
|
||||
{
|
||||
testName: 'Length - Add variable',
|
||||
addVariable: true,
|
||||
constraint: 'length',
|
||||
value: '83, length001',
|
||||
},
|
||||
{
|
||||
testName: 'Length - No variable',
|
||||
addVariable: false,
|
||||
constraint: 'length',
|
||||
value: '83, 78.33',
|
||||
},
|
||||
] as const
|
||||
for (const { testName, addVariable, value, constraint } of cases) {
|
||||
test(`${testName}`, async ({ page }) => {
|
||||
// constants and locators
|
||||
const cmdBarKclInput = page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.getByRole('textbox')
|
||||
const cmdBarKclVariableNameInput =
|
||||
page.getByPlaceholder('Variable name')
|
||||
const cmdBarSubmitButton = page.getByRole('button', {
|
||||
name: 'arrow right Continue',
|
||||
})
|
||||
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`yo = 5
|
||||
part001 = startSketchOn('XZ')
|
||||
|> startProfileAt([-7.54, -26.74], %)
|
||||
|> line([74.36, 130.4], %)
|
||||
|> line([78.92, -120.11], %)
|
||||
|> line([9.16, 77.79], %)
|
||||
|> line([51.19, 48.97], %)
|
||||
part002 = startSketchOn('XZ')
|
||||
|> startProfileAt([299.05, 231.45], %)
|
||||
|> xLine(-425.34, %, $seg_what)
|
||||
|> yLine(-264.06, %)
|
||||
|> xLine(segLen(seg_what), %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)`
|
||||
)
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
await u.waitForAuthSkipAppStart()
|
||||
|
||||
await page.getByText('line([74.36, 130.4], %)').click()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
|
||||
const line3 = await u.getSegmentBodyCoords(
|
||||
`[data-overlay-index="${2}"]`
|
||||
)
|
||||
|
||||
await page.mouse.click(line3.x, line3.y)
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Length: open menu',
|
||||
})
|
||||
.click()
|
||||
await page.getByTestId('dropdown-constraint-' + constraint).click()
|
||||
|
||||
if (!addVariable) {
|
||||
await test.step(`Clear the variable input`, async () => {
|
||||
await cmdBarKclVariableNameInput.clear()
|
||||
await cmdBarKclVariableNameInput.press('Backspace')
|
||||
})
|
||||
}
|
||||
await expect(cmdBarKclInput).toHaveText('78.33')
|
||||
await cmdBarSubmitButton.click()
|
||||
|
||||
const changedCode = `|> angledLine([${value}], %)`
|
||||
await expect(page.locator('.cm-content')).toContainText(changedCode)
|
||||
// checking active assures the cursor is where it should be
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(changedCode)
|
||||
|
||||
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
|
||||
})
|
||||
}
|
||||
})
|
||||
test.describe('Many segments - no modal constraints', () => {
|
||||
const cases = [
|
||||
{
|
||||
@ -868,6 +949,15 @@ part002 = startSketchOn('XZ')
|
||||
|> line([3.13, -2.4], %)`
|
||||
)
|
||||
})
|
||||
|
||||
// constants and locators
|
||||
const cmdBarKclInput = page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.getByRole('textbox')
|
||||
const cmdBarSubmitButton = page.getByRole('button', {
|
||||
name: 'arrow right Continue',
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
|
||||
@ -928,8 +1018,8 @@ part002 = startSketchOn('XZ')
|
||||
// await page.getByRole('button', { name: 'length', exact: true }).click()
|
||||
await page.getByTestId('dropdown-constraint-length').click()
|
||||
|
||||
await page.getByLabel('length Value').fill('10')
|
||||
await page.getByRole('button', { name: 'Add constraining value' }).click()
|
||||
await cmdBarKclInput.fill('10')
|
||||
await cmdBarSubmitButton.click()
|
||||
|
||||
activeLinesContent = await page.locator('.cm-activeLine').all()
|
||||
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)
|
||||
|
@ -23,7 +23,7 @@ test.describe('Test toggling perspective', () => {
|
||||
y: screenHeight * 0.4,
|
||||
}
|
||||
const backgroundColor: [number, number, number] = [29, 29, 29]
|
||||
const xzPlaneColor: [number, number, number] = [50, 50, 99]
|
||||
const xzPlaneColor: [number, number, number] = [82, 55, 96]
|
||||
const locationToHaveColor = async (color: [number, number, number]) => {
|
||||
return u.getGreatestPixDiff(checkedScreenLocation, color)
|
||||
}
|
||||
|
@ -91,7 +91,14 @@ test.describe('Testing segment overlays', () => {
|
||||
await page.getByTestId('constraint-symbol-popover').count()
|
||||
).toBeGreaterThan(0)
|
||||
await unconstrainedLocator.click()
|
||||
await page.getByText('Add variable').click()
|
||||
await expect(
|
||||
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
|
||||
).toBeFocused()
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'arrow right Continue',
|
||||
})
|
||||
.click()
|
||||
await expect(page.locator('.cm-content')).toContainText(expectFinal)
|
||||
}
|
||||
|
||||
@ -151,7 +158,14 @@ test.describe('Testing segment overlays', () => {
|
||||
await page.getByTestId('constraint-symbol-popover').count()
|
||||
).toBeGreaterThan(0)
|
||||
await unconstrainedLocator.click()
|
||||
await page.getByText('Add variable').click()
|
||||
await expect(
|
||||
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
|
||||
).toBeFocused()
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'arrow right Continue',
|
||||
})
|
||||
.click()
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
expectAfterUnconstrained
|
||||
)
|
||||
|
@ -77,30 +77,31 @@ test.describe('Testing selections', () => {
|
||||
const startXPx = 600
|
||||
await u.closeDebugPanel()
|
||||
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)`)
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)`
|
||||
)
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
|
||||
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${commonPoints.startAt}, sketch001)
|
||||
|> xLine(${commonPoints.num1}, %)`)
|
||||
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
|
||||
commonPoints.startAt
|
||||
}, sketch001)
|
||||
|> xLine(${commonPoints.num1}, %)
|
||||
|> yLine(${commonPoints.num1 + 0.01}, %)`)
|
||||
await page.waitForTimeout(100)
|
||||
await page.mouse.click(startXPx, 500 - PUR * 20)
|
||||
await expect(page.locator('.cm-content'))
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt(${commonPoints.startAt}, %)
|
||||
.toHaveText(`sketch001 = startSketchOn('XZ')profile001 = startProfileAt(${
|
||||
commonPoints.startAt
|
||||
}, sketch001)
|
||||
|> xLine(${commonPoints.num1}, %)
|
||||
|> yLine(${commonPoints.num1 + 0.01}, %)
|
||||
|> xLine(${commonPoints.num2 * -1}, %)`)
|
||||
@ -330,6 +331,28 @@ part009 = startSketchOn('XY')
|
||||
|> angledLineToX({ angle = 60, to = pipeLargeDia }, %)
|
||||
|> close(%)
|
||||
rev = revolve({ axis = 'y' }, part009)
|
||||
sketch006 = startSketchOn('XY')
|
||||
profile001 = circle({
|
||||
center = [42.91, -70.42],
|
||||
radius = 17.96
|
||||
}, sketch006)
|
||||
profile002 = startProfileAt([86.92, -63.81], sketch006)
|
||||
|> angledLine([0, 63.81], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
17.05
|
||||
], %)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
profile003 = startProfileAt([40.16, -120.48], sketch006)
|
||||
|> line([26.95, 24.21], %)
|
||||
|> line([20.91, -28.61], %)
|
||||
|> line([32.46, 18.71], %)
|
||||
|
||||
`
|
||||
)
|
||||
}, KCL_DEFAULT_LENGTH)
|
||||
@ -362,9 +385,10 @@ rev = revolve({ axis = 'y' }, part009)
|
||||
})
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
const revolve = { x: 646, y: 248 }
|
||||
const revolve = { x: 635, y: 253 }
|
||||
const parentExtrude = { x: 915, y: 133 }
|
||||
const solid2d = { x: 770, y: 167 }
|
||||
const individualProfile = { x: 694, y: 432 }
|
||||
|
||||
// DELETE REVOLVE
|
||||
await page.mouse.click(revolve.x, revolve.y)
|
||||
@ -430,6 +454,20 @@ rev = revolve({ axis = 'y' }, part009)
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(u.codeLocator).not.toContainText(`sketch005 = startSketchOn({`)
|
||||
|
||||
// Delete a single profile
|
||||
await page.mouse.click(individualProfile.x, individualProfile.y)
|
||||
await page.waitForTimeout(100)
|
||||
const codeToBeDeletedSnippet =
|
||||
'profile003 = startProfileAt([40.16, -120.48], sketch006)'
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(
|
||||
' |> line([20.91, -28.61], %)'
|
||||
)
|
||||
await u.clearCommandLogs()
|
||||
await page.keyboard.press('Backspace')
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]', 10_000)
|
||||
await page.waitForTimeout(200)
|
||||
await expect(u.codeLocator).not.toContainText(codeToBeDeletedSnippet)
|
||||
})
|
||||
test("Deleting solid that the AST mod can't handle results in a toast message", async ({
|
||||
page,
|
||||
@ -1258,12 +1296,15 @@ extrude001 = extrude(50, sketch001)
|
||||
|
||||
await page.waitForTimeout(600)
|
||||
|
||||
const firstClickCoords = { x: 650, y: 200 } as const
|
||||
// Place a point because the line tool will exit if no points are pressed
|
||||
await page.mouse.click(650, 200)
|
||||
await page.mouse.click(firstClickCoords.x, firstClickCoords.y)
|
||||
await page.waitForTimeout(600)
|
||||
|
||||
// Code before exiting the tool
|
||||
let previousCodeContent = await page.locator('.cm-content').innerText()
|
||||
let previousCodeContent = (
|
||||
await page.locator('.cm-content').innerText()
|
||||
).replace(/\s+/g, '')
|
||||
|
||||
// deselect the line tool by clicking it
|
||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||
@ -1275,14 +1316,23 @@ extrude001 = extrude(50, sketch001)
|
||||
await page.mouse.click(750, 200)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// expect no change
|
||||
await expect(page.locator('.cm-content')).toHaveText(previousCodeContent)
|
||||
await expect
|
||||
.poll(async () => {
|
||||
let str = await page.locator('.cm-content').innerText()
|
||||
str = str.replace(/\s+/g, '')
|
||||
return str
|
||||
})
|
||||
.toBe(previousCodeContent)
|
||||
|
||||
// select line tool again
|
||||
await page.getByRole('button', { name: 'line Line', exact: true }).click()
|
||||
|
||||
await u.closeDebugPanel()
|
||||
|
||||
// Click to continue profile
|
||||
await page.mouse.click(firstClickCoords.x, firstClickCoords.y)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// line tool should work as expected again
|
||||
await page.mouse.click(700, 200)
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(
|
||||
|
@ -224,8 +224,13 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
|
||||
// Draw a line
|
||||
await page.mouse.move(700, 200, { steps: 5 })
|
||||
await page.mouse.click(700, 200)
|
||||
await page.mouse.move(800, 250, { steps: 5 })
|
||||
await page.mouse.click(800, 250)
|
||||
|
||||
const secondMousePosition = { x: 800, y: 250 }
|
||||
|
||||
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
|
||||
steps: 5,
|
||||
})
|
||||
await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
|
||||
// Unequip line tool
|
||||
await page.keyboard.press('Escape')
|
||||
// Make sure we didn't pop out of sketch mode.
|
||||
@ -234,9 +239,17 @@ test('First escape in tool pops you out of tool, second exits sketch mode', asyn
|
||||
// Equip arc tool
|
||||
await page.keyboard.press('a')
|
||||
await expect(arcButton).toHaveAttribute('aria-pressed', 'true')
|
||||
|
||||
// click in the same position again to continue the profile
|
||||
await page.mouse.move(secondMousePosition.x, secondMousePosition.y, {
|
||||
steps: 5,
|
||||
})
|
||||
await page.mouse.click(secondMousePosition.x, secondMousePosition.y)
|
||||
|
||||
await page.mouse.move(1000, 100, { steps: 5 })
|
||||
await page.mouse.click(1000, 100)
|
||||
await page.keyboard.press('Escape')
|
||||
await expect(arcButton).toHaveAttribute('aria-pressed', 'false')
|
||||
await page.keyboard.press('l')
|
||||
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
|
||||
|
||||
@ -537,9 +550,9 @@ test('Sketch on face', async ({ page }) => {
|
||||
|
||||
await expect.poll(u.normalisedEditorCode).toContain(
|
||||
u.normalisedCode(`sketch002 = startSketchOn(extrude001, seg01)
|
||||
|> startProfileAt([-12.94, 6.6], %)
|
||||
|> line([2.45, -0.2], %)
|
||||
|> line([-2.6, -1.25], %)
|
||||
profile001 = startProfileAt([-12.88, 6.66], sketch002)
|
||||
|> line([2.71, -0.22], %)
|
||||
|> line([-2.87, -1.38], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`)
|
||||
)
|
||||
@ -554,8 +567,7 @@ test('Sketch on face', async ({ page }) => {
|
||||
await page.getByText('startProfileAt([-12').click()
|
||||
await expect(page.getByRole('button', { name: 'Edit Sketch' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(400)
|
||||
await page.waitForTimeout(150)
|
||||
await page.waitForTimeout(500)
|
||||
await page.setViewportSize({ width: 1200, height: 1200 })
|
||||
await u.openAndClearDebugPanel()
|
||||
await u.updateCamPosition([452, -152, 1166])
|
||||
|
@ -1,20 +1,9 @@
|
||||
import type { ForgeConfig } from '@electron-forge/shared-types'
|
||||
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
|
||||
import { MakerZIP } from '@electron-forge/maker-zip'
|
||||
import { MakerDeb } from '@electron-forge/maker-deb'
|
||||
import { MakerRpm } from '@electron-forge/maker-rpm'
|
||||
import { VitePlugin } from '@electron-forge/plugin-vite'
|
||||
import { MakerWix, MakerWixConfig } from '@electron-forge/maker-wix'
|
||||
import { FusesPlugin } from '@electron-forge/plugin-fuses'
|
||||
import { FuseV1Options, FuseVersion } from '@electron/fuses'
|
||||
import path from 'path'
|
||||
|
||||
interface ExtendedMakerWixConfig extends MakerWixConfig {
|
||||
// see https://github.com/electron/forge/issues/3673
|
||||
// this is an undocumented property of electron-wix-msi
|
||||
associateExtensions?: string
|
||||
}
|
||||
|
||||
const rootDir = process.cwd()
|
||||
|
||||
const config: ForgeConfig = {
|
||||
@ -39,26 +28,7 @@ const config: ForgeConfig = {
|
||||
extendInfo: 'Info.plist', // Information for file associations.
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers: [
|
||||
new MakerSquirrel({
|
||||
setupIcon: path.resolve(rootDir, 'assets', 'icon.ico'),
|
||||
}),
|
||||
new MakerWix({
|
||||
icon: path.resolve(rootDir, 'assets', 'icon.ico'),
|
||||
associateExtensions: 'kcl',
|
||||
} as ExtendedMakerWixConfig),
|
||||
new MakerZIP({}, ['darwin']),
|
||||
new MakerRpm({
|
||||
options: {
|
||||
icon: path.resolve(rootDir, 'assets', 'icon.png'),
|
||||
},
|
||||
}),
|
||||
new MakerDeb({
|
||||
options: {
|
||||
icon: path.resolve(rootDir, 'assets', 'icon.png'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
makers: [],
|
||||
plugins: [
|
||||
new VitePlugin({
|
||||
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
|
||||
|
37
package.json
@ -39,7 +39,6 @@
|
||||
"chokidar": "^4.0.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"decamelize": "^6.0.0",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-updater": "6.3.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html2canvas-pro": "^1.5.8",
|
||||
@ -69,7 +68,7 @@
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"start": "vite --port=3000 --host=0.0.0.0",
|
||||
"start:prod": "vite preview --port=3000",
|
||||
"serve": "vite serve --port=3000",
|
||||
"build": "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && source \"$HOME/.cargo/env\" && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -y && yarn build:wasm && vite build",
|
||||
@ -81,6 +80,7 @@
|
||||
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||
"simpleserver:ci": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||
"simpleserver:bg": "yarn pretest && http-server ./public --cors -p 3000 &",
|
||||
"simpleserver:stop": "kill-port 3000",
|
||||
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e ./packages",
|
||||
"fetch:wasm": "./get-latest-wasm-bundle.sh",
|
||||
@ -95,14 +95,14 @@
|
||||
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
|
||||
"files:set-notes": "./scripts/set-files-notes.sh",
|
||||
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
|
||||
"files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh",
|
||||
"files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly",
|
||||
"postinstall": "yarn fetch:samples && yarn xstate:typegen && ./node_modules/.bin/electron-rebuild",
|
||||
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\"",
|
||||
"make:dev": "make dev",
|
||||
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
|
||||
"tron:start": "electron-forge start",
|
||||
"tron:package": "electron-forge package",
|
||||
"tron:make": "electron-forge make",
|
||||
"tron:publish": "electron-forge publish",
|
||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep=@electron",
|
||||
"tronb:vite": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
|
||||
"tronb:package": "electron-builder --config electron-builder.yml",
|
||||
@ -145,19 +145,13 @@
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@electron-forge/cli": "^7.4.0",
|
||||
"@electron-forge/maker-deb": "^7.4.0",
|
||||
"@electron-forge/maker-rpm": "^7.4.0",
|
||||
"@electron-forge/maker-squirrel": "^7.4.0",
|
||||
"@electron-forge/maker-wix": "^7.5.0",
|
||||
"@electron-forge/maker-zip": "^7.5.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
|
||||
"@electron-forge/plugin-fuses": "^7.4.0",
|
||||
"@electron-forge/plugin-vite": "^7.4.0",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@electron/rebuild": "^3.6.0",
|
||||
"@electron-forge/cli": "7.4.0",
|
||||
"@electron-forge/plugin-fuses": "7.4.0",
|
||||
"@electron-forge/plugin-vite": "7.4.0",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@lezer/generator": "^1.7.1",
|
||||
"@nabla/vite-plugin-eslint": "^2.0.5",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
@ -170,7 +164,7 @@
|
||||
"@types/pixelmatch": "^5.2.6",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
"@types/three": "^0.163.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
@ -184,15 +178,15 @@
|
||||
"@xstate/cli": "^0.5.17",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"d3-force": "^3.0.0",
|
||||
"electron": "^32.1.2",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"electron": "32.1.2",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-notarize": "1.2.2",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
||||
"happy-dom": "^15.10.2",
|
||||
"happy-dom": "^15.11.7",
|
||||
"http-server": "^14.1.1",
|
||||
"husky": "^9.1.5",
|
||||
"kill-port": "^2.0.1",
|
||||
@ -207,12 +201,11 @@
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-webgl-canvas-mock": "^1.1.0",
|
||||
"wasm-pack": "^0.13.0",
|
||||
"wasm-pack": "^0.13.1",
|
||||
"ws": "^8.17.0",
|
||||
"yarn": "^1.22.22"
|
||||
}
|
||||
|
@ -119,6 +119,11 @@
|
||||
"title": "Pipe and Flange Assembly",
|
||||
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
|
||||
},
|
||||
{
|
||||
"file": "pipe-with-bend.kcl",
|
||||
"title": "Pipe with bend",
|
||||
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
|
||||
},
|
||||
{
|
||||
"file": "poopy-shoe.kcl",
|
||||
"title": "Poopy Shoe",
|
||||
|
5
scripts/get-nightly-changelog.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
echo "## What's Changed"
|
||||
git log ${PREVIOUS_TAG}..HEAD --oneline --pretty=format:%s | grep -v Bump | awk '{print "* "toupper(substr($0,0,1))substr($0,2)}'
|
||||
echo ""
|
||||
echo "**Full Changelog**: https://github.com/KittyCAD/modeling-app/compare/${PREVIOUS_TAG}...${TAG}"
|
11
scripts/invalidate-files-bucket.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
base_dir="/releases/modeling-app"
|
||||
if [[ $1 = "--nightly" ]]; then
|
||||
base_dir="/releases/modeling-app/nightly"
|
||||
fi
|
||||
|
||||
echo "Invalidating json and yml files at $base_dir in the download bucket"
|
||||
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="$base_dir/last_download.json" --async
|
||||
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="$base_dir/latest-linux-arm64.yml" --async
|
||||
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="$base_dir/latest-mac.yml" --async
|
||||
gcloud compute url-maps invalidate-cdn-cache dl-url-map --path="$base_dir/latest.yml" --async
|
@ -6,7 +6,6 @@ import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { isSingleCursorInPipe } from 'lang/queryAst'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
@ -22,6 +21,7 @@ import {
|
||||
} from 'lib/toolbar'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { isCursorInFunctionDefinition } from 'lang/queryAst'
|
||||
|
||||
export function Toolbar({
|
||||
className = '',
|
||||
@ -38,7 +38,12 @@ export function Toolbar({
|
||||
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
|
||||
|
||||
const sketchPathId = useMemo(() => {
|
||||
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
|
||||
if (
|
||||
isCursorInFunctionDefinition(
|
||||
kclManager.ast,
|
||||
context.selectionRanges.graphSelections[0]
|
||||
)
|
||||
)
|
||||
return false
|
||||
return isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactGraph,
|
||||
|
@ -105,7 +105,7 @@ export class CameraControls {
|
||||
pendingZoom: number | null = null
|
||||
pendingRotation: Vector2 | null = null
|
||||
pendingPan: Vector2 | null = null
|
||||
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
|
||||
interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo
|
||||
isFovAnimationInProgress = false
|
||||
perspectiveFovBeforeOrtho = 45
|
||||
get isPerspective() {
|
||||
@ -155,7 +155,6 @@ export class CameraControls {
|
||||
this.camera.zoom = camProps.zoom || 1
|
||||
}
|
||||
this.camera.updateProjectionMatrix()
|
||||
console.log('doing this thing', camProps)
|
||||
this.update(true)
|
||||
}
|
||||
|
||||
@ -273,14 +272,26 @@ export class CameraControls {
|
||||
camSettings.center.y,
|
||||
camSettings.center.z
|
||||
)
|
||||
const quat = new Quaternion(
|
||||
const orientation = new Quaternion(
|
||||
camSettings.orientation.x,
|
||||
camSettings.orientation.y,
|
||||
camSettings.orientation.z,
|
||||
camSettings.orientation.w
|
||||
).invert()
|
||||
|
||||
this.camera.up.copy(new Vector3(0, 1, 0).applyQuaternion(quat))
|
||||
const newUp = new Vector3(
|
||||
camSettings.up.x,
|
||||
camSettings.up.y,
|
||||
camSettings.up.z
|
||||
)
|
||||
this.camera.quaternion.set(
|
||||
orientation.x,
|
||||
orientation.y,
|
||||
orientation.z,
|
||||
orientation.w
|
||||
)
|
||||
this.camera.up.copy(newUp)
|
||||
this.camera.updateProjectionMatrix()
|
||||
if (this.camera instanceof PerspectiveCamera && camSettings.ortho) {
|
||||
this.useOrthographicCamera()
|
||||
}
|
||||
@ -1164,7 +1175,7 @@ export class CameraControls {
|
||||
this.camera.updateProjectionMatrix()
|
||||
}
|
||||
|
||||
if (this.syncDirection === 'clientToEngine' || forceUpdate)
|
||||
if (this.syncDirection === 'clientToEngine' || forceUpdate) {
|
||||
this.throttledUpdateEngineCamera({
|
||||
quaternion: this.camera.quaternion,
|
||||
position: this.camera.position,
|
||||
@ -1172,6 +1183,7 @@ export class CameraControls {
|
||||
isPerspective: this.isPerspective,
|
||||
target: this.target,
|
||||
})
|
||||
}
|
||||
this.deferReactUpdate(this.reactCameraProperties)
|
||||
Object.values(this._camChangeCallbacks).forEach((cb) => cb())
|
||||
}
|
||||
|
@ -29,6 +29,9 @@ import {
|
||||
Expr,
|
||||
parse,
|
||||
recast,
|
||||
defaultSourceRange,
|
||||
resultIsOk,
|
||||
ProgramMemory,
|
||||
} from 'lang/wasm'
|
||||
import { CustomIcon, CustomIconName } from 'components/CustomIcon'
|
||||
import { ConstrainInfo } from 'lang/std/stdTypes'
|
||||
@ -412,14 +415,15 @@ export async function deleteSegment({
|
||||
if (err(modifiedAst)) return Promise.reject(modifiedAst)
|
||||
|
||||
const newCode = recast(modifiedAst)
|
||||
modifiedAst = parse(newCode)
|
||||
if (err(modifiedAst)) return Promise.reject(modifiedAst)
|
||||
const pResult = parse(newCode)
|
||||
if (err(pResult) || !resultIsOk(pResult)) return Promise.reject(pResult)
|
||||
modifiedAst = pResult.program
|
||||
|
||||
const testExecute = await executeAst({
|
||||
ast: modifiedAst,
|
||||
idGenerator: kclManager.execState.idGenerator,
|
||||
useFakeExecutor: true,
|
||||
engineCommandManager: engineCommandManager,
|
||||
// We make sure to send an empty program memory to denote we mean mock mode.
|
||||
programMemoryOverride: ProgramMemory.empty(),
|
||||
})
|
||||
if (testExecute.errors.length) {
|
||||
toast.error('Segment tag used outside of current Sketch. Could not delete.')
|
||||
@ -429,6 +433,8 @@ export async function deleteSegment({
|
||||
if (!sketchDetails) return
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
pathToNode,
|
||||
sketchDetails.sketchNodePaths,
|
||||
sketchDetails.planeNodePath,
|
||||
modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
@ -501,7 +507,8 @@ const ConstraintSymbol = ({
|
||||
constrainInfo: ConstrainInfo
|
||||
verticalPosition: 'top' | 'bottom'
|
||||
}) => {
|
||||
const { context, send } = useModelingContext()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { context } = useModelingContext()
|
||||
const varNameMap: {
|
||||
[key in ConstrainInfo['type']]: {
|
||||
varName: string
|
||||
@ -590,7 +597,9 @@ const ConstraintSymbol = ({
|
||||
if (err(_node)) return
|
||||
const node = _node.node
|
||||
|
||||
const range: SourceRange = node ? [node.start, node.end] : [0, 0]
|
||||
const range: SourceRange = node
|
||||
? [node.start, node.end, true]
|
||||
: defaultSourceRange()
|
||||
|
||||
if (_type === 'intersectionTag') return null
|
||||
|
||||
@ -612,25 +621,34 @@ const ConstraintSymbol = ({
|
||||
editorManager.setHighlightRange([range])
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
editorManager.setHighlightRange([defaultSourceRange()])
|
||||
}}
|
||||
// disabled={isConstrained || !convertToVarEnabled}
|
||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||
onClick={toSync(async () => {
|
||||
if (!isConstrained) {
|
||||
send({
|
||||
type: 'Convert to variable',
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
pathToNode,
|
||||
variableName: varName,
|
||||
name: 'Constrain with named value',
|
||||
groupId: 'modeling',
|
||||
argDefaultValues: {
|
||||
currentValue: {
|
||||
pathToNode,
|
||||
variableName: varName,
|
||||
valueText: value,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
} else if (isConstrained) {
|
||||
try {
|
||||
const parsed = parse(recast(kclManager.ast))
|
||||
if (trap(parsed)) return Promise.reject(parsed)
|
||||
const pResult = parse(recast(kclManager.ast))
|
||||
if (trap(pResult) || !resultIsOk(pResult))
|
||||
return Promise.reject(pResult)
|
||||
|
||||
const _node1 = getNodeFromPath<CallExpression>(
|
||||
parsed,
|
||||
pResult.program!,
|
||||
pathToNode,
|
||||
'CallExpression',
|
||||
true
|
||||
|
@ -691,19 +691,21 @@ export function createProfileStartHandle({
|
||||
scale = 1,
|
||||
theme,
|
||||
isSelected,
|
||||
size = 12,
|
||||
...rest
|
||||
}: {
|
||||
from: Coords2d
|
||||
scale?: number
|
||||
theme: Themes
|
||||
isSelected?: boolean
|
||||
size?: number
|
||||
} & (
|
||||
| { isDraft: true }
|
||||
| { isDraft: false; id: string; pathToNode: PathToNode }
|
||||
)) {
|
||||
const group = new Group()
|
||||
|
||||
const geometry = new BoxGeometry(12, 12, 12) // in pixels scaled later
|
||||
const geometry = new BoxGeometry(size, size, size) // in pixels scaled later
|
||||
const baseColor = getThemeColorForThreeJs(theme)
|
||||
const color = isSelected ? 0x0000ff : baseColor
|
||||
const body = new MeshBasicMaterial({ color })
|
||||
|
@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { trap } from 'lib/trap'
|
||||
import { codeToIdSelections } from 'lib/selections'
|
||||
import { codeRefFromRange } from 'lang/std/artifactGraph'
|
||||
import { defaultSourceRange } from 'lang/wasm'
|
||||
|
||||
export function AstExplorer() {
|
||||
const { context } = useModelingContext()
|
||||
@ -46,7 +47,7 @@ export function AstExplorer() {
|
||||
<div
|
||||
className="h-full relative"
|
||||
onMouseLeave={(e) => {
|
||||
editorManager.setHighlightRange([[0, 0]])
|
||||
editorManager.setHighlightRange([defaultSourceRange()])
|
||||
}}
|
||||
>
|
||||
<pre className="text-xs">
|
||||
@ -115,15 +116,19 @@ function DisplayObj({
|
||||
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
|
||||
}`}
|
||||
onMouseEnter={(e) => {
|
||||
editorManager.setHighlightRange([[obj?.start || 0, obj.end]])
|
||||
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onMouseMove={(e) => {
|
||||
e.stopPropagation()
|
||||
editorManager.setHighlightRange([[obj?.start || 0, obj.end]])
|
||||
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
|
||||
}}
|
||||
onClick={(e) => {
|
||||
const range: [number, number] = [obj?.start || 0, obj.end || 0]
|
||||
const range: [number, number, boolean] = [
|
||||
obj?.start || 0,
|
||||
obj.end || 0,
|
||||
true,
|
||||
]
|
||||
const idInfo = codeToIdSelections([
|
||||
{ codeRef: codeRefFromRange(range, kclManager.ast) },
|
||||
])[0]
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { parse, BinaryPart, Expr, ProgramMemory } from '../lang/wasm'
|
||||
import {
|
||||
parse,
|
||||
BinaryPart,
|
||||
Expr,
|
||||
ProgramMemory,
|
||||
resultIsOk,
|
||||
} from '../lang/wasm'
|
||||
import {
|
||||
createIdentifier,
|
||||
createLiteral,
|
||||
@ -141,8 +147,9 @@ export function useCalc({
|
||||
useEffect(() => {
|
||||
try {
|
||||
const code = `const __result__ = ${value}`
|
||||
const ast = parse(code)
|
||||
if (trap(ast)) return
|
||||
const pResult = parse(code)
|
||||
if (trap(pResult) || !resultIsOk(pResult)) return
|
||||
const ast = pResult.program
|
||||
const _programMem: ProgramMemory = ProgramMemory.empty()
|
||||
for (const { key, value } of availableVarInfo.variables) {
|
||||
const error = _programMem.set(key, {
|
||||
@ -156,18 +163,17 @@ export function useCalc({
|
||||
executeAst({
|
||||
ast,
|
||||
engineCommandManager,
|
||||
useFakeExecutor: true,
|
||||
// We make sure to send an empty program memory to denote we mean mock mode.
|
||||
programMemoryOverride: kclManager.programMemory.clone(),
|
||||
idGenerator: kclManager.execState.idGenerator,
|
||||
}).then(({ execState }) => {
|
||||
const resultDeclaration = ast.body.find(
|
||||
(a) =>
|
||||
a.type === 'VariableDeclaration' &&
|
||||
a.declarations?.[0]?.id?.name === '__result__'
|
||||
a.declaration.id?.name === '__result__'
|
||||
)
|
||||
const init =
|
||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||
resultDeclaration?.declarations?.[0]?.init
|
||||
resultDeclaration?.declaration.init
|
||||
const result = execState.memory?.get('__result__')?.value
|
||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||
init && setValueNode(init)
|
||||
|
@ -8,11 +8,16 @@ import { getSystemTheme } from 'lib/theme'
|
||||
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
|
||||
import { roundOff } from 'lib/utils'
|
||||
import { varMentions } from 'lib/varCompletionExtension'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import styles from './CommandBarKclInput.module.css'
|
||||
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
|
||||
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
|
||||
import { useSelector } from '@xstate/react'
|
||||
|
||||
const machineContextSelector = (snapshot?: {
|
||||
context: Record<string, unknown>
|
||||
}) => snapshot?.context
|
||||
|
||||
function CommandBarKclInput({
|
||||
arg,
|
||||
@ -31,12 +36,44 @@ function CommandBarKclInput({
|
||||
arg.name
|
||||
] as KclCommandValue | undefined
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const defaultValue = (arg.defaultValue as string) || ''
|
||||
const argMachineContext = useSelector(
|
||||
arg.machineActor,
|
||||
machineContextSelector
|
||||
)
|
||||
const defaultValue = useMemo(
|
||||
() =>
|
||||
arg.defaultValue
|
||||
? arg.defaultValue instanceof Function
|
||||
? arg.defaultValue(commandBarState.context, argMachineContext)
|
||||
: arg.defaultValue
|
||||
: '',
|
||||
[arg.defaultValue, commandBarState.context, argMachineContext]
|
||||
)
|
||||
const initialVariableName = useMemo(() => {
|
||||
// Use the configured variable name if it exists
|
||||
if (arg.variableName !== undefined) {
|
||||
return arg.variableName instanceof Function
|
||||
? arg.variableName(commandBarState.context, argMachineContext)
|
||||
: arg.variableName
|
||||
}
|
||||
// or derive it from the previously set value or the argument name
|
||||
return previouslySetValue && 'variableName' in previouslySetValue
|
||||
? previouslySetValue.variableName
|
||||
: arg.name
|
||||
}, [
|
||||
arg.variableName,
|
||||
commandBarState.context,
|
||||
argMachineContext,
|
||||
arg.name,
|
||||
previouslySetValue,
|
||||
])
|
||||
const [value, setValue] = useState(
|
||||
previouslySetValue?.valueText || defaultValue || ''
|
||||
)
|
||||
const [createNewVariable, setCreateNewVariable] = useState(
|
||||
previouslySetValue && 'variableName' in previouslySetValue
|
||||
(previouslySetValue && 'variableName' in previouslySetValue) ||
|
||||
arg.createVariableByDefault ||
|
||||
false
|
||||
)
|
||||
const [canSubmit, setCanSubmit] = useState(true)
|
||||
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
|
||||
@ -52,10 +89,7 @@ function CommandBarKclInput({
|
||||
isNewVariableNameUnique,
|
||||
} = useCalculateKclExpression({
|
||||
value,
|
||||
initialVariableName:
|
||||
previouslySetValue && 'variableName' in previouslySetValue
|
||||
? previouslySetValue.variableName
|
||||
: arg.name,
|
||||
initialVariableName,
|
||||
})
|
||||
const varMentionData: Completion[] = prevVariables.map((v) => ({
|
||||
label: v.key,
|
||||
|
@ -1,13 +1,23 @@
|
||||
import toast from 'react-hot-toast'
|
||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||
import { RefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
MouseEvent,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { Dialog } from '@headlessui/react'
|
||||
|
||||
interface ContextMenuProps
|
||||
export interface ContextMenuProps
|
||||
extends Omit<React.HTMLAttributes<HTMLUListElement>, 'children'> {
|
||||
items?: React.ReactElement[]
|
||||
menuTargetElement?: RefObject<HTMLElement>
|
||||
guard?: (e: globalThis.MouseEvent) => boolean
|
||||
event?: 'contextmenu' | 'mouseup'
|
||||
}
|
||||
|
||||
const DefaultContextMenuItems = [
|
||||
@ -20,6 +30,8 @@ export function ContextMenu({
|
||||
items = DefaultContextMenuItems,
|
||||
menuTargetElement,
|
||||
className,
|
||||
guard,
|
||||
event = 'contextmenu',
|
||||
...props
|
||||
}: ContextMenuProps) {
|
||||
const dialogRef = useRef<HTMLDivElement>(null)
|
||||
@ -32,6 +44,15 @@ export function ContextMenu({
|
||||
useHotkeys('esc', () => setOpen(false), {
|
||||
enabled: open,
|
||||
})
|
||||
const handleContextMenu = useCallback(
|
||||
(e: globalThis.MouseEvent) => {
|
||||
if (guard && !guard(e)) return
|
||||
e.preventDefault()
|
||||
setPosition({ x: e.clientX, y: e.clientY })
|
||||
setOpen(true)
|
||||
},
|
||||
[guard, setPosition, setOpen]
|
||||
)
|
||||
|
||||
const dialogPositionStyle = useMemo(() => {
|
||||
if (!dialogRef.current)
|
||||
@ -78,21 +99,9 @@ export function ContextMenu({
|
||||
|
||||
// Add context menu listener to target once mounted
|
||||
useEffect(() => {
|
||||
const handleContextMenu = (e: MouseEvent) => {
|
||||
console.log('context menu', e)
|
||||
e.preventDefault()
|
||||
setPosition({ x: e.x, y: e.y })
|
||||
setOpen(true)
|
||||
}
|
||||
menuTargetElement?.current?.addEventListener(
|
||||
'contextmenu',
|
||||
handleContextMenu
|
||||
)
|
||||
menuTargetElement?.current?.addEventListener(event, handleContextMenu)
|
||||
return () => {
|
||||
menuTargetElement?.current?.removeEventListener(
|
||||
'contextmenu',
|
||||
handleContextMenu
|
||||
)
|
||||
menuTargetElement?.current?.removeEventListener(event, handleContextMenu)
|
||||
}
|
||||
}, [menuTargetElement?.current])
|
||||
|
||||
@ -100,7 +109,10 @@ export function ContextMenu({
|
||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||
<div
|
||||
className="fixed inset-0 z-50 w-screen h-screen"
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault()
|
||||
setPosition({ x: e.clientX, y: e.clientY })
|
||||
}}
|
||||
>
|
||||
<Dialog.Backdrop className="fixed z-10 inset-0" />
|
||||
<Dialog.Panel
|
||||
|
@ -266,6 +266,7 @@ const FileTreeItem = ({
|
||||
// Let the lsp servers know we closed a file.
|
||||
onFileClose(currentFile?.path || null, project?.path || null)
|
||||
onFileOpen(fileOrDir.path, project?.path || null)
|
||||
kclManager.switchedFiles = true
|
||||
|
||||
// Open kcl files
|
||||
navigate(`${PATHS.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { SceneInfra } from 'clientSideScene/sceneInfra'
|
||||
import { sceneInfra } from 'lib/singletons'
|
||||
import { MutableRefObject, useEffect, useMemo, useRef } from 'react'
|
||||
import { MutableRefObject, useEffect, useRef } from 'react'
|
||||
import {
|
||||
WebGLRenderer,
|
||||
Scene,
|
||||
@ -19,16 +19,14 @@ import {
|
||||
Intersection,
|
||||
Object3D,
|
||||
} from 'three'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuDivider,
|
||||
ContextMenuItem,
|
||||
ContextMenuItemRefresh,
|
||||
} from './ContextMenu'
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import {
|
||||
useViewControlMenuItems,
|
||||
ViewControlContextMenu,
|
||||
} from './ViewControlMenu'
|
||||
import { AxisNames } from 'lib/constants'
|
||||
|
||||
const CANVAS_SIZE = 80
|
||||
const FRUSTUM_SIZE = 0.5
|
||||
@ -40,64 +38,14 @@ enum AxisColors {
|
||||
Z = '#6689ef',
|
||||
Gray = '#c6c7c2',
|
||||
}
|
||||
enum AxisNames {
|
||||
X = 'x',
|
||||
Y = 'y',
|
||||
Z = 'z',
|
||||
NEG_X = '-x',
|
||||
NEG_Y = '-y',
|
||||
NEG_Z = '-z',
|
||||
}
|
||||
const axisNamesSemantic: Record<AxisNames, string> = {
|
||||
[AxisNames.X]: 'Right',
|
||||
[AxisNames.Y]: 'Back',
|
||||
[AxisNames.Z]: 'Top',
|
||||
[AxisNames.NEG_X]: 'Left',
|
||||
[AxisNames.NEG_Y]: 'Front',
|
||||
[AxisNames.NEG_Z]: 'Bottom',
|
||||
}
|
||||
|
||||
export default function Gizmo() {
|
||||
const menuItems = useViewControlMenuItems()
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null)
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null)
|
||||
const raycasterIntersect = useRef<Intersection<Object3D> | null>(null)
|
||||
const cameraPassiveUpdateTimer = useRef(0)
|
||||
const raycasterPassiveUpdateTimer = useRef(0)
|
||||
const { send: modelingSend } = useModelingContext()
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => (
|
||||
<ContextMenuItem
|
||||
key={axisName}
|
||||
onClick={() => {
|
||||
sceneInfra.camControls
|
||||
.updateCameraToAxis(axisName as AxisNames)
|
||||
.catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
{axisSemantic} view
|
||||
</ContextMenuItem>
|
||||
)),
|
||||
<ContextMenuDivider />,
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||
}}
|
||||
>
|
||||
Reset view
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem
|
||||
onClick={() => {
|
||||
modelingSend({ type: 'Center camera on selection' })
|
||||
}}
|
||||
>
|
||||
Center view on selection
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuDivider />,
|
||||
<ContextMenuItemRefresh />,
|
||||
],
|
||||
[axisNamesSemantic]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return
|
||||
@ -161,7 +109,7 @@ export default function Gizmo() {
|
||||
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm"
|
||||
>
|
||||
<canvas ref={canvasRef} />
|
||||
<ContextMenu menuTargetElement={wrapperRef} items={menuItems} />
|
||||
<ViewControlContextMenu menuTargetElement={wrapperRef} />
|
||||
</div>
|
||||
<GizmoDropdown items={menuItems} />
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { APP_VERSION } from 'routes/Settings'
|
||||
import { APP_VERSION, getReleaseUrl } from 'routes/Settings'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { PATHS } from 'lib/paths'
|
||||
@ -72,10 +72,8 @@ export function LowerRightControls({
|
||||
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
|
||||
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
|
||||
<a
|
||||
onClick={openExternalBrowserIfDesktop(
|
||||
`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
|
||||
)}
|
||||
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
|
||||
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
|
||||
href={getReleaseUrl()}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={'!no-underline font-mono text-xs ' + linkOverrideClassName}
|
||||
|
@ -69,14 +69,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [isKclLspReady, setIsKclLspReady] = useState(false)
|
||||
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
|
||||
|
||||
const {
|
||||
auth,
|
||||
settings: {
|
||||
context: {
|
||||
modeling: { defaultUnit },
|
||||
},
|
||||
},
|
||||
} = useSettingsAuthContext()
|
||||
const { auth } = useSettingsAuthContext()
|
||||
const token = auth?.context.token
|
||||
const navigate = useNavigate()
|
||||
|
||||
@ -92,7 +85,6 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const initEvent: KclWorkerOptions = {
|
||||
wasmUrl: wasmUrl(),
|
||||
token: token,
|
||||
baseUnit: defaultUnit.current,
|
||||
apiBaseUrl: VITE_KC_API_BASE_URL,
|
||||
}
|
||||
lspWorker.postMessage({
|
||||
|
@ -24,7 +24,7 @@ import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import {
|
||||
isCursorInSketchCommandRange,
|
||||
updatePathToNodeFromMap,
|
||||
updateSketchDetailsNodePaths,
|
||||
} from 'lang/util'
|
||||
import {
|
||||
kclManager,
|
||||
@ -41,7 +41,10 @@ import {
|
||||
angleBetweenInfo,
|
||||
applyConstraintAngleBetween,
|
||||
} from './Toolbar/SetAngleBetween'
|
||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
||||
import {
|
||||
applyConstraintAngleLength,
|
||||
applyConstraintLength,
|
||||
} from './Toolbar/setAngleLength'
|
||||
import {
|
||||
canSweepSelection,
|
||||
handleSelectionBatch,
|
||||
@ -50,6 +53,9 @@ import {
|
||||
isSketchPipe,
|
||||
Selections,
|
||||
updateSelections,
|
||||
canLoftSelection,
|
||||
canRevolveSelection,
|
||||
canShellSelection,
|
||||
} from 'lib/selections'
|
||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||
@ -61,16 +67,28 @@ import {
|
||||
getSketchOrientationDetails,
|
||||
} from 'clientSideScene/sceneEntities'
|
||||
import {
|
||||
moveValueIntoNewVariablePath,
|
||||
insertNamedConstant,
|
||||
replaceValueAtNodePath,
|
||||
sketchOnExtrudedFace,
|
||||
sketchOnOffsetPlane,
|
||||
splitPipedProfile,
|
||||
startSketchOnDefault,
|
||||
} from 'lang/modifyAst'
|
||||
import { Program, parse, recast } from 'lang/wasm'
|
||||
import {
|
||||
PathToNode,
|
||||
Program,
|
||||
VariableDeclaration,
|
||||
parse,
|
||||
recast,
|
||||
resultIsOk,
|
||||
} from 'lang/wasm'
|
||||
import {
|
||||
doesSceneHaveExtrudedSketch,
|
||||
doesSceneHaveSweepableSketch,
|
||||
getNodePathFromSourceRange,
|
||||
isSingleCursorInPipe,
|
||||
doesSketchPipeNeedSplitting,
|
||||
getNodeFromPath,
|
||||
isCursorInFunctionDefinition,
|
||||
traverse,
|
||||
} from 'lang/queryAst'
|
||||
import { exportFromEngine } from 'lib/exportFromEngine'
|
||||
import { Models } from '@kittycad/lib/dist/types/src'
|
||||
@ -78,11 +96,10 @@ import toast from 'react-hot-toast'
|
||||
import { EditorSelection, Transaction } from '@codemirror/state'
|
||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
||||
import { err, reportRejection, trap } from 'lib/trap'
|
||||
import { err, reportRejection, trap, reject } from 'lib/trap'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { modelingMachineEvent } from 'editor/manager'
|
||||
import { hasValidEdgeTreatmentSelection } from 'lang/modifyAst/addFillet'
|
||||
import { hasValidEdgeTreatmentSelection } from 'lang/modifyAst/addEdgeTreatment'
|
||||
import {
|
||||
ExportIntent,
|
||||
EngineConnectionStateType,
|
||||
@ -93,6 +110,10 @@ import { useFileContext } from 'hooks/useFileContext'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { IndexLoaderData } from 'lib/types'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import {
|
||||
getPathsFromArtifact,
|
||||
getPlaneFromArtifact,
|
||||
} from 'lang/std/artifactGraph'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -283,7 +304,7 @@ export const ModelingMachineProvider = ({
|
||||
return {
|
||||
sketchDetails: {
|
||||
...sketchDetails,
|
||||
sketchPathToNode: event.data,
|
||||
sketchEntryNodePath: event.data,
|
||||
},
|
||||
}
|
||||
}),
|
||||
@ -406,9 +427,17 @@ export const ModelingMachineProvider = ({
|
||||
selectionRanges: setSelections.selection,
|
||||
sketchDetails: {
|
||||
...sketchDetails,
|
||||
sketchPathToNode:
|
||||
setSelections.updatedPathToNode ||
|
||||
sketchDetails?.sketchPathToNode ||
|
||||
sketchEntryNodePath:
|
||||
setSelections.updatedSketchEntryNodePath ||
|
||||
sketchDetails?.sketchEntryNodePath ||
|
||||
[],
|
||||
sketchNodePaths:
|
||||
setSelections.updatedSketchNodePaths ||
|
||||
sketchDetails?.sketchNodePaths ||
|
||||
[],
|
||||
planeNodePath:
|
||||
setSelections.updatedPlaneNodePath ||
|
||||
sketchDetails?.planeNodePath ||
|
||||
[],
|
||||
},
|
||||
}
|
||||
@ -569,6 +598,58 @@ export const ModelingMachineProvider = ({
|
||||
if (err(canSweep)) return false
|
||||
return canSweep
|
||||
},
|
||||
'has valid revolve selection': ({ context: { selectionRanges } }) => {
|
||||
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||
const hasNoSelection =
|
||||
selectionRanges.graphSelections.length === 0 ||
|
||||
isRangeBetweenCharacters(selectionRanges) ||
|
||||
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||
|
||||
if (hasNoSelection) {
|
||||
// they have no selection, we should enable the button
|
||||
// so they can select the face through the cmdbar
|
||||
// BUT only if there's extrudable geometry
|
||||
return doesSceneHaveSweepableSketch(kclManager.ast)
|
||||
}
|
||||
if (!isSketchPipe(selectionRanges)) return false
|
||||
|
||||
const canSweep = canRevolveSelection(selectionRanges)
|
||||
if (err(canSweep)) return false
|
||||
return canSweep
|
||||
},
|
||||
'has valid loft selection': ({ context: { selectionRanges } }) => {
|
||||
const hasNoSelection =
|
||||
selectionRanges.graphSelections.length === 0 ||
|
||||
isRangeBetweenCharacters(selectionRanges) ||
|
||||
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||
|
||||
if (hasNoSelection) {
|
||||
const count = 2
|
||||
return doesSceneHaveSweepableSketch(kclManager.ast, count)
|
||||
}
|
||||
|
||||
const canLoft = canLoftSelection(selectionRanges)
|
||||
if (err(canLoft)) return false
|
||||
return canLoft
|
||||
},
|
||||
'has valid shell selection': ({
|
||||
context: { selectionRanges },
|
||||
event,
|
||||
}) => {
|
||||
const hasNoSelection =
|
||||
selectionRanges.graphSelections.length === 0 ||
|
||||
isRangeBetweenCharacters(selectionRanges) ||
|
||||
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||
|
||||
if (hasNoSelection) {
|
||||
return doesSceneHaveExtrudedSketch(kclManager.ast)
|
||||
}
|
||||
|
||||
const canShell = canShellSelection(selectionRanges)
|
||||
if (err(canShell)) return false
|
||||
return canShell
|
||||
},
|
||||
'has valid selection for deletion': ({
|
||||
context: { selectionRanges },
|
||||
}) => {
|
||||
@ -588,7 +669,12 @@ export const ModelingMachineProvider = ({
|
||||
'Selection is on face': ({ context: { selectionRanges }, event }) => {
|
||||
if (event.type !== 'Enter sketch') return false
|
||||
if (event.data?.forceNewSketch) return false
|
||||
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
|
||||
if (
|
||||
isCursorInFunctionDefinition(
|
||||
kclManager.ast,
|
||||
selectionRanges.graphSelections[0]
|
||||
)
|
||||
)
|
||||
return false
|
||||
return !!isCursorInSketchCommandRange(
|
||||
engineCommandManager.artifactGraph,
|
||||
@ -596,15 +682,11 @@ export const ModelingMachineProvider = ({
|
||||
)
|
||||
},
|
||||
'Has exportable geometry': () => {
|
||||
if (
|
||||
kclManager.kclErrors.length === 0 &&
|
||||
kclManager.ast.body.length > 0
|
||||
)
|
||||
if (!kclManager.hasErrors() && kclManager.ast.body.length > 0)
|
||||
return true
|
||||
else {
|
||||
let errorMessage = 'Unable to Export '
|
||||
if (kclManager.kclErrors.length > 0)
|
||||
errorMessage += 'due to KCL Errors'
|
||||
if (kclManager.hasErrors()) errorMessage += 'due to KCL Errors'
|
||||
else if (kclManager.ast.body.length === 0)
|
||||
errorMessage += 'due to Empty Scene'
|
||||
console.error(errorMessage)
|
||||
@ -623,10 +705,32 @@ export const ModelingMachineProvider = ({
|
||||
// this assumes no changes have been made to the sketch besides what we did when entering the sketch
|
||||
// i.e. doesn't account for user's adding code themselves, maybe we need store a flag userEditedSinceSketchMode?
|
||||
const newAst = structuredClone(kclManager.ast)
|
||||
const varDecIndex = sketchDetails.sketchPathToNode[1][0]
|
||||
const varDecIndex = sketchDetails.planeNodePath[1][0]
|
||||
|
||||
const varDec = getNodeFromPath<VariableDeclaration>(
|
||||
newAst,
|
||||
sketchDetails.planeNodePath,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(varDec)) return reject(new Error('No varDec'))
|
||||
const variableName = varDec.node.declaration.id.name
|
||||
let isIdentifierUsed = false
|
||||
traverse(newAst, {
|
||||
enter: (node) => {
|
||||
if (
|
||||
node.type === 'Identifier' &&
|
||||
node.name === variableName
|
||||
) {
|
||||
isIdentifierUsed = true
|
||||
}
|
||||
},
|
||||
})
|
||||
if (isIdentifierUsed) return
|
||||
|
||||
// remove body item at varDecIndex
|
||||
newAst.body = newAst.body.filter((_, i) => i !== varDecIndex)
|
||||
await kclManager.executeAstMock(newAst)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(newAst)
|
||||
}
|
||||
sceneInfra.setCallbacks({
|
||||
onClick: () => {},
|
||||
@ -636,7 +740,7 @@ export const ModelingMachineProvider = ({
|
||||
}
|
||||
),
|
||||
'animate-to-face': fromPromise(async ({ input }) => {
|
||||
if (!input) return undefined
|
||||
if (!input) return null
|
||||
if (input.type === 'extrudeFace' || input.type === 'offsetPlane') {
|
||||
const sketched =
|
||||
input.type === 'extrudeFace'
|
||||
@ -663,7 +767,9 @@ export const ModelingMachineProvider = ({
|
||||
await letEngineAnimateAndSyncCamAfter(engineCommandManager, id)
|
||||
sceneInfra.camControls.syncDirection = 'clientToEngine'
|
||||
return {
|
||||
sketchPathToNode: pathToNewSketchNode,
|
||||
sketchEntryNodePath: [],
|
||||
planeNodePath: pathToNewSketchNode,
|
||||
sketchNodePaths: [],
|
||||
zAxis: input.zAxis,
|
||||
yAxis: input.yAxis,
|
||||
origin: input.position,
|
||||
@ -683,7 +789,9 @@ export const ModelingMachineProvider = ({
|
||||
)
|
||||
|
||||
return {
|
||||
sketchPathToNode: pathToNode,
|
||||
sketchEntryNodePath: [],
|
||||
planeNodePath: pathToNode,
|
||||
sketchNodePaths: [],
|
||||
zAxis: input.zAxis,
|
||||
yAxis: input.yAxis,
|
||||
origin: [0, 0, 0],
|
||||
@ -691,12 +799,14 @@ export const ModelingMachineProvider = ({
|
||||
}),
|
||||
'animate-to-sketch': fromPromise(
|
||||
async ({ input: { selectionRanges } }) => {
|
||||
const sourceRange =
|
||||
selectionRanges.graphSelections[0]?.codeRef?.range
|
||||
const sketchPathToNode = getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
sourceRange
|
||||
const sketchPathToNode =
|
||||
selectionRanges.graphSelections[0]?.codeRef?.pathToNode
|
||||
const plane = getPlaneFromArtifact(
|
||||
selectionRanges.graphSelections[0].artifact,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
if (err(plane)) return Promise.reject(plane)
|
||||
|
||||
const info = await getSketchOrientationDetails(
|
||||
sketchPathToNode || []
|
||||
)
|
||||
@ -704,8 +814,17 @@ export const ModelingMachineProvider = ({
|
||||
engineCommandManager,
|
||||
info?.sketchDetails?.faceId || ''
|
||||
)
|
||||
return {
|
||||
const sketchPaths = getPathsFromArtifact({
|
||||
artifact: selectionRanges.graphSelections[0].artifact,
|
||||
sketchPathToNode: sketchPathToNode || [],
|
||||
})
|
||||
if (err(sketchPaths)) return Promise.reject(sketchPaths)
|
||||
if (!plane.codeRef)
|
||||
return Promise.reject(new Error('No plane codeRef'))
|
||||
return {
|
||||
sketchEntryNodePath: sketchPathToNode || [],
|
||||
sketchNodePaths: sketchPaths,
|
||||
planeNodePath: plane.codeRef.pathToNode,
|
||||
zAxis: info.sketchDetails.zAxis || null,
|
||||
yAxis: info.sketchDetails.yAxis || null,
|
||||
origin: info.sketchDetails.origin.map(
|
||||
@ -717,21 +836,35 @@ export const ModelingMachineProvider = ({
|
||||
|
||||
'Get horizontal info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||
await applyConstraintHorzVertDistance({
|
||||
constraint: 'setHorzDistance',
|
||||
selectionRanges,
|
||||
})
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
const pResult = parse(recast(modifiedAst))
|
||||
if (trap(pResult) || !resultIsOk(pResult))
|
||||
return Promise.reject(new Error('Unexpected compilation error'))
|
||||
const _modifiedAst = pResult.program
|
||||
|
||||
if (!sketchDetails)
|
||||
return Promise.reject(new Error('No sketch details'))
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
|
||||
const {
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
} = updateSketchDetailsNodePaths({
|
||||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||
planeNodePath: sketchDetails.planeNodePath,
|
||||
exprInsertIndex,
|
||||
})
|
||||
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
@ -752,27 +885,42 @@ export const ModelingMachineProvider = ({
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
}
|
||||
}
|
||||
),
|
||||
'Get vertical info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||
await applyConstraintHorzVertDistance({
|
||||
constraint: 'setVertDistance',
|
||||
selectionRanges,
|
||||
})
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
const pResult = parse(recast(modifiedAst))
|
||||
if (trap(pResult) || !resultIsOk(pResult))
|
||||
return Promise.reject(new Error('Unexpected compilation error'))
|
||||
const _modifiedAst = pResult.program
|
||||
if (!sketchDetails)
|
||||
return Promise.reject(new Error('No sketch details'))
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
|
||||
const {
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
} = updateSketchDetailsNodePaths({
|
||||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||
planeNodePath: sketchDetails.planeNodePath,
|
||||
exprInsertIndex,
|
||||
})
|
||||
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
@ -793,7 +941,9 @@ export const ModelingMachineProvider = ({
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
}
|
||||
}
|
||||
),
|
||||
@ -803,26 +953,40 @@ export const ModelingMachineProvider = ({
|
||||
selectionRanges,
|
||||
})
|
||||
if (err(info)) return Promise.reject(info)
|
||||
const { modifiedAst, pathToNodeMap } = await (info.enabled
|
||||
? applyConstraintAngleBetween({
|
||||
selectionRanges,
|
||||
})
|
||||
: applyConstraintAngleLength({
|
||||
selectionRanges,
|
||||
angleOrLength: 'setAngle',
|
||||
}))
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||
await (info.enabled
|
||||
? applyConstraintAngleBetween({
|
||||
selectionRanges,
|
||||
})
|
||||
: applyConstraintAngleLength({
|
||||
selectionRanges,
|
||||
angleOrLength: 'setAngle',
|
||||
}))
|
||||
const pResult = parse(recast(modifiedAst))
|
||||
if (trap(pResult) || !resultIsOk(pResult))
|
||||
return Promise.reject(new Error('Unexpected compilation error'))
|
||||
const _modifiedAst = pResult.program
|
||||
if (err(_modifiedAst)) return Promise.reject(_modifiedAst)
|
||||
|
||||
if (!sketchDetails)
|
||||
return Promise.reject(new Error('No sketch details'))
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
|
||||
const {
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
} = updateSketchDetailsNodePaths({
|
||||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||
planeNodePath: sketchDetails.planeNodePath,
|
||||
exprInsertIndex,
|
||||
})
|
||||
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
@ -843,26 +1007,47 @@ export const ModelingMachineProvider = ({
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
}
|
||||
}
|
||||
),
|
||||
'Get length info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
await applyConstraintAngleLength({
|
||||
selectionRanges,
|
||||
})
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
astConstrainLength: fromPromise(
|
||||
async ({
|
||||
input: { selectionRanges, sketchDetails, lengthValue },
|
||||
}) => {
|
||||
if (!lengthValue)
|
||||
return Promise.reject(new Error('No length value'))
|
||||
const constraintResult = await applyConstraintLength({
|
||||
selectionRanges,
|
||||
length: lengthValue,
|
||||
})
|
||||
if (err(constraintResult)) return Promise.reject(constraintResult)
|
||||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||
constraintResult
|
||||
const pResult = parse(recast(modifiedAst))
|
||||
if (trap(pResult) || !resultIsOk(pResult))
|
||||
return Promise.reject(new Error('Unexpected compilation error'))
|
||||
const _modifiedAst = pResult.program
|
||||
if (!sketchDetails)
|
||||
return Promise.reject(new Error('No sketch details'))
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
|
||||
const {
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
} = updateSketchDetailsNodePaths({
|
||||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||
planeNodePath: sketchDetails.planeNodePath,
|
||||
exprInsertIndex,
|
||||
})
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
@ -883,26 +1068,40 @@ export const ModelingMachineProvider = ({
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
}
|
||||
}
|
||||
),
|
||||
'Get perpendicular distance info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||
await applyConstraintIntersect({
|
||||
selectionRanges,
|
||||
})
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
const pResult = parse(recast(modifiedAst))
|
||||
if (trap(pResult) || !resultIsOk(pResult))
|
||||
return Promise.reject(new Error('Unexpected compilation error'))
|
||||
const _modifiedAst = pResult.program
|
||||
if (!sketchDetails)
|
||||
return Promise.reject(new Error('No sketch details'))
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
|
||||
const {
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
} = updateSketchDetailsNodePaths({
|
||||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||
planeNodePath: sketchDetails.planeNodePath,
|
||||
exprInsertIndex,
|
||||
})
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
@ -923,27 +1122,41 @@ export const ModelingMachineProvider = ({
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
}
|
||||
}
|
||||
),
|
||||
'Get ABS X info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||
await applyConstraintAbsDistance({
|
||||
constraint: 'xAbs',
|
||||
selectionRanges,
|
||||
})
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
const pResult = parse(recast(modifiedAst))
|
||||
if (trap(pResult) || !resultIsOk(pResult))
|
||||
return Promise.reject(new Error('Unexpected compilation error'))
|
||||
const _modifiedAst = pResult.program
|
||||
if (!sketchDetails)
|
||||
return Promise.reject(new Error('No sketch details'))
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
|
||||
const {
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
} = updateSketchDetailsNodePaths({
|
||||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||
planeNodePath: sketchDetails.planeNodePath,
|
||||
exprInsertIndex,
|
||||
})
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
@ -964,27 +1177,41 @@ export const ModelingMachineProvider = ({
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
}
|
||||
}
|
||||
),
|
||||
'Get ABS Y info': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails } }) => {
|
||||
const { modifiedAst, pathToNodeMap } =
|
||||
const { modifiedAst, pathToNodeMap, exprInsertIndex } =
|
||||
await applyConstraintAbsDistance({
|
||||
constraint: 'yAbs',
|
||||
selectionRanges,
|
||||
})
|
||||
const _modifiedAst = parse(recast(modifiedAst))
|
||||
const pResult = parse(recast(modifiedAst))
|
||||
if (trap(pResult) || !resultIsOk(pResult))
|
||||
return Promise.reject(new Error('Unexpected compilation error'))
|
||||
const _modifiedAst = pResult.program
|
||||
if (!sketchDetails)
|
||||
return Promise.reject(new Error('No sketch details'))
|
||||
const updatedPathToNode = updatePathToNodeFromMap(
|
||||
sketchDetails.sketchPathToNode,
|
||||
pathToNodeMap
|
||||
)
|
||||
|
||||
const {
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
} = updateSketchDetailsNodePaths({
|
||||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||
planeNodePath: sketchDetails.planeNodePath,
|
||||
exprInsertIndex,
|
||||
})
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
_modifiedAst,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
@ -1005,37 +1232,109 @@ export const ModelingMachineProvider = ({
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection,
|
||||
updatedPathToNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
}
|
||||
}
|
||||
),
|
||||
'Get convert to variable info': fromPromise(
|
||||
'Apply named value constraint': fromPromise(
|
||||
async ({ input: { selectionRanges, sketchDetails, data } }) => {
|
||||
if (!sketchDetails)
|
||||
if (!sketchDetails) {
|
||||
return Promise.reject(new Error('No sketch details'))
|
||||
const { variableName } = await getVarNameModal({
|
||||
valueName: data?.variableName || 'var',
|
||||
})
|
||||
let parsed = parse(recast(kclManager.ast))
|
||||
if (trap(parsed)) return Promise.reject(parsed)
|
||||
parsed = parsed as Node<Program>
|
||||
}
|
||||
if (!data) {
|
||||
return Promise.reject(new Error('No data from command flow'))
|
||||
}
|
||||
let pResult = parse(recast(kclManager.ast))
|
||||
if (trap(pResult) || !resultIsOk(pResult))
|
||||
return Promise.reject(new Error('Unexpected compilation error'))
|
||||
let parsed = pResult.program
|
||||
|
||||
const { modifiedAst: _modifiedAst, pathToReplacedNode } =
|
||||
moveValueIntoNewVariablePath(
|
||||
parsed,
|
||||
kclManager.programMemory,
|
||||
data?.pathToNode || [],
|
||||
variableName
|
||||
let result: {
|
||||
modifiedAst: Node<Program>
|
||||
pathToReplaced: PathToNode | null
|
||||
exprInsertIndex: number
|
||||
} = {
|
||||
modifiedAst: parsed,
|
||||
pathToReplaced: null,
|
||||
exprInsertIndex: -1,
|
||||
}
|
||||
// If the user provided a constant name,
|
||||
// we need to insert the named constant
|
||||
// and then replace the node with the constant's name.
|
||||
if ('variableName' in data.namedValue) {
|
||||
const astAfterReplacement = replaceValueAtNodePath({
|
||||
ast: parsed,
|
||||
pathToNode: data.currentValue.pathToNode,
|
||||
newExpressionString: data.namedValue.variableName,
|
||||
})
|
||||
if (trap(astAfterReplacement)) {
|
||||
return Promise.reject(astAfterReplacement)
|
||||
}
|
||||
const parseResultAfterInsertion = parse(
|
||||
recast(
|
||||
insertNamedConstant({
|
||||
node: astAfterReplacement.modifiedAst,
|
||||
newExpression: data.namedValue,
|
||||
})
|
||||
)
|
||||
)
|
||||
parsed = parse(recast(_modifiedAst))
|
||||
if (
|
||||
trap(parseResultAfterInsertion) ||
|
||||
!resultIsOk(parseResultAfterInsertion)
|
||||
)
|
||||
return Promise.reject(parseResultAfterInsertion)
|
||||
result = {
|
||||
modifiedAst: parseResultAfterInsertion.program,
|
||||
pathToReplaced: astAfterReplacement.pathToReplaced,
|
||||
exprInsertIndex: astAfterReplacement.exprInsertIndex,
|
||||
}
|
||||
} else if ('valueText' in data.namedValue) {
|
||||
// If they didn't provide a constant name,
|
||||
// just replace the node with the value.
|
||||
const astAfterReplacement = replaceValueAtNodePath({
|
||||
ast: parsed,
|
||||
pathToNode: data.currentValue.pathToNode,
|
||||
newExpressionString: data.namedValue.valueText,
|
||||
})
|
||||
if (trap(astAfterReplacement)) {
|
||||
return Promise.reject(astAfterReplacement)
|
||||
}
|
||||
// The `replacer` function returns a pathToNode that assumes
|
||||
// an identifier is also being inserted into the AST, creating an off-by-one error.
|
||||
// This corrects that error, but TODO we should fix this upstream
|
||||
// to avoid this kind of error in the future.
|
||||
astAfterReplacement.pathToReplaced[1][0] =
|
||||
(astAfterReplacement.pathToReplaced[1][0] as number) - 1
|
||||
result = astAfterReplacement
|
||||
}
|
||||
|
||||
pResult = parse(recast(result.modifiedAst))
|
||||
if (trap(pResult) || !resultIsOk(pResult))
|
||||
return Promise.reject(new Error('Unexpected compilation error'))
|
||||
parsed = pResult.program
|
||||
|
||||
if (trap(parsed)) return Promise.reject(parsed)
|
||||
parsed = parsed as Node<Program>
|
||||
if (!pathToReplacedNode)
|
||||
if (!result.pathToReplaced)
|
||||
return Promise.reject(new Error('No path to replaced node'))
|
||||
const {
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
} = updateSketchDetailsNodePaths({
|
||||
sketchEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||
planeNodePath: sketchDetails.planeNodePath,
|
||||
exprInsertIndex: result.exprInsertIndex,
|
||||
})
|
||||
|
||||
const updatedAst =
|
||||
await sceneEntitiesManager.updateAstAndRejigSketch(
|
||||
pathToReplacedNode || [],
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
parsed,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
@ -1048,7 +1347,7 @@ export const ModelingMachineProvider = ({
|
||||
)
|
||||
|
||||
const selection = updateSelections(
|
||||
{ 0: pathToReplacedNode },
|
||||
{ 0: result.pathToReplaced },
|
||||
selectionRanges,
|
||||
updatedAst.newAst
|
||||
)
|
||||
@ -1056,7 +1355,140 @@ export const ModelingMachineProvider = ({
|
||||
return {
|
||||
selectionType: 'completeSelection',
|
||||
selection,
|
||||
updatedPathToNode: pathToReplacedNode,
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
updatedPlaneNodePath,
|
||||
}
|
||||
}
|
||||
),
|
||||
'set-up-draft-circle': fromPromise(
|
||||
async ({ input: { sketchDetails, data } }) => {
|
||||
if (!sketchDetails || !data)
|
||||
return reject('No sketch details or data')
|
||||
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||
|
||||
const result = await sceneEntitiesManager.setupDraftCircle(
|
||||
sketchDetails.sketchEntryNodePath,
|
||||
sketchDetails.sketchNodePaths,
|
||||
sketchDetails.planeNodePath,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin,
|
||||
data
|
||||
)
|
||||
if (err(result)) return reject(result)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||
|
||||
return result
|
||||
}
|
||||
),
|
||||
'set-up-draft-rectangle': fromPromise(
|
||||
async ({ input: { sketchDetails, data } }) => {
|
||||
if (!sketchDetails || !data)
|
||||
return reject('No sketch details or data')
|
||||
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||
|
||||
const result = await sceneEntitiesManager.setupDraftRectangle(
|
||||
sketchDetails.sketchEntryNodePath,
|
||||
sketchDetails.sketchNodePaths,
|
||||
sketchDetails.planeNodePath,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin,
|
||||
data
|
||||
)
|
||||
if (err(result)) return reject(result)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||
|
||||
return result
|
||||
}
|
||||
),
|
||||
'set-up-draft-center-rectangle': fromPromise(
|
||||
async ({ input: { sketchDetails, data } }) => {
|
||||
if (!sketchDetails || !data)
|
||||
return reject('No sketch details or data')
|
||||
await sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||
const result = await sceneEntitiesManager.setupDraftCenterRectangle(
|
||||
sketchDetails.sketchEntryNodePath,
|
||||
sketchDetails.sketchNodePaths,
|
||||
sketchDetails.planeNodePath,
|
||||
sketchDetails.zAxis,
|
||||
sketchDetails.yAxis,
|
||||
sketchDetails.origin,
|
||||
data
|
||||
)
|
||||
if (err(result)) return reject(result)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(kclManager.ast)
|
||||
|
||||
return result
|
||||
}
|
||||
),
|
||||
'setup-client-side-sketch-segments': fromPromise(
|
||||
async ({ input: { sketchDetails, selectionRanges } }) => {
|
||||
if (!sketchDetails) return
|
||||
if (!sketchDetails.sketchEntryNodePath.length) return
|
||||
if (Object.keys(sceneEntitiesManager.activeSegments).length > 0) {
|
||||
sceneEntitiesManager.tearDownSketch({ removeAxis: false })
|
||||
}
|
||||
sceneInfra.resetMouseListeners()
|
||||
await sceneEntitiesManager.setupSketch({
|
||||
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
|
||||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||
forward: sketchDetails.zAxis,
|
||||
up: sketchDetails.yAxis,
|
||||
position: sketchDetails.origin,
|
||||
maybeModdedAst: kclManager.ast,
|
||||
selectionRanges,
|
||||
})
|
||||
sceneInfra.resetMouseListeners()
|
||||
|
||||
sceneEntitiesManager.setupSketchIdleCallbacks({
|
||||
sketchEntryNodePath: sketchDetails?.sketchEntryNodePath || [],
|
||||
forward: sketchDetails.zAxis,
|
||||
up: sketchDetails.yAxis,
|
||||
position: sketchDetails.origin,
|
||||
sketchNodePaths: sketchDetails.sketchNodePaths,
|
||||
planeNodePath: sketchDetails.planeNodePath,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
),
|
||||
'split-sketch-pipe-if-needed': fromPromise(
|
||||
async ({ input: { sketchDetails } }) => {
|
||||
if (!sketchDetails) return reject('No sketch details')
|
||||
const existingSketchInfoNoOp = {
|
||||
updatedEntryNodePath: sketchDetails.sketchEntryNodePath,
|
||||
updatedSketchNodePaths: sketchDetails.sketchNodePaths,
|
||||
updatedPlaneNodePath: sketchDetails.planeNodePath,
|
||||
} as const
|
||||
if (
|
||||
!sketchDetails.sketchNodePaths.length &&
|
||||
sketchDetails.planeNodePath.length
|
||||
) {
|
||||
// new sketch, no profiles yet
|
||||
return existingSketchInfoNoOp
|
||||
}
|
||||
const doesNeedSplitting = doesSketchPipeNeedSplitting(
|
||||
kclManager.ast,
|
||||
sketchDetails.sketchEntryNodePath
|
||||
)
|
||||
if (err(doesNeedSplitting)) return reject(doesNeedSplitting)
|
||||
if (!doesNeedSplitting) return existingSketchInfoNoOp
|
||||
|
||||
const splitResult = splitPipedProfile(
|
||||
kclManager.ast,
|
||||
sketchDetails.sketchEntryNodePath
|
||||
)
|
||||
if (err(splitResult)) return reject(splitResult)
|
||||
|
||||
await kclManager.executeAstMock(splitResult.modifiedAst)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||
splitResult.modifiedAst
|
||||
)
|
||||
return {
|
||||
updatedEntryNodePath: splitResult.pathToProfile,
|
||||
updatedSketchNodePaths: [splitResult.pathToProfile],
|
||||
updatedPlaneNodePath: sketchDetails.planeNodePath,
|
||||
}
|
||||
}
|
||||
),
|
||||
|
@ -6,6 +6,7 @@ import Tooltip from 'components/Tooltip'
|
||||
import { CustomIconName } from 'components/CustomIcon'
|
||||
import { IconDefinition } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ActionIcon } from 'components/ActionIcon'
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
|
||||
export interface ModelingPaneProps {
|
||||
id: string
|
||||
@ -70,13 +71,13 @@ export const ModelingPane = ({
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const onboardingStatus = settings.context.app.onboardingStatus
|
||||
const pointerEventsCssClass =
|
||||
onboardingStatus.current === 'camera'
|
||||
onboardingStatus.current === onboardingPaths.CAMERA
|
||||
? 'pointer-events-none '
|
||||
: 'pointer-events-auto '
|
||||
return (
|
||||
<section
|
||||
{...props}
|
||||
title={title && typeof title === 'string' ? title : ''}
|
||||
aria-label={title && typeof title === 'string' ? title : ''}
|
||||
data-testid={detailsTestId}
|
||||
id={id}
|
||||
className={
|
||||
|
@ -40,7 +40,9 @@ export const KclEditorMenu = ({ children }: PropsWithChildren) => {
|
||||
<Menu.Items className="absolute right-0 left-auto w-72 flex flex-col gap-1 divide-y divide-chalkboard-20 dark:divide-chalkboard-70 align-stretch px-0 py-1 bg-chalkboard-10 dark:bg-chalkboard-100 rounded-sm shadow-lg border border-solid border-chalkboard-20/50 dark:border-chalkboard-80/50">
|
||||
<Menu.Item>
|
||||
<button
|
||||
onClick={() => kclManager.format()}
|
||||
onClick={() => {
|
||||
kclManager.format().catch(reportRejection)
|
||||
}}
|
||||
className={styles.button}
|
||||
>
|
||||
<span>Format code</span>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { processMemory } from './MemoryPane'
|
||||
import { enginelessExecutor } from '../../../lib/testHelpers'
|
||||
import { initPromise, parse, ProgramMemory } from '../../../lang/wasm'
|
||||
import { assertParse, initPromise, ProgramMemory } from '../../../lang/wasm'
|
||||
|
||||
beforeAll(async () => {
|
||||
await initPromise
|
||||
@ -28,12 +28,16 @@ describe('processMemory', () => {
|
||||
|> lineTo([0.98, 5.16], %)
|
||||
|> lineTo([2.15, 4.32], %)
|
||||
// |> rx(90, %)`
|
||||
const ast = parse(code)
|
||||
const ast = assertParse(code)
|
||||
const execState = await enginelessExecutor(ast, ProgramMemory.empty())
|
||||
const output = processMemory(execState.memory)
|
||||
expect(output.myVar).toEqual(5)
|
||||
expect(output.otherVar).toEqual(3)
|
||||
expect(output).toEqual({
|
||||
HALF_TURN: 180,
|
||||
QUARTER_TURN: 90,
|
||||
THREE_QUARTER_TURN: 270,
|
||||
ZERO: 0,
|
||||
myVar: 5,
|
||||
myFn: '__function(a)__',
|
||||
otherVar: 3,
|
||||
|
@ -90,7 +90,7 @@ export const sidebarPanes: SidebarPane[] = [
|
||||
keybinding: 'Shift + C',
|
||||
showBadge: {
|
||||
value: ({ kclContext }) => {
|
||||
return kclContext.errors.length
|
||||
return kclContext.diagnostics.length
|
||||
},
|
||||
onClick: (e) => {
|
||||
e.preventDefault()
|
||||
|