Compare commits
47 Commits
achalmers/
...
pierremtb/
Author | SHA1 | Date | |
---|---|---|---|
7c935741e4 | |||
87e299e0bb | |||
465e71c12f | |||
df86c93a04 | |||
824669a1c2 | |||
ba8f8a1722 | |||
f4a4e6c5be | |||
0d148e80aa | |||
3300993ac8 | |||
033eaed32e | |||
8aabac0be7 | |||
138728a95d | |||
9a92e7d642 | |||
efedc8de58 | |||
f7ee248a26 | |||
336f4f27ba | |||
e1f128d64a | |||
f858a611f1 | |||
6ce270c0d0 | |||
30ac0e4f48 | |||
8f90c352fe | |||
bc6f0fceca | |||
5c830a4ed4 | |||
8a9d50226f | |||
8397405998 | |||
f5b8298735 | |||
25ad603502 | |||
86349375d0 | |||
56d861f2cc | |||
3e8ee3ffc4 | |||
a44516bc7e | |||
ce62fe67cf | |||
763a1b6628 | |||
3281e62e6b | |||
f1a458f124 | |||
229433126d | |||
b962b5fcb3 | |||
428d125139 | |||
cffeb52b4b | |||
e0ef10e7bb | |||
7095ce2377 | |||
5b207d7d1a | |||
2fac213c58 | |||
2f72a8ef14 | |||
27ce9f8aa4 | |||
b0426e3f94 | |||
d707c66e53 |
@ -2,8 +2,8 @@ NODE_ENV=development
|
||||
DEV=true
|
||||
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
|
||||
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
|
||||
BASE_URL=https://api.dev.zoo.dev
|
||||
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
|
||||
VITE_KC_SITE_APP_URL=https://app.dev.zoo.dev
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||
# ONLY add your token in .env.development.local if you want to skip auth, otherwise this token takes precedence!
|
||||
|
@ -1,5 +1,8 @@
|
||||
NODE_ENV=production
|
||||
DEV=false
|
||||
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
|
||||
VITE_KC_API_BASE_URL=https://api.zoo.dev
|
||||
VITE_KC_SITE_BASE_URL=https://zoo.dev
|
||||
VITE_KC_SITE_APP_URL=https://app.zoo.dev
|
||||
VITE_KC_SKIP_AUTH=false
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||
|
@ -29,6 +29,13 @@
|
||||
{
|
||||
"name": "isNaN",
|
||||
"message": "Use Number.isNaN() instead."
|
||||
},
|
||||
],
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "CallExpression[callee.object.name='Array'][callee.property.name='isArray']",
|
||||
"message": "Use isArray() in lib/utils.ts instead of Array.isArray()."
|
||||
}
|
||||
],
|
||||
"semi": [
|
||||
|
16
.github/workflows/build-apps.yml
vendored
@ -134,8 +134,6 @@ jobs:
|
||||
max_attempts: 3
|
||||
command: yarn install
|
||||
|
||||
- run: yarn tronb:vite
|
||||
|
||||
- name: Prepare certificate and variables (Windows only)
|
||||
if: ${{ (env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true') && matrix.os == 'windows-2022' }}
|
||||
run: |
|
||||
@ -165,8 +163,8 @@ jobs:
|
||||
- name: Build the app (debug)
|
||||
if: ${{ env.IS_RELEASE == 'false' && env.IS_NIGHTLY == 'false' }}
|
||||
# electron-builder doesn't have a concept of release vs debug,
|
||||
# this is just not doing any codesign or release yml generation
|
||||
run: yarn electron-builder --config
|
||||
# this is just not doing any codesign or release yml generation, and points to dev infra
|
||||
run: yarn tronb:package:dev
|
||||
|
||||
- name: Build the app (release)
|
||||
if: ${{ env.IS_RELEASE == 'true' || env.IS_NIGHTLY == 'true' }}
|
||||
@ -185,7 +183,7 @@ jobs:
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: yarn electron-builder --config --publish always
|
||||
command: yarn tronb:package:prod
|
||||
|
||||
- name: List artifacts in out/
|
||||
run: ls -R out
|
||||
@ -246,7 +244,7 @@ jobs:
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
command: yarn electron-builder --config --publish always
|
||||
command: yarn tronb:package:prod
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ env.IS_RELEASE == 'true' }}
|
||||
@ -390,19 +388,19 @@ jobs:
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
uses: 'google-github-actions/auth@v2.1.7'
|
||||
uses: 'google-github-actions/auth@v2.1.8'
|
||||
with:
|
||||
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||
|
||||
- name: Set up Google Cloud SDK
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
uses: google-github-actions/setup-gcloud@v2.1.2
|
||||
uses: google-github-actions/setup-gcloud@v2.1.4
|
||||
with:
|
||||
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
|
||||
|
||||
- name: Upload nightly files to public bucket
|
||||
if: ${{ env.IS_NIGHTLY == 'true' }}
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.1
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.2
|
||||
with:
|
||||
path: out
|
||||
glob: '*'
|
||||
|
@ -1,4 +1,4 @@
|
||||
name: E2E Tests
|
||||
name: E2E Flow Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
@ -33,7 +33,7 @@ jobs:
|
||||
rust:
|
||||
- 'src/wasm-lib/**'
|
||||
|
||||
electron:
|
||||
flow-tests:
|
||||
timeout-minutes: 60
|
||||
name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }}
|
||||
strategy:
|
||||
@ -47,23 +47,29 @@ jobs:
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- uses: KittyCAD/action-install-cli@main
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: yarn
|
||||
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright/
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
shell: bash
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
- name: Download Wasm Cache
|
||||
id: download-wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
@ -75,29 +81,35 @@ jobs:
|
||||
workflow: build-and-store-wasm.yml
|
||||
branch: main
|
||||
path: src/wasm-lib/pkg
|
||||
|
||||
- name: copy wasm blob
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
shell: bash
|
||||
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
continue-on-error: true
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache Wasm (because rust diff)
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: OR Cache Wasm (because wasm cache failed)
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: install good sed
|
||||
if: ${{ startsWith(matrix.os, 'macos') }}
|
||||
shell: bash
|
||||
run: |
|
||||
brew install gnu-sed
|
||||
echo "/opt/homebrew/opt/gnu-sed/libexec/gnubin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install vector
|
||||
shell: bash
|
||||
# TODO: figure out what to do with this, it's failing
|
||||
@ -115,81 +127,33 @@ jobs:
|
||||
sed -i "s#GH_ACTIONS_AXIOM_TOKEN#${{secrets.GH_ACTIONS_AXIOM_TOKEN}}#g" /tmp/vector.toml
|
||||
cat /tmp/vector.toml
|
||||
${HOME}/.vector/bin/vector --config /tmp/vector.toml &
|
||||
|
||||
- name: Build Wasm (because rust diff)
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
shell: bash
|
||||
run: yarn build:wasm
|
||||
|
||||
- name: OR Build Wasm (because wasm cache failed)
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
shell: bash
|
||||
run: yarn build:wasm
|
||||
- name: build electron
|
||||
|
||||
- name: build web
|
||||
shell: bash
|
||||
run: yarn tron:package
|
||||
# - name: Run ubuntu/chrome snapshots
|
||||
# if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
|
||||
# shell: bash
|
||||
# # TODO: break this in its own job, for now it's not slowing down the overall execution as ubuntu is the quickest,
|
||||
# # but we could do better. This forces a large 1/1 shard of all 20 snapshot tests that runs in about 3 minutes.
|
||||
# run: |
|
||||
# PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot --shard=1/1
|
||||
# env:
|
||||
# CI: true
|
||||
# NODE_ENV: development
|
||||
# VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
# VITE_KC_SKIP_AUTH: true
|
||||
# token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
# snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
with:
|
||||
name: playwright-report-${{ matrix.os }}-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
run: yarn tronb:vite:dev
|
||||
|
||||
- name: Clean up test-results
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
continue-on-error: true
|
||||
run: rm -r test-results
|
||||
- name: check for changes
|
||||
if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }}
|
||||
shell: bash
|
||||
id: git-check
|
||||
run: |
|
||||
git add .
|
||||
if git status | grep -q "Changes to be committed"
|
||||
then echo "modified=true" >> $GITHUB_OUTPUT
|
||||
else echo "modified=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
# - name: Commit changes, if any
|
||||
# if: steps.git-check.outputs.modified == 'true'
|
||||
# shell: bash
|
||||
# run: |
|
||||
# git add .
|
||||
# git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
# git config --local user.name "github-actions[bot]"
|
||||
# git remote set-url origin https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
||||
# git fetch origin
|
||||
# echo ${{ github.head_ref }}
|
||||
# git checkout ${{ github.head_ref }}
|
||||
# git commit -am "A snapshot a day keeps the bugs away! 📷🐛 (OS: ${{matrix.os}})" || true
|
||||
# git push
|
||||
# git push origin ${{ github.head_ref }}
|
||||
# only upload artifacts if there's actually changes
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: steps.git-check.outputs.modified == 'true'
|
||||
with:
|
||||
name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: test-results-${{ matrix.os }}-${{ matrix.shardIndex }}-${{ github.sha }}
|
||||
path: test-results/
|
||||
|
||||
- name: Run playwright/electron flow (with retries)
|
||||
id: retry
|
||||
if: ${{ !cancelled() && (success() || failure()) }}
|
||||
@ -203,6 +167,7 @@ jobs:
|
||||
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
VITE_KC_SKIP_AUTH: true
|
||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
@ -211,6 +176,7 @@ jobs:
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
@ -219,4 +185,3 @@ jobs:
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
145
.github/workflows/e2e-snapshot-tests.yml
vendored
Normal file
@ -0,0 +1,145 @@
|
||||
name: E2E Snapshot Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
check-rust-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
rust-changed: ${{ steps.filter.outputs.rust }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- id: filter
|
||||
name: Check for Rust changes
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
rust:
|
||||
- 'src/wasm-lib/**'
|
||||
|
||||
snapshot-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-rust-changes
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Cache Playwright Browsers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright/
|
||||
key: ${{ runner.os }}-playwright-${{ hashFiles('yarn.lock') }}
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: yarn playwright install --with-deps
|
||||
|
||||
- name: Download Wasm Cache
|
||||
id: download-wasm
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
uses: dawidd6/action-download-artifact@v7
|
||||
continue-on-error: true
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
name: wasm-bundle
|
||||
workflow: build-and-store-wasm.yml
|
||||
branch: main
|
||||
path: src/wasm-lib/pkg
|
||||
|
||||
- name: copy wasm blob
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
||||
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
||||
continue-on-error: true
|
||||
|
||||
- name: Setup Rust
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache Wasm (because rust diff)
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: OR Cache Wasm (because wasm cache failed)
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src/wasm-lib'
|
||||
|
||||
- name: Build Wasm (because rust diff)
|
||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
||||
run: yarn build:wasm
|
||||
|
||||
- name: OR Build Wasm (because wasm cache failed)
|
||||
if: steps.download-wasm.outcome == 'failure'
|
||||
run: yarn build:wasm
|
||||
|
||||
- name: build web
|
||||
run: yarn tronb:vite:dev
|
||||
|
||||
- name: Run chrome snapshots
|
||||
run: |
|
||||
PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: development
|
||||
VITE_KC_DEV_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
VITE_KC_SKIP_AUTH: true
|
||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
|
||||
|
||||
- name: check for changes
|
||||
id: git-check
|
||||
run: |
|
||||
{
|
||||
echo 'changes<<EOF'
|
||||
git diff --name-only e2e/playwright/snapshot-tests.spec.ts-snapshots
|
||||
echo EOF
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# only upload artifacts if there's actually changes
|
||||
- name: Upload changes, if any
|
||||
if: steps.git-check.outputs.changes != ''
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-snapshots-${{ runner.os }}-${{ github.sha }}
|
||||
path: ${{ steps.git-check.outputs.changes }}
|
||||
|
||||
- name: Upload report, if any
|
||||
uses: actions/upload-artifact@v4
|
||||
if: steps.git-check.outputs.changes != ''
|
||||
with:
|
||||
name: playwright-report-${{ runner.os }}-${{ github.sha }}
|
||||
path: playwright-report/
|
||||
include-hidden-files: true
|
||||
retention-days: 30
|
||||
|
||||
- name: Fail the run if we have snapshot updates
|
||||
if: steps.git-check.outputs.changes != ''
|
||||
run: exit 1
|
||||
|
||||
# TODO: check if we could comment on the PR as well
|
6
.github/workflows/publish-apps-release.yml
vendored
@ -108,17 +108,17 @@ jobs:
|
||||
run: yarn files:set-notes
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: 'google-github-actions/auth@v2.1.7'
|
||||
uses: 'google-github-actions/auth@v2.1.8'
|
||||
with:
|
||||
credentials_json: '${{ secrets.GOOGLE_CLOUD_DL_SA }}'
|
||||
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@v2.1.2
|
||||
uses: google-github-actions/setup-gcloud@v2.1.4
|
||||
with:
|
||||
project_id: ${{ env.GOOGLE_CLOUD_PROJECT_ID }}
|
||||
|
||||
- name: Upload release files to public bucket
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.1
|
||||
uses: google-github-actions/upload-cloud-storage@v2.2.2
|
||||
with:
|
||||
path: out
|
||||
glob: '*'
|
||||
|
@ -101,7 +101,7 @@ This will start the application and hot-reload on changes.
|
||||
|
||||
Devtools can be opened with the usual Cmd-Opt-I (Mac) or Ctrl-Shift-I (Linux and Windows).
|
||||
|
||||
To build, run `yarn tron:package`.
|
||||
To build with electron-builder, run `yarn tronb:package:dev` (or `yarn tronb:package:prod` to point to the .env.production variables)
|
||||
|
||||
## Checking out commits / Bisecting
|
||||
|
||||
|
@ -47,21 +47,6 @@ myObj = { a = 0, b = "thing" }
|
||||
We support two different ways of getting properties from objects, you can call
|
||||
`myObj.a` or `myObj["a"]` both work.
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
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) {
|
||||
return x
|
||||
}
|
||||
```
|
||||
|
||||
As you can see above `myFn` just returns whatever it is given.
|
||||
|
||||
|
||||
## Binary expressions
|
||||
|
||||
You can also do math! Let's show an example below:
|
||||
@ -76,6 +61,120 @@ You can nest expressions in parenthesis as well:
|
||||
myMathExpression = 3 + (1 * 2 / (3 - 7))
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
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) {
|
||||
return x
|
||||
}
|
||||
```
|
||||
|
||||
As you can see above `myFn` just returns whatever it is given.
|
||||
|
||||
KCL's early drafts used positional arguments, but we now use keyword arguments. If you declare a
|
||||
function like this:
|
||||
|
||||
```
|
||||
fn add(left, right) {
|
||||
return left + right
|
||||
}
|
||||
```
|
||||
|
||||
You can call it like this:
|
||||
|
||||
```
|
||||
total = add(left = 1, right = 2)
|
||||
```
|
||||
|
||||
Functions can also declare one *unlabeled* arg. If you do want to declare an unlabeled arg, it must
|
||||
be the first arg declared.
|
||||
|
||||
```
|
||||
// The @ indicates an argument can be used without a label.
|
||||
// Note that only the first argument can use @.
|
||||
fn increment(@x) {
|
||||
return x + 1
|
||||
}
|
||||
|
||||
fn add(@x, delta) {
|
||||
return x + delta
|
||||
}
|
||||
|
||||
two = increment(1)
|
||||
three = add(1, delta = 2)
|
||||
```
|
||||
|
||||
## Pipelines
|
||||
|
||||
It can be hard to read repeated function calls, because of all the nested brackets.
|
||||
|
||||
```
|
||||
i = 1
|
||||
x = h(g(f(i)))
|
||||
```
|
||||
|
||||
You can make this easier to read by breaking it into many declarations, but that is a bit annoying.
|
||||
|
||||
```
|
||||
i = 1
|
||||
x0 = f(i)
|
||||
x1 = g(x0)
|
||||
x = h(x1)
|
||||
```
|
||||
|
||||
Instead, you can use the pipeline operator (`|>`) to simplify this.
|
||||
|
||||
Basically, `x |> f(%)` is a shorthand for `f(x)`. The left-hand side of the `|>` gets put into
|
||||
the `%` in the right-hand side.
|
||||
|
||||
So, this means `x |> f(%) |> g(%)` is shorthand for `g(f(x))`. The code example above, with its
|
||||
somewhat-clunky `x0` and `x1` constants could be rewritten as
|
||||
|
||||
```
|
||||
i = 1
|
||||
x = i
|
||||
|> f(%)
|
||||
|> g(%)
|
||||
|> h(%)
|
||||
```
|
||||
|
||||
This helps keep your code neat and avoid unnecessary declarations.
|
||||
|
||||
## Pipelines and keyword arguments
|
||||
|
||||
Say you have a long pipeline of sketch functions, like this:
|
||||
|
||||
```
|
||||
startSketch()
|
||||
|> line(%, end = [3, 4])
|
||||
|> line(%, end = [10, 10])
|
||||
|> line(%, end = [-13, -14])
|
||||
|> close(%)
|
||||
```
|
||||
|
||||
In this example, each function call outputs a sketch, and it gets put into the next function call via
|
||||
the `%`, into the first (unlabeled) argument.
|
||||
|
||||
If a function call uses an unlabeled first parameter, it will default to `%` if it's not given. This
|
||||
means that `|> line(%, end = [3, 4])` and `|> line(end = [3, 4])` are equivalent! So the above
|
||||
could be rewritten as
|
||||
|
||||
```
|
||||
startSketch()
|
||||
|> line(end = [3, 4])
|
||||
|> line(end = [10, 10])
|
||||
|> line(end = [-13, -14])
|
||||
|> close()
|
||||
```
|
||||
|
||||
Note that we are still in the process of migrating KCL's standard library to use keyword arguments. So some
|
||||
functions are still unfortunately using positional arguments. We're moving them over, so keep checking back.
|
||||
Some functions like `angledLine`, `startProfileAt` etc are still using the old positional argument syntax.
|
||||
Check the docs page for each function and look at its examples to see.
|
||||
|
||||
## Tags
|
||||
|
||||
Tags are used to give a name (tag) to a specific path.
|
||||
@ -88,17 +187,17 @@ way:
|
||||
```
|
||||
startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
196.99
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
|> angledLine({angle = 0, length = 191.26}, %, $rectangleSegmentA001)
|
||||
|> angledLine({
|
||||
angle = segAng(rectangleSegmentA001) - 90,
|
||||
length = 196.99,
|
||||
}, %, $rectangleSegmentB001)
|
||||
|> angledLine({
|
||||
angle = segAng(rectangleSegmentA001),
|
||||
length = -segLen(rectangleSegmentA001),
|
||||
}, %, $rectangleSegmentC001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
```
|
||||
|
||||
### Tag Identifier
|
||||
@ -121,17 +220,17 @@ However if the code was written like this:
|
||||
fn rect(origin) {
|
||||
return startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
196.99
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
|> angledLine({angle = 0, length = 191.26}, %, $rectangleSegmentA001)
|
||||
|> angledLine({
|
||||
angle = segAng(rectangleSegmentA001) - 90,
|
||||
length = 196.99
|
||||
}, %, $rectangleSegmentB001)
|
||||
|> angledLine({
|
||||
angle = segAng(rectangleSegmentA001),
|
||||
length = -segLen(rectangleSegmentA001)
|
||||
}, %, $rectangleSegmentC001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
}
|
||||
|
||||
rect([0, 0])
|
||||
@ -149,17 +248,17 @@ For example the following code works.
|
||||
fn rect(origin) {
|
||||
return startSketchOn('XZ')
|
||||
|> startProfileAt(origin, %)
|
||||
|> angledLine([0, 191.26], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
196.99
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
|> angledLine({angle = 0, length = 191.26}, %, $rectangleSegmentA001)
|
||||
|> angledLine({
|
||||
angle = segAng(rectangleSegmentA001) - 90,
|
||||
length = 196.99
|
||||
}, %, $rectangleSegmentB001)
|
||||
|> angledLine({
|
||||
angle = segAng(rectangleSegmentA001),
|
||||
length = -segLen(rectangleSegmentA001)
|
||||
}, %, $rectangleSegmentC001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
}
|
||||
|
||||
rect([0, 0])
|
||||
|
@ -144,7 +144,7 @@ async function doBasicSketch(
|
||||
|> xLine(-segLen(seg01), %)`)
|
||||
}
|
||||
|
||||
test.describe('Basic sketch', () => {
|
||||
test.describe('Basic sketch', { tag: ['@skipWin'] }, () => {
|
||||
test.fixme('code pane open at start', async ({ page, homePage }) => {
|
||||
await doBasicSketch(page, homePage, ['code'])
|
||||
})
|
||||
|
@ -4,7 +4,10 @@ import { getUtils } from './test-utils'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
|
||||
test.describe('Can create sketches on all planes and their back sides', () => {
|
||||
test.describe(
|
||||
'Can create sketches on all planes and their back sides',
|
||||
{ tag: ['@skipWin'] },
|
||||
() => {
|
||||
const sketchOnPlaneAndBackSideTest = async (
|
||||
page: Page,
|
||||
homePage: HomePageFixture,
|
||||
@ -88,11 +91,17 @@ test.describe('Can create sketches on all planes and their back sides', () => {
|
||||
})
|
||||
|
||||
test('YZ', async ({ page, homePage }) => {
|
||||
await sketchOnPlaneAndBackSideTest(page, homePage, 'YZ', { x: 700, y: 250 }) // green plane
|
||||
await sketchOnPlaneAndBackSideTest(page, homePage, 'YZ', {
|
||||
x: 700,
|
||||
y: 250,
|
||||
}) // green plane
|
||||
})
|
||||
|
||||
test('XZ', async ({ page, homePage }) => {
|
||||
await sketchOnPlaneAndBackSideTest(page, homePage, '-XZ', { x: 700, y: 80 }) // blue plane
|
||||
await sketchOnPlaneAndBackSideTest(page, homePage, '-XZ', {
|
||||
x: 700,
|
||||
y: 80,
|
||||
}) // blue plane
|
||||
})
|
||||
|
||||
test('-XY', async ({ page, homePage }) => {
|
||||
@ -110,6 +119,10 @@ test.describe('Can create sketches on all planes and their back sides', () => {
|
||||
})
|
||||
|
||||
test('-XZ', async ({ page, homePage }) => {
|
||||
await sketchOnPlaneAndBackSideTest(page, homePage, 'XZ', { x: 700, y: 427 }) // back of blue plane
|
||||
await sketchOnPlaneAndBackSideTest(page, homePage, 'XZ', {
|
||||
x: 700,
|
||||
y: 427,
|
||||
}) // back of blue plane
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -6,7 +6,7 @@ import { bracket } from 'lib/exampleKcl'
|
||||
import { TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW } from './storageStates'
|
||||
import fsp from 'fs/promises'
|
||||
|
||||
test.describe('Code pane and errors', () => {
|
||||
test.describe('Code pane and errors', { tag: ['@skipWin'] }, () => {
|
||||
test('Typing KCL errors induces a badge on the code pane button', async ({
|
||||
page,
|
||||
homePage,
|
||||
|
@ -4,7 +4,7 @@ import { executorInputPath, getUtils } from './test-utils'
|
||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||
import path from 'path'
|
||||
|
||||
test.describe('Command bar tests', () => {
|
||||
test.describe('Command bar tests', { tag: ['@skipWin'] }, () => {
|
||||
test('Extrude from command bar selects extrude line after', async ({
|
||||
page,
|
||||
homePage,
|
||||
@ -167,10 +167,10 @@ test.describe('Command bar tests', () => {
|
||||
await expect(commandLevelArgButton).toHaveText('level: project')
|
||||
})
|
||||
|
||||
test('Command bar keybinding works from code editor and can change a setting', async ({
|
||||
page,
|
||||
homePage,
|
||||
}) => {
|
||||
test(
|
||||
'Command bar keybinding works from code editor and can change a setting',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ page, homePage }) => {
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
@ -202,10 +202,9 @@ test.describe('Command bar tests', () => {
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
await expect(page.getByRole('option', { name: 'system' })).toHaveAttribute(
|
||||
'data-headlessui-state',
|
||||
'active'
|
||||
)
|
||||
await expect(
|
||||
page.getByRole('option', { name: 'system' })
|
||||
).toHaveAttribute('data-headlessui-state', 'active')
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Check the toast appeared
|
||||
@ -214,7 +213,8 @@ test.describe('Command bar tests', () => {
|
||||
).toBeVisible()
|
||||
// Check that the theme changed
|
||||
await expect(page.locator('body')).not.toHaveClass(`body-bg dark`)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test('Can extrude from the command bar', async ({ page, homePage }) => {
|
||||
await page.addInitScript(async () => {
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
|
||||
import { join } from 'path'
|
||||
|
||||
test.describe('Editor tests', () => {
|
||||
test.describe('Editor tests', { tag: ['@skipWin'] }, () => {
|
||||
test('can comment out code with ctrl+/', async ({ page, homePage }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
@ -966,10 +966,10 @@ test.describe('Editor tests', () => {
|
||||
|> close()`)
|
||||
})
|
||||
|
||||
test('Can undo a sketch modification with ctrl+z', async ({
|
||||
page,
|
||||
homePage,
|
||||
}) => {
|
||||
test(
|
||||
'Can undo a sketch modification with ctrl+z',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ page, homePage }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
@ -1113,7 +1113,8 @@ test.describe('Editor tests', () => {
|
||||
|> tangentialArcTo([24.95, -0.38], %)
|
||||
|> close()
|
||||
|> extrude(length = 5)`)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.fixme(
|
||||
`Can use the import stdlib function on a local OBJ file`,
|
||||
|
@ -19,7 +19,7 @@ test.describe('integrations tests', () => {
|
||||
)
|
||||
})
|
||||
|
||||
const [clickObj] = await scene.makeMouseHelpers(600, 300)
|
||||
const [clickObj] = await scene.makeMouseHelpers(726, 272)
|
||||
|
||||
await test.step('setup test', async () => {
|
||||
await homePage.expectState({
|
||||
@ -61,6 +61,7 @@ test.describe('integrations tests', () => {
|
||||
})
|
||||
await test.step('setup for next assertion', async () => {
|
||||
await toolbar.openFile('main.kcl')
|
||||
await scene.waitForExecutionDone()
|
||||
await clickObj()
|
||||
await scene.moveNoWhere()
|
||||
await editor.expectState({
|
||||
@ -1185,4 +1186,56 @@ test.describe('Undo and redo do not keep history when navigating between files',
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`cloned file has an incremented name and same contents`,
|
||||
{ tag: '@electron' },
|
||||
async ({ page, context, homePage }, testInfo) => {
|
||||
const { panesOpen, createNewFile, cloneFile } = await getUtils(page, test)
|
||||
|
||||
const { dir } = await context.folderSetupFn(async (dir) => {
|
||||
const finalDir = join(dir, 'testDefault')
|
||||
await fsp.mkdir(finalDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('e2e-can-sketch-on-chamfer.kcl'),
|
||||
join(finalDir, 'lee.kcl')
|
||||
)
|
||||
})
|
||||
|
||||
const contentOriginal = await fsp.readFile(
|
||||
join(dir, 'testDefault', 'lee.kcl'),
|
||||
'utf-8'
|
||||
)
|
||||
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await panesOpen(['files'])
|
||||
await homePage.openProject('testDefault')
|
||||
|
||||
await cloneFile('lee.kcl')
|
||||
await cloneFile('lee-1.kcl')
|
||||
await cloneFile('lee-2.kcl')
|
||||
await cloneFile('lee-3.kcl')
|
||||
await cloneFile('lee-4.kcl')
|
||||
|
||||
await test.step('Postcondition: there are 5 new lee-*.kcl files', async () => {
|
||||
await expect(
|
||||
page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: /lee[-]?[0-5]?/ })
|
||||
).toHaveCount(5)
|
||||
})
|
||||
|
||||
await test.step('Postcondition: the files have the same contents', async () => {
|
||||
for (let n = 0; n < 5; n += 1) {
|
||||
const content = await fsp.readFile(
|
||||
join(dir, 'testDefault', `lee-${n + 1}.kcl`),
|
||||
'utf-8'
|
||||
)
|
||||
await expect(content).toEqual(contentOriginal)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -89,18 +89,11 @@ export class HomePageFixture {
|
||||
* Maybe there a good sanity check we can do each time?
|
||||
*/
|
||||
expectState = async (expectedState: HomePageState) => {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const [projectCards, sortBy] = await Promise.all([
|
||||
this._serialiseProjectCards(),
|
||||
this._serialiseSortBy(),
|
||||
])
|
||||
return {
|
||||
projectCards,
|
||||
sortBy,
|
||||
await expect.poll(this._serialiseSortBy).toEqual(expectedState.sortBy)
|
||||
|
||||
for (const projectCard of expectedState.projectCards) {
|
||||
await expect.poll(this._serialiseProjectCards).toContainEqual(projectCard)
|
||||
}
|
||||
})
|
||||
.toEqual(expectedState)
|
||||
}
|
||||
|
||||
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
|
||||
|
@ -18,6 +18,7 @@ export class ToolbarFixture {
|
||||
filletButton!: Locator
|
||||
chamferButton!: Locator
|
||||
shellButton!: Locator
|
||||
revolveButton!: Locator
|
||||
offsetPlaneButton!: Locator
|
||||
startSketchBtn!: Locator
|
||||
lineBtn!: Locator
|
||||
@ -47,6 +48,7 @@ export class ToolbarFixture {
|
||||
this.filletButton = page.getByTestId('fillet3d')
|
||||
this.chamferButton = page.getByTestId('chamfer3d')
|
||||
this.shellButton = page.getByTestId('shell')
|
||||
this.revolveButton = page.getByTestId('revolve')
|
||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||
this.startSketchBtn = page.getByTestId('sketch')
|
||||
this.lineBtn = page.getByTestId('line')
|
||||
@ -60,7 +62,9 @@ export class ToolbarFixture {
|
||||
this.filePane = page.locator('#files-pane')
|
||||
this.featureTreePane = page.locator('#feature-tree-pane')
|
||||
this.fileCreateToast = page.getByText('Successfully created')
|
||||
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
|
||||
this.exeIndicator = page.getByTestId(
|
||||
'model-state-indicator-receive-reliable'
|
||||
)
|
||||
}
|
||||
|
||||
get logoLink() {
|
||||
|
@ -8,16 +8,15 @@ import { getUtils } from './test-utils'
|
||||
|
||||
// test file is for testing point an click code gen functionality that's not sketch mode related
|
||||
|
||||
test('verify extruding circle works', async ({
|
||||
test.describe('Point-and-click tests', { tag: ['@skipWin'] }, () => {
|
||||
test('verify extruding circle works', async ({
|
||||
context,
|
||||
homePage,
|
||||
cmdBar,
|
||||
editor,
|
||||
toolbar,
|
||||
scene,
|
||||
}) => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
}) => {
|
||||
const file = await fs.readFile(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
@ -94,11 +93,9 @@ test('verify extruding circle works', async ({
|
||||
|
||||
await editor.expectEditor.toContain(expectString)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('verify sketch on chamfer works', () => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
test.describe('verify sketch on chamfer works', () => {
|
||||
const _sketchOnAChamfer =
|
||||
(
|
||||
page: Page,
|
||||
@ -217,7 +214,8 @@ test.describe('verify sketch on chamfer works', () => {
|
||||
getOppositeEdge(seg01)
|
||||
]}, %)`,
|
||||
|
||||
afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)',
|
||||
afterChamferSelectSnippet:
|
||||
'sketch002 = startSketchOn(extrude001, seg03)',
|
||||
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|
||||
|> angledLine([
|
||||
@ -248,7 +246,8 @@ test.describe('verify sketch on chamfer works', () => {
|
||||
]
|
||||
}, %)`,
|
||||
|
||||
afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)',
|
||||
afterChamferSelectSnippet:
|
||||
'sketch003 = startSketchOn(extrude001, seg04)',
|
||||
afterRectangle1stClickSnippet: 'startProfileAt([-209.64, 255.28], %)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|
||||
|> angledLine([
|
||||
@ -273,7 +272,8 @@ test.describe('verify sketch on chamfer works', () => {
|
||||
getNextAdjacentEdge(seg02)
|
||||
]
|
||||
}, %)`,
|
||||
afterChamferSelectSnippet: 'sketch003 = startSketchOn(extrude001, seg04)',
|
||||
afterChamferSelectSnippet:
|
||||
'sketch003 = startSketchOn(extrude001, seg04)',
|
||||
afterRectangle1stClickSnippet: 'startProfileAt([75.8, 317.2], %)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0, 11.56], %, $rectangleSegmentA003)
|
||||
|> angledLine([
|
||||
@ -296,7 +296,8 @@ test.describe('verify sketch on chamfer works', () => {
|
||||
length = 30,
|
||||
tags = [getNextAdjacentEdge(yo)]
|
||||
}, %)`,
|
||||
afterChamferSelectSnippet: 'sketch005 = startSketchOn(extrude001, seg06)',
|
||||
afterChamferSelectSnippet:
|
||||
'sketch005 = startSketchOn(extrude001, seg06)',
|
||||
afterRectangle1stClickSnippet: 'startProfileAt([-23.43, 19.69], %)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0, 9.1], %, $rectangleSegmentA005)
|
||||
|
||||
@ -436,7 +437,8 @@ test.describe('verify sketch on chamfer works', () => {
|
||||
getOppositeEdge(seg01)
|
||||
]}, extrude001)`,
|
||||
beforeChamferSnippetEnd: '}, extrude001)',
|
||||
afterChamferSelectSnippet: 'sketch002 = startSketchOn(extrude001, seg03)',
|
||||
afterChamferSelectSnippet:
|
||||
'sketch002 = startSketchOn(extrude001, seg03)',
|
||||
afterRectangle1stClickSnippet: 'startProfileAt([205.96, 254.59], %)',
|
||||
afterRectangle2ndClickSnippet: `angledLine([0, 11.39], %, $rectangleSegmentA002)
|
||||
|> angledLine([
|
||||
@ -494,15 +496,15 @@ sketch002 = startSketchOn(extrude001, seg03)
|
||||
{ shouldNormalise: true }
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Verify axis, origin, and horizontal snapping`, async ({
|
||||
test(`Verify axis, origin, and horizontal snapping`, async ({
|
||||
page,
|
||||
homePage,
|
||||
editor,
|
||||
toolbar,
|
||||
scene,
|
||||
}) => {
|
||||
}) => {
|
||||
const viewPortSize = { width: 1200, height: 500 }
|
||||
|
||||
await page.setBodyDimensions(viewPortSize)
|
||||
@ -599,16 +601,16 @@ test(`Verify axis, origin, and horizontal snapping`, async ({
|
||||
expectedCodeSnippets.afterSegmentDraggedOnYAxis
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Verify user can double-click to edit a sketch`, async ({
|
||||
test(`Verify user can double-click to edit a sketch`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
editor,
|
||||
toolbar,
|
||||
scene,
|
||||
}) => {
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
const initialCode = `closedSketch = startSketchOn('XZ')
|
||||
@ -690,7 +692,11 @@ openSketch = startSketchOn('XY')
|
||||
|
||||
await test.step(`Double-click on the open sketch`, async () => {
|
||||
await moveToOpenPath()
|
||||
await scene.expectPixelColor([250, 250, 250], pointOnPathAfterSketching, 15)
|
||||
await scene.expectPixelColor(
|
||||
[250, 250, 250],
|
||||
pointOnPathAfterSketching,
|
||||
15
|
||||
)
|
||||
// There is a full execution after exiting sketch that clears the scene.
|
||||
await page.waitForTimeout(500)
|
||||
await dblClickOpenPath()
|
||||
@ -704,9 +710,9 @@ openSketch = startSketchOn('XY')
|
||||
diagnostics: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Offset plane point-and-click`, async ({
|
||||
test(`Offset plane point-and-click`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
@ -714,7 +720,7 @@ test(`Offset plane point-and-click`, async ({
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
}) => {
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 700, y: 150 }
|
||||
const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
@ -767,13 +773,13 @@ test(`Offset plane point-and-click`, async ({
|
||||
await page.keyboard.press('Backspace')
|
||||
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const loftPointAndClickCases = [
|
||||
const loftPointAndClickCases = [
|
||||
{ shouldPreselect: true },
|
||||
{ shouldPreselect: false },
|
||||
]
|
||||
loftPointAndClickCases.forEach(({ shouldPreselect }) => {
|
||||
]
|
||||
loftPointAndClickCases.forEach(({ shouldPreselect }) => {
|
||||
test(`Loft point-and-click (preselected sketches: ${shouldPreselect})`, async ({
|
||||
context,
|
||||
page,
|
||||
@ -859,16 +865,16 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
|
||||
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: merge with above test. Right now we're not able to delete a loft
|
||||
// right after creation via selection for some reason, so we go with a new instance
|
||||
test('Loft and offset plane deletion via selection', async ({
|
||||
// TODO: merge with above test. Right now we're not able to delete a loft
|
||||
// right after creation via selection for some reason, so we go with a new instance
|
||||
test('Loft and offset plane deletion via selection', async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
}) => {
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||
plane001 = offsetPlane('XZ', 50)
|
||||
@ -885,7 +891,10 @@ loft001 = loft([sketch001, sketch002])
|
||||
// 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 [clickOnSketch2] = scene.makeMouseHelpers(
|
||||
testPoint.x,
|
||||
testPoint.y + 80
|
||||
)
|
||||
|
||||
await test.step(`Delete loft`, async () => {
|
||||
// Check for loft
|
||||
@ -920,9 +929,9 @@ loft001 = loft([sketch001, sketch002])
|
||||
// Check for sketch 1
|
||||
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Sweep point-and-click`, async ({
|
||||
test(`Sweep point-and-click`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
@ -930,7 +939,7 @@ test(`Sweep point-and-click`, async ({
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('YZ')
|
||||
|> circle({
|
||||
center = [0, 0],
|
||||
@ -951,7 +960,10 @@ sketch002 = startSketchOn('XZ')
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 700, y: 250 }
|
||||
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
|
||||
const [clickOnSketch2] = scene.makeMouseHelpers(
|
||||
testPoint.x - 50,
|
||||
testPoint.y
|
||||
)
|
||||
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
|
||||
|
||||
await test.step(`Look for sketch001`, async () => {
|
||||
@ -1012,16 +1024,16 @@ sketch002 = startSketchOn('XZ')
|
||||
await toolbar.closePane('feature-tree')
|
||||
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Sweep point-and-click failing validation`, async ({
|
||||
test(`Sweep point-and-click failing validation`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('YZ')
|
||||
|> circle({
|
||||
center = [0, 0],
|
||||
@ -1042,7 +1054,10 @@ sketch002 = startSketchOn('XZ')
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 700, y: 250 }
|
||||
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
|
||||
const [clickOnSketch2] = scene.makeMouseHelpers(
|
||||
testPoint.x - 50,
|
||||
testPoint.y
|
||||
)
|
||||
|
||||
await test.step(`Look for sketch001`, async () => {
|
||||
await toolbar.closePane('code')
|
||||
@ -1078,12 +1093,12 @@ sketch002 = startSketchOn('XZ')
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(
|
||||
page.getByText('Unable to sweep with the provided selection')
|
||||
page.getByText('Unable to sweep with the current selection. Reason:')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Fillet point-and-click`, async ({
|
||||
test(`Fillet point-and-click`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
@ -1091,7 +1106,7 @@ test(`Fillet point-and-click`, async ({
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
}) => {
|
||||
// Code samples
|
||||
const initialCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-12, -6], %)
|
||||
@ -1100,7 +1115,7 @@ test(`Fillet point-and-click`, async ({
|
||||
|> line(end = [0, -12])
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(-12, sketch001)
|
||||
extrude001 = extrude(sketch001, length = -12)
|
||||
`
|
||||
const firstFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)'
|
||||
const secondFilletDeclaration =
|
||||
@ -1183,7 +1198,7 @@ extrude001 = extrude(-12, sketch001)
|
||||
currentArgKey: 'radius',
|
||||
currentArgValue: '5',
|
||||
headerArguments: {
|
||||
Selection: '1 face',
|
||||
Selection: '1 segment',
|
||||
Radius: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
@ -1192,7 +1207,7 @@ extrude001 = extrude(-12, sketch001)
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
headerArguments: {
|
||||
Selection: '1 face',
|
||||
Selection: '1 segment',
|
||||
Radius: '5',
|
||||
},
|
||||
stage: 'review',
|
||||
@ -1296,9 +1311,173 @@ extrude001 = extrude(-12, sketch001)
|
||||
lowTolerance
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test(`Chamfer point-and-click`, async ({
|
||||
// Test 3: Delete fillets
|
||||
await test.step('Delete fillet via feature tree selection', async () => {
|
||||
await test.step('Open Feature Tree Pane', async () => {
|
||||
await toolbar.openPane('feature-tree')
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
await test.step('Delete fillet via feature tree selection', async () => {
|
||||
await editor.expectEditor.toContain(secondFilletDeclaration)
|
||||
const operationButton = await toolbar.getFeatureTreeOperation(
|
||||
'Fillet',
|
||||
1
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.waitForTimeout(500)
|
||||
await scene.expectPixelColor(edgeColorWhite, secondEdgeLocation, 15) // deleted
|
||||
await editor.expectEditor.not.toContain(secondFilletDeclaration)
|
||||
await scene.expectPixelColor(filletColor, firstEdgeLocation, 15) // stayed
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Fillet point-and-click delete`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
}) => {
|
||||
// Code samples
|
||||
const initialCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-12, -6], %)
|
||||
|> line(end = [0, 12])
|
||||
|> line(end = [24, 0], tag = $seg02)
|
||||
|> line(end = [0, -12])
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -12)
|
||||
|> fillet({ radius = 5, tags = [seg01] }, %) // fillet01
|
||||
|> fillet({ radius = 5, tags = [seg02] }, %) // fillet02
|
||||
fillet03 = fillet({ radius = 5, tags = [getOppositeEdge(seg01)]}, extrude001)
|
||||
fillet04 = fillet({ radius = 5, tags = [getOppositeEdge(seg02)]}, extrude001)
|
||||
`
|
||||
const pipedFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)'
|
||||
const secondPipedFilletDeclaration =
|
||||
'fillet({ radius = 5, tags = [seg02] }, %)'
|
||||
const standaloneFilletDeclaration =
|
||||
'fillet03 = fillet({ radius = 5, tags = [getOppositeEdge(seg01)]}, extrude001)'
|
||||
const secondStandaloneFilletDeclaration =
|
||||
'fillet04 = fillet({ radius = 5, tags = [getOppositeEdge(seg02)]}, extrude001)'
|
||||
|
||||
// Locators
|
||||
const pipedFilletEdgeLocation = { x: 600, y: 193 }
|
||||
const standaloneFilletEdgeLocation = { x: 600, y: 383 }
|
||||
const bodyLocation = { x: 630, y: 290 }
|
||||
|
||||
// Colors
|
||||
const edgeColorWhite: [number, number, number] = [248, 248, 248]
|
||||
const bodyColor: [number, number, number] = [155, 155, 155]
|
||||
const filletColor: [number, number, number] = [127, 127, 127]
|
||||
const backgroundColor: [number, number, number] = [30, 30, 30]
|
||||
const lowTolerance = 20
|
||||
const highTolerance = 40
|
||||
|
||||
// Setup
|
||||
await test.step(`Initial test setup`, async () => {
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
// verify modeling scene is loaded
|
||||
await scene.expectPixelColor(
|
||||
backgroundColor,
|
||||
standaloneFilletEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
|
||||
// wait for stream to load
|
||||
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
|
||||
})
|
||||
|
||||
// Test
|
||||
await test.step('Delete fillet via feature tree selection', async () => {
|
||||
await test.step('Open Feature Tree Pane', async () => {
|
||||
await toolbar.openPane('feature-tree')
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
|
||||
await test.step('Delete piped fillet via feature tree selection', async () => {
|
||||
await test.step('Verify all fillets are present in the editor', async () => {
|
||||
await editor.expectEditor.toContain(pipedFilletDeclaration)
|
||||
await editor.expectEditor.toContain(secondPipedFilletDeclaration)
|
||||
await editor.expectEditor.toContain(standaloneFilletDeclaration)
|
||||
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
|
||||
})
|
||||
await test.step('Verify test fillets are present in the scene', async () => {
|
||||
await scene.expectPixelColor(
|
||||
filletColor,
|
||||
pipedFilletEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
await scene.expectPixelColor(
|
||||
backgroundColor,
|
||||
standaloneFilletEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
})
|
||||
await test.step('Delete piped fillet', async () => {
|
||||
const operationButton = await toolbar.getFeatureTreeOperation(
|
||||
'Fillet',
|
||||
0
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
await test.step('Verify piped fillet is deleted but other fillets are not (in the editor)', async () => {
|
||||
await editor.expectEditor.not.toContain(pipedFilletDeclaration)
|
||||
await editor.expectEditor.toContain(secondPipedFilletDeclaration)
|
||||
await editor.expectEditor.toContain(standaloneFilletDeclaration)
|
||||
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
|
||||
})
|
||||
await test.step('Verify piped fillet is deleted but non-piped is not (in the scene)', async () => {
|
||||
await scene.expectPixelColor(
|
||||
edgeColorWhite, // you see edge because fillet is deleted
|
||||
pipedFilletEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
await scene.expectPixelColor(
|
||||
backgroundColor, // you see background because fillet is not deleted
|
||||
standaloneFilletEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Delete non-piped fillet via feature tree selection', async () => {
|
||||
await test.step('Delete non-piped fillet', async () => {
|
||||
const operationButton = await toolbar.getFeatureTreeOperation(
|
||||
'Fillet',
|
||||
1
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
await test.step('Verify non-piped fillet is deleted but other two fillets are not (in the editor)', async () => {
|
||||
await editor.expectEditor.toContain(secondPipedFilletDeclaration)
|
||||
await editor.expectEditor.not.toContain(standaloneFilletDeclaration)
|
||||
await editor.expectEditor.toContain(secondStandaloneFilletDeclaration)
|
||||
})
|
||||
await test.step('Verify non-piped fillet is deleted but piped is not (in the scene)', async () => {
|
||||
await scene.expectPixelColor(
|
||||
edgeColorWhite,
|
||||
standaloneFilletEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Chamfer point-and-click`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
@ -1306,7 +1485,7 @@ test(`Chamfer point-and-click`, async ({
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
}) => {
|
||||
// Code samples
|
||||
const initialCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-12, -6], %)
|
||||
@ -1398,7 +1577,7 @@ extrude001 = extrude(sketch001, length = -12)
|
||||
currentArgKey: 'length',
|
||||
currentArgValue: '5',
|
||||
headerArguments: {
|
||||
Selection: '1 face',
|
||||
Selection: '1 segment',
|
||||
Length: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
@ -1407,7 +1586,7 @@ extrude001 = extrude(sketch001, length = -12)
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Chamfer',
|
||||
headerArguments: {
|
||||
Selection: '1 face',
|
||||
Selection: '1 segment',
|
||||
Length: '5',
|
||||
},
|
||||
stage: 'review',
|
||||
@ -1425,7 +1604,11 @@ extrude001 = extrude(sketch001, length = -12)
|
||||
})
|
||||
|
||||
await test.step(`Confirm scene has changed`, async () => {
|
||||
await scene.expectPixelColor(chamferColor, firstEdgeLocation, lowTolerance)
|
||||
await scene.expectPixelColor(
|
||||
chamferColor,
|
||||
firstEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
})
|
||||
|
||||
// Test 2: Command bar flow without preselected edges
|
||||
@ -1511,13 +1694,179 @@ extrude001 = extrude(sketch001, length = -12)
|
||||
lowTolerance
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const shellPointAndClickCapCases = [
|
||||
// Test 3: Delete chamfer via feature tree selection
|
||||
await test.step('Open Feature Tree Pane', async () => {
|
||||
await toolbar.openPane('feature-tree')
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
await test.step('Delete chamfer via feature tree selection', async () => {
|
||||
const operationButton = await toolbar.getFeatureTreeOperation(
|
||||
'Chamfer',
|
||||
1
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.waitForTimeout(500)
|
||||
await scene.expectPixelColor(edgeColorWhite, secondEdgeLocation, 15) // deleted
|
||||
await scene.expectPixelColor(chamferColor, firstEdgeLocation, 15) // stayed
|
||||
})
|
||||
})
|
||||
|
||||
test(`Chamfer point-and-click delete`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
}) => {
|
||||
// Code samples
|
||||
const initialCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-12, -6], %)
|
||||
|> line(end = [0, 12])
|
||||
|> line(end = [24, 0], tag = $seg02)
|
||||
|> line(end = [0, -12])
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg01)
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -12)
|
||||
|> chamfer({ length = 5, tags = [seg01] }, %) // chamfer01
|
||||
|> chamfer({ length = 5, tags = [seg02] }, %) // chamfer02
|
||||
chamfer03 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)]}, extrude001)
|
||||
chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001)
|
||||
`
|
||||
const pipedChamferDeclaration = 'chamfer({ length = 5, tags = [seg01] }, %)'
|
||||
const secondPipedChamferDeclaration =
|
||||
'chamfer({ length = 5, tags = [seg02] }, %)'
|
||||
const standaloneChamferDeclaration =
|
||||
'chamfer03 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)]}, extrude001)'
|
||||
const secondStandaloneChamferDeclaration =
|
||||
'chamfer04 = chamfer({ length = 5, tags = [getOppositeEdge(seg02)]}, extrude001)'
|
||||
|
||||
// Locators
|
||||
const pipedChamferEdgeLocation = { x: 600, y: 193 }
|
||||
const standaloneChamferEdgeLocation = { x: 600, y: 383 }
|
||||
const bodyLocation = { x: 630, y: 290 }
|
||||
|
||||
// Colors
|
||||
const edgeColorWhite: [number, number, number] = [248, 248, 248]
|
||||
const bodyColor: [number, number, number] = [155, 155, 155]
|
||||
const chamferColor: [number, number, number] = [168, 168, 168]
|
||||
const backgroundColor: [number, number, number] = [30, 30, 30]
|
||||
const lowTolerance = 20
|
||||
const highTolerance = 40
|
||||
|
||||
// Setup
|
||||
await test.step(`Initial test setup`, async () => {
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
// verify modeling scene is loaded
|
||||
await scene.expectPixelColor(
|
||||
backgroundColor,
|
||||
standaloneChamferEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
|
||||
// wait for stream to load
|
||||
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
|
||||
})
|
||||
|
||||
// Test
|
||||
await test.step('Delete chamfer via feature tree selection', async () => {
|
||||
await test.step('Open Feature Tree Pane', async () => {
|
||||
await toolbar.openPane('feature-tree')
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
|
||||
await test.step('Delete piped chamfer via feature tree selection', async () => {
|
||||
await test.step('Verify all chamfers are present in the editor', async () => {
|
||||
await editor.expectEditor.toContain(pipedChamferDeclaration)
|
||||
await editor.expectEditor.toContain(secondPipedChamferDeclaration)
|
||||
await editor.expectEditor.toContain(standaloneChamferDeclaration)
|
||||
await editor.expectEditor.toContain(
|
||||
secondStandaloneChamferDeclaration
|
||||
)
|
||||
})
|
||||
await test.step('Verify test chamfers are present in the scene', async () => {
|
||||
await scene.expectPixelColor(
|
||||
chamferColor,
|
||||
pipedChamferEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
await scene.expectPixelColor(
|
||||
backgroundColor,
|
||||
standaloneChamferEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
})
|
||||
await test.step('Delete piped chamfer', async () => {
|
||||
const operationButton = await toolbar.getFeatureTreeOperation(
|
||||
'Chamfer',
|
||||
0
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
await test.step('Verify piped chamfer is deleted but other chamfers are not (in the editor)', async () => {
|
||||
await editor.expectEditor.not.toContain(pipedChamferDeclaration)
|
||||
await editor.expectEditor.toContain(secondPipedChamferDeclaration)
|
||||
await editor.expectEditor.toContain(standaloneChamferDeclaration)
|
||||
await editor.expectEditor.toContain(
|
||||
secondStandaloneChamferDeclaration
|
||||
)
|
||||
})
|
||||
await test.step('Verify piped chamfer is deleted but non-piped is not (in the scene)', async () => {
|
||||
await scene.expectPixelColor(
|
||||
edgeColorWhite, // you see edge color because chamfer is deleted
|
||||
pipedChamferEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
await scene.expectPixelColor(
|
||||
backgroundColor, // you see background color instead of edge because it's chamfered
|
||||
standaloneChamferEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Delete non-piped chamfer via feature tree selection', async () => {
|
||||
await test.step('Delete non-piped chamfer', async () => {
|
||||
const operationButton = await toolbar.getFeatureTreeOperation(
|
||||
'Chamfer',
|
||||
1
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
await test.step('Verify non-piped chamfer is deleted but other two chamfers are not (in the editor)', async () => {
|
||||
await editor.expectEditor.toContain(secondPipedChamferDeclaration)
|
||||
await editor.expectEditor.not.toContain(standaloneChamferDeclaration)
|
||||
await editor.expectEditor.toContain(
|
||||
secondStandaloneChamferDeclaration
|
||||
)
|
||||
})
|
||||
await test.step('Verify non-piped chamfer is deleted but piped is not (in the scene)', async () => {
|
||||
await scene.expectPixelColor(
|
||||
edgeColorWhite,
|
||||
standaloneChamferEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const shellPointAndClickCapCases = [
|
||||
{ shouldPreselect: true },
|
||||
{ shouldPreselect: false },
|
||||
]
|
||||
shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
|
||||
]
|
||||
shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
|
||||
test(`Shell point-and-click cap (preselected sketches: ${shouldPreselect})`, async ({
|
||||
context,
|
||||
page,
|
||||
@ -1527,8 +1876,6 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
const initialCode = `sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||
extrude001 = extrude(sketch001, length = 30)
|
||||
@ -1611,9 +1958,9 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
|
||||
await scene.expectPixelColor([146, 146, 146], testPoint, 15)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('Shell point-and-click wall', async ({
|
||||
test('Shell point-and-click wall', async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
@ -1621,7 +1968,7 @@ test('Shell point-and-click wall', async ({
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-20, 20], %)
|
||||
|> xLine(40, %)
|
||||
@ -1700,9 +2047,9 @@ extrude001 = extrude(sketch001, length = 40)
|
||||
await page.keyboard.press('Backspace')
|
||||
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const shellSketchOnFacesCases = [
|
||||
const shellSketchOnFacesCases = [
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 100 }, %)
|
||||
|> extrude(length = 100)
|
||||
@ -1719,8 +2066,8 @@ sketch002 = startSketchOn(extrude001, 'END')
|
||||
|> circle({ center = [0, 0], radius = 50 }, %)
|
||||
extrude002 = extrude(sketch002, length = 50)
|
||||
`,
|
||||
]
|
||||
shellSketchOnFacesCases.forEach((initialCode, index) => {
|
||||
]
|
||||
shellSketchOnFacesCases.forEach((initialCode, index) => {
|
||||
const hasExtrudesInPipe = index === 0
|
||||
test(`Shell point-and-click sketch on face (extrudes in pipes: ${hasExtrudesInPipe})`, async ({
|
||||
context,
|
||||
@ -1792,9 +2139,9 @@ shellSketchOnFacesCases.forEach((initialCode, index) => {
|
||||
await scene.expectPixelColor([73, 73, 73], testPoint, 15)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Shell dry-run validation rejects sweeps`, async ({
|
||||
test(`Shell dry-run validation rejects sweeps`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
@ -1802,7 +2149,7 @@ test(`Shell dry-run validation rejects sweeps`, async ({
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('YZ')
|
||||
|> circle({
|
||||
center = [0, 0],
|
||||
@ -1846,8 +2193,169 @@ sweep001 = sweep({ path = sketch002 }, sketch001)
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(
|
||||
page.getByText('Unable to shell with the provided selection')
|
||||
page.getByText('Unable to shell with the current selection. Reason:')
|
||||
).toBeVisible()
|
||||
await page.waitForTimeout(1000)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Revolve point and click workflows', () => {
|
||||
test('Base case workflow, auto spam continue in command bar', async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `
|
||||
sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([-100.0, 100.0], %)
|
||||
|> angledLine([0, 200.0], %, $rectangleSegmentA001)
|
||||
|> angledLine([segAng(rectangleSegmentA001) - 90, 200], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = 200)
|
||||
sketch002 = startSketchOn(extrude001, rectangleSegmentA001)
|
||||
|> startProfileAt([-66.77, 84.81], %)
|
||||
|> angledLine([180, 27.08], %, $rectangleSegmentA002)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002) - 90,
|
||||
27.8
|
||||
], %, $rectangleSegmentB002)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA002),
|
||||
-segLen(rectangleSegmentA002)
|
||||
], %, $rectangleSegmentC002)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
`
|
||||
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
// select line of code
|
||||
const codeToSelecton = `segAng(rectangleSegmentA002) - 90,`
|
||||
// revolve
|
||||
await page.getByText(codeToSelecton).click()
|
||||
await toolbar.revolveButton.click()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
const newCodeToFind = `revolve001 = revolve({ angle = 360, axis = 'X' }, sketch002)`
|
||||
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
|
||||
})
|
||||
test('revolve surface around edge from an extruded solid2d', async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `
|
||||
sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([-102.57, 101.72], %)
|
||||
|> angledLine([0, 202.6], %, $rectangleSegmentA001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001) - 90,
|
||||
202.6
|
||||
], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = 50)
|
||||
sketch002 = startSketchOn(extrude001, rectangleSegmentA001)
|
||||
|> circle({
|
||||
center = [-11.34, 10.0],
|
||||
radius = 8.69
|
||||
}, %)
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
// select line of code
|
||||
const codeToSelecton = `center = [-11.34, 10.0]`
|
||||
// revolve
|
||||
await page.getByText(codeToSelecton).click()
|
||||
await toolbar.revolveButton.click()
|
||||
await page.getByText('Edge', { exact: true }).click()
|
||||
const lineCodeToSelection = `|> angledLine([0, 202.6], %, $rectangleSegmentA001)`
|
||||
await page.getByText(lineCodeToSelection).click()
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
const newCodeToFind = `revolve001 = revolve({angle = 360, axis = getOppositeEdge(rectangleSegmentA001)}, sketch002) `
|
||||
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
|
||||
})
|
||||
test('revolve sketch circle around line segment from startProfileAt sketch', async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `
|
||||
sketch002 = startSketchOn('XY')
|
||||
|> startProfileAt([-2.02, 1.79], %)
|
||||
|> xLine(2.6, %)
|
||||
sketch001 = startSketchOn('-XY')
|
||||
|> startProfileAt([-0.48, 1.25], %)
|
||||
|> angledLine([0, 2.38], %, $rectangleSegmentA001)
|
||||
|> angledLine([segAng(rectangleSegmentA001) - 90, 2.4], %, $rectangleSegmentB001)
|
||||
|> angledLine([
|
||||
segAng(rectangleSegmentA001),
|
||||
-segLen(rectangleSegmentA001)
|
||||
], %, $rectangleSegmentC001)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = 5)
|
||||
sketch003 = startSketchOn(extrude001, 'START')
|
||||
|> circle({
|
||||
center = [-0.69, 0.56],
|
||||
radius = 0.28
|
||||
}, %)
|
||||
`
|
||||
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
// select line of code
|
||||
const codeToSelecton = `center = [-0.69, 0.56]`
|
||||
// revolve
|
||||
await page.getByText(codeToSelecton).click()
|
||||
await toolbar.revolveButton.click()
|
||||
await page.getByText('Edge', { exact: true }).click()
|
||||
const lineCodeToSelection = `|> xLine(2.6, %)`
|
||||
await page.getByText(lineCodeToSelection).click()
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
const newCodeToFind = `revolve001 = revolve({ angle = 360, axis = seg01 }, sketch003)`
|
||||
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -185,7 +185,7 @@ test(
|
||||
|
||||
// error text on hover
|
||||
await page.hover('.cm-lint-marker-error')
|
||||
const crypticErrorText = `Expected a tag identifier`
|
||||
const crypticErrorText = `Expected a tag declarator`
|
||||
await expect(page.getByText(crypticErrorText).first()).toBeVisible()
|
||||
|
||||
// black pixel means the scene has been cleared.
|
||||
@ -572,7 +572,7 @@ test(
|
||||
fs.utimesSync(`${dir}/lego/main.kcl`, _1995, _1995)
|
||||
})
|
||||
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
await page.setBodyDimensions({ width: 1200, height: 600 })
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
@ -1527,12 +1527,10 @@ test(
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page, cmdBar, homePage }, testInfo) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
await Promise.all([
|
||||
fsp.mkdir(path.join(dir, 'router-template-slate'), { recursive: true }),
|
||||
fsp.mkdir(path.join(dir, 'bracket'), { recursive: true }),
|
||||
])
|
||||
await Promise.all([
|
||||
fsp.copyFile(
|
||||
await fsp.mkdir(path.join(dir, 'router-template-slate'), {
|
||||
recursive: true,
|
||||
})
|
||||
await fsp.copyFile(
|
||||
path.join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
@ -1542,8 +1540,9 @@ test(
|
||||
'router-template-slate.kcl'
|
||||
),
|
||||
path.join(dir, 'router-template-slate', 'main.kcl')
|
||||
),
|
||||
fsp.copyFile(
|
||||
)
|
||||
await fsp.mkdir(path.join(dir, 'bracket'), { recursive: true })
|
||||
await fsp.copyFile(
|
||||
path.join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
@ -1553,8 +1552,7 @@ test(
|
||||
'focusrite_scarlett_mounting_braket.kcl'
|
||||
),
|
||||
path.join(dir, 'bracket', 'main.kcl')
|
||||
),
|
||||
])
|
||||
)
|
||||
})
|
||||
const u = await getUtils(page)
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
|
@ -35,7 +35,7 @@ sketch003 = startSketchOn('XY')
|
||||
extrude003 = extrude(sketch003, length = 20)
|
||||
`
|
||||
|
||||
test.describe('Check the happy path, for basic changing color', () => {
|
||||
test.fixme('Check the happy path, for basic changing color', () => {
|
||||
const cases = [
|
||||
{
|
||||
desc: 'User accepts change',
|
||||
@ -134,7 +134,7 @@ test.describe('Check the happy path, for basic changing color', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test.describe('bad path', () => {
|
||||
test.describe('bad path', { tag: ['@skipWin'] }, () => {
|
||||
test(`bad edit prompt`, async ({
|
||||
context,
|
||||
homePage,
|
||||
|
@ -5,7 +5,7 @@ import { getUtils, executorInputPath } from './test-utils'
|
||||
import { TEST_CODE_TRIGGER_ENGINE_EXPORT_ERROR } from './storageStates'
|
||||
import { bracket } from 'lib/exampleKcl'
|
||||
|
||||
test.describe('Regression tests', () => {
|
||||
test.describe('Regression tests', { tag: ['@skipWin'] }, () => {
|
||||
// bugs we found that don't fit neatly into other categories
|
||||
test('bad model has inline error #3251', async ({
|
||||
context,
|
||||
@ -239,12 +239,6 @@ extrude001 = extrude(sketch001, length = 50)
|
||||
'Position _ Is Out Of Range... regression test',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ context, page, homePage }) => {
|
||||
// SKip on windows, its being weird.
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'This test is being weird on windows'
|
||||
)
|
||||
|
||||
const u = await getUtils(page)
|
||||
// const PUR = 400 / 37.5 //pixeltoUnitRatio
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
@ -425,13 +419,6 @@ extrude001 = extrude(sketch001, length = 50)
|
||||
'ensure you can not export while an export is already going',
|
||||
{ tag: ['@skipLinux', '@skipWin'] },
|
||||
async ({ page, homePage }) => {
|
||||
// This is being weird on ubuntu and windows.
|
||||
test.skip(
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
process.platform === 'linux' || process.platform === 'win32',
|
||||
'This test is being weird on ubuntu'
|
||||
)
|
||||
|
||||
const u = await getUtils(page)
|
||||
await test.step('Set up the code and durations', async () => {
|
||||
await page.addInitScript(
|
||||
@ -560,8 +547,6 @@ extrude001 = extrude(sketch001, length = 50)
|
||||
page,
|
||||
homePage,
|
||||
}) => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Constants and locators
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
} from './test-utils'
|
||||
import { uuidv4, roundOff } from 'lib/utils'
|
||||
|
||||
test.describe('Sketch tests', () => {
|
||||
test.describe('Sketch tests', { tag: ['@skipWin'] }, () => {
|
||||
test('multi-sketch file shows multiple Edit Sketch buttons', async ({
|
||||
page,
|
||||
context,
|
||||
@ -312,7 +312,10 @@ test.describe('Sketch tests', () => {
|
||||
|> line(end = [1.97, 2.06])
|
||||
|> close()`)
|
||||
}
|
||||
test('code pane open at start-handles', async ({ page, homePage }) => {
|
||||
test(
|
||||
'code pane open at start-handles',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ page, homePage }) => {
|
||||
// Load the app with the code panes
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
@ -326,9 +329,13 @@ test.describe('Sketch tests', () => {
|
||||
)
|
||||
})
|
||||
await doEditSegmentsByDraggingHandle(page, homePage, ['code'])
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test('code pane closed at start-handles', async ({ page, homePage }) => {
|
||||
test(
|
||||
'code pane closed at start-handles',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ page, homePage }) => {
|
||||
// Load the app with the code panes
|
||||
await page.addInitScript(async (persistModelingContext) => {
|
||||
localStorage.setItem(
|
||||
@ -337,7 +344,8 @@ test.describe('Sketch tests', () => {
|
||||
)
|
||||
}, PERSIST_MODELING_CONTEXT)
|
||||
await doEditSegmentsByDraggingHandle(page, homePage, [])
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test('Can edit a circle center and radius by dragging its handles', async ({
|
||||
@ -636,8 +644,6 @@ test.describe('Sketch tests', () => {
|
||||
|> revolve({ axis = "X" }, %)`)
|
||||
})
|
||||
test('Can add multiple sketches', async ({ page, homePage }) => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
const u = await getUtils(page)
|
||||
|
||||
const viewportSize = { width: 1200, height: 500 }
|
||||
@ -835,8 +841,6 @@ test.describe('Sketch tests', () => {
|
||||
page,
|
||||
homePage,
|
||||
}) => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
// this was a regression https://github.com/KittyCAD/modeling-app/issues/2832
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
@ -886,7 +890,7 @@ test.describe('Sketch tests', () => {
|
||||
// sketch selection should already have been made. "Selection: 1 face" only show up when the selection has been made already
|
||||
// otherwise the cmdbar would be waiting for a selection.
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'selection : 1 face', exact: false })
|
||||
page.getByRole('button', { name: 'selection : 1 segment', exact: false })
|
||||
).toBeVisible({
|
||||
timeout: 10_000,
|
||||
})
|
||||
@ -1427,7 +1431,7 @@ test.describe('Redirecting to home page and back to the original file should cle
|
||||
'persistCode',
|
||||
` sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([256.85, 14.41], %)
|
||||
|> lineTo([0, 211.07], %)
|
||||
|> line(endAbsolute = [0, 211.07])
|
||||
`
|
||||
)
|
||||
})
|
||||
|
@ -55,13 +55,6 @@ test.skip(
|
||||
'exports of each format should work',
|
||||
{ tag: ['@snapshot', '@skipWin', '@skipMacos'] },
|
||||
async ({ page, context }) => {
|
||||
// skip on macos and windows.
|
||||
test.skip(
|
||||
// eslint-disable-next-line jest/valid-title
|
||||
process.platform === 'darwin' || process.platform === 'win32',
|
||||
'Skip on macos and windows'
|
||||
)
|
||||
|
||||
// FYI this test doesn't work with only engine running locally
|
||||
// And you will need to have the KittyCAD CLI installed
|
||||
const u = await getUtils(page)
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 145 KiB |
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 49 KiB |
@ -552,6 +552,16 @@ export async function getUtils(page: Page, test_?: typeof test) {
|
||||
})
|
||||
},
|
||||
|
||||
cloneFile: async (name: string) => {
|
||||
return test?.step(`Cloning file '${name}'`, async () => {
|
||||
await page
|
||||
.locator('[data-testid="file-pane-scroll-container"] button')
|
||||
.filter({ hasText: name })
|
||||
.click({ button: 'right' })
|
||||
await page.getByTestId('context-menu-clone').click()
|
||||
})
|
||||
},
|
||||
|
||||
selectFile: async (name: string) => {
|
||||
return test?.step(`Select ${name}`, async () => {
|
||||
await page
|
||||
|
@ -3,13 +3,8 @@ import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { getUtils } from './test-utils'
|
||||
|
||||
test.describe('Testing Camera Movement', () => {
|
||||
test.describe('Testing Camera Movement', { tag: ['@skipWin'] }, () => {
|
||||
test('Can move camera reliably', async ({ page, context, homePage }) => {
|
||||
// TODO: fix this test on windows too after the electron migration
|
||||
const winOrMac =
|
||||
process.platform === 'win32' || process.platform === 'darwin'
|
||||
// eslint-disable-next-line
|
||||
test.skip(winOrMac, 'Skip on windows')
|
||||
const u = await getUtils(page)
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
|
||||
@ -347,8 +342,6 @@ test.describe('Testing Camera Movement', () => {
|
||||
homePage,
|
||||
page,
|
||||
}) => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
/**
|
||||
* Currently we only allow zooming by scroll when no other camera movement is happening,
|
||||
* set within cameraMouseDragGuards in cameraControls.ts,
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
import { XOR } from 'lib/utils'
|
||||
import path from 'node:path'
|
||||
|
||||
test.describe('Testing constraints', () => {
|
||||
test.describe('Testing constraints', { tag: ['@skipWin'] }, () => {
|
||||
test('Can constrain line length', async ({ page, homePage }) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
@ -129,8 +129,6 @@ test.describe('Testing constraints', () => {
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
|
||||
})
|
||||
test.describe('Test perpendicular distance constraint', () => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
const cases = [
|
||||
{
|
||||
testName: 'Add variable',
|
||||
@ -251,8 +249,6 @@ test.describe('Testing constraints', () => {
|
||||
}
|
||||
})
|
||||
test.describe('Test distance between constraint', () => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
const cases = [
|
||||
{
|
||||
testName: 'Add variable',
|
||||
@ -472,8 +468,6 @@ test.describe('Testing constraints', () => {
|
||||
}
|
||||
})
|
||||
test.describe('Test Angle constraint double segment selection', () => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
const cases = [
|
||||
{
|
||||
testName: 'Add variable',
|
||||
@ -664,8 +658,6 @@ test.describe('Testing constraints', () => {
|
||||
}
|
||||
})
|
||||
test.describe('Test Length constraint single selection', () => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
const cases = [
|
||||
{
|
||||
testName: 'Length - Add variable',
|
||||
@ -851,8 +843,6 @@ part002 = startSketchOn('XZ')
|
||||
}
|
||||
})
|
||||
test.describe('Two segment - no modal constraints', () => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
const cases = [
|
||||
{
|
||||
codeAfter: `|> angledLine([83, segLen(seg01)], %)`,
|
||||
|
@ -3,9 +3,7 @@ import { getUtils } from './test-utils'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { TEST_CODE_GIZMO } from './storageStates'
|
||||
|
||||
test.describe('Testing Gizmo', () => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
test.describe('Testing Gizmo', { tag: ['@skipWin'] }, () => {
|
||||
const cases = [
|
||||
{
|
||||
testDescription: 'top view',
|
||||
|
@ -69,7 +69,6 @@ test.describe('Testing in-app sample loading', () => {
|
||||
await confirmButton.click()
|
||||
|
||||
await editor.expectEditor.toContain('// ' + newSample.title)
|
||||
await expect(unitsToast('in')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
@ -158,7 +157,6 @@ test.describe('Testing in-app sample loading', () => {
|
||||
await editor.expectEditor.toContain('// ' + sampleOne.title)
|
||||
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
|
||||
await expect(projectMenuButton).toContainText(sampleOne.file)
|
||||
await expect(unitsToast('in')).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step(`Now overwrite the current file`, async () => {
|
||||
@ -188,7 +186,6 @@ test.describe('Testing in-app sample loading', () => {
|
||||
await expect(newlyCreatedFile(sampleOne.file)).toBeVisible()
|
||||
await expect(newlyCreatedFile(sampleTwo.file)).not.toBeVisible()
|
||||
await expect(projectMenuButton).toContainText(sampleOne.file)
|
||||
await expect(unitsToast('mm')).toBeVisible()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -5,10 +5,10 @@ import { LineInputsType } from 'lang/std/sketchcombos'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { EditorFixture } from './fixtures/editorFixture'
|
||||
|
||||
test.describe('Testing segment overlays', () => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
test.describe('Hover over a segment should show its overlay, hovering over the input overlays should show its popover, clicking the input overlay should constrain/unconstrain it:\nfor the following segments', () => {
|
||||
test.describe('Testing segment overlays', { tag: ['@skipWin'] }, () => {
|
||||
test.fixme(
|
||||
'Hover over a segment should show its overlay, hovering over the input overlays should show its popover, clicking the input overlay should constrain/unconstrain it:\nfor the following segments',
|
||||
() => {
|
||||
// TODO: fix this test on mac after the electron migration
|
||||
test.skip(process.platform === 'darwin', 'Skip on mac')
|
||||
/**
|
||||
@ -557,7 +557,8 @@ test.describe('Testing segment overlays', () => {
|
||||
'angledLineOfYLength({ angle = -91, length = 19 + 0 }, %)',
|
||||
expectAfterUnconstrained:
|
||||
'angledLineOfYLength({ angle = angle002, length = 19 + 0 }, %)',
|
||||
expectFinal: 'angledLineOfYLength({ angle = -91, length = 19 + 0 }, %)',
|
||||
expectFinal:
|
||||
'angledLineOfYLength({ angle = -91, length = 19 + 0 }, %)',
|
||||
ang: ang + 180,
|
||||
steps: 6,
|
||||
locator: '[data-overlay-toolbar-index="8"]',
|
||||
@ -656,7 +657,9 @@ test.describe('Testing segment overlays', () => {
|
||||
locator: '[data-overlay-toolbar-index="9"]',
|
||||
})
|
||||
|
||||
const angledLineToY = await u.getBoundingBox(`[data-overlay-index="10"]`)
|
||||
const angledLineToY = await u.getBoundingBox(
|
||||
`[data-overlay-index="10"]`
|
||||
)
|
||||
ang = await u.getAngle(`[data-overlay-index="10"]`)
|
||||
console.log('angledLineToY')
|
||||
await clickUnconstrained({
|
||||
@ -677,7 +680,8 @@ test.describe('Testing segment overlays', () => {
|
||||
constraintType: 'yAbsolute',
|
||||
expectBeforeUnconstrained:
|
||||
'angledLineToY({ angle = 89, to = 9.14 + 0 }, %)',
|
||||
expectAfterUnconstrained: 'angledLineToY({ angle = 89, to = 9.14 }, %)',
|
||||
expectAfterUnconstrained:
|
||||
'angledLineToY({ angle = 89, to = 9.14 }, %)',
|
||||
expectFinal: 'angledLineToY({ angle = 89, to = yAbs001 }, %)',
|
||||
ang: ang + 180,
|
||||
locator: '[data-overlay-toolbar-index="10"]',
|
||||
@ -857,7 +861,8 @@ test.describe('Testing segment overlays', () => {
|
||||
constraintType: 'xAbsolute',
|
||||
expectBeforeUnconstrained:
|
||||
'circle({ center = [1 + 0, 0], radius = 8 }, %)',
|
||||
expectAfterUnconstrained: 'circle({ center = [1, 0], radius = 8 }, %)',
|
||||
expectAfterUnconstrained:
|
||||
'circle({ center = [1, 0], radius = 8 }, %)',
|
||||
expectFinal: 'circle({ center = [xAbs001, 0], radius = 8 }, %)',
|
||||
ang: ang + 105,
|
||||
steps: 6,
|
||||
@ -890,7 +895,8 @@ test.describe('Testing segment overlays', () => {
|
||||
locator: '[data-overlay-toolbar-index="0"]',
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
test.describe('Testing deleting a segment', () => {
|
||||
const _deleteSegmentSequence =
|
||||
(page: Page, editor: EditorFixture) =>
|
||||
|
@ -5,15 +5,12 @@ import { Coords2d } from 'lang/std/sketch'
|
||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
|
||||
test.describe('Testing selections', () => {
|
||||
test.describe('Testing selections', { tag: ['@skipWin'] }, () => {
|
||||
test.setTimeout(90_000)
|
||||
test(
|
||||
'Selections work on fresh and edited sketch',
|
||||
{ tag: ['@skipWin'] },
|
||||
async ({ page, homePage }) => {
|
||||
// Skip on windows its being weird.
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
|
||||
test('Selections work on fresh and edited sketch', async ({
|
||||
page,
|
||||
homePage,
|
||||
}) => {
|
||||
// tests mapping works on fresh sketch and edited sketch
|
||||
// tests using hovers which is the same as selections, because if
|
||||
// source ranges are wrong, hovers won't work
|
||||
@ -160,9 +157,7 @@ test.describe('Testing selections', () => {
|
||||
|
||||
// check the same selection again by putting cursor in code first then selecting axis
|
||||
await test.step(`Same selection but code selection then axis`, async () => {
|
||||
await page
|
||||
.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`)
|
||||
.click()
|
||||
await page.getByText(` |> xLine(${commonPoints.num2 * -1}, %)`).click()
|
||||
await page.keyboard.down('Shift')
|
||||
await constrainButton.click()
|
||||
await expect(absXButton).toBeDisabled()
|
||||
@ -190,9 +185,7 @@ test.describe('Testing selections', () => {
|
||||
|
||||
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.up(
|
||||
process.platform === 'linux' ? 'Control' : 'Meta'
|
||||
)
|
||||
await page.keyboard.up(process.platform === 'linux' ? 'Control' : 'Meta')
|
||||
|
||||
// clear selection by clicking on nothing
|
||||
await emptySpaceClick()
|
||||
@ -253,8 +246,7 @@ test.describe('Testing selections', () => {
|
||||
await test.step(`Test hovering and selecting on edited sketch`, async () => {
|
||||
await selectionSequence()
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test('Solids should be select and deletable', async ({ page, homePage }) => {
|
||||
test.setTimeout(90_000)
|
||||
@ -492,8 +484,6 @@ test.describe('Testing selections', () => {
|
||||
page,
|
||||
homePage,
|
||||
}) => {
|
||||
// TODO: fix this test on windows after the electron migration
|
||||
test.skip(process.platform === 'win32', 'Skip on windows')
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async (KCL_DEFAULT_LENGTH) => {
|
||||
localStorage.setItem(
|
||||
|
@ -435,11 +435,6 @@ test.describe('Text-to-CAD tests', () => {
|
||||
async ({ page, homePage }) => {
|
||||
// Let this test run longer since we've seen it timeout.
|
||||
test.setTimeout(180_000)
|
||||
// skip on windows
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'This test is flaky, skipping for now'
|
||||
)
|
||||
|
||||
const u = await getUtils(page)
|
||||
|
||||
|
@ -75,3 +75,6 @@ publish:
|
||||
channel: latest
|
||||
releaseInfo:
|
||||
releaseNotesFile: release-notes.md
|
||||
protocols:
|
||||
- name: Zoo Studio
|
||||
schemes: ['zoo-studio']
|
||||
|
@ -9,23 +9,8 @@ const rootDir = process.cwd()
|
||||
const config: ForgeConfig = {
|
||||
packagerConfig: {
|
||||
asar: true,
|
||||
osxSign: (process.env.BUILD_RELEASE === 'true' && {}) || undefined,
|
||||
osxNotarize:
|
||||
(process.env.BUILD_RELEASE === 'true' && {
|
||||
appleId: process.env.APPLE_ID || '',
|
||||
appleIdPassword: process.env.APPLE_PASSWORD || '',
|
||||
teamId: process.env.APPLE_TEAM_ID || '',
|
||||
}) ||
|
||||
undefined,
|
||||
executableName: 'zoo-modeling-app',
|
||||
icon: path.resolve(rootDir, 'assets', 'icon'),
|
||||
protocols: [
|
||||
{
|
||||
name: 'Zoo Studio',
|
||||
schemes: ['zoo-studio'],
|
||||
},
|
||||
],
|
||||
extendInfo: 'Info.plist', // Information for file associations.
|
||||
},
|
||||
rebuildConfig: {},
|
||||
makers: [],
|
||||
|
2
interface.d.ts
vendored
@ -32,6 +32,7 @@ export interface IElectronAPI {
|
||||
callback: (eventType: string, path: string) => void
|
||||
) => void
|
||||
readFile: typeof fs.readFile
|
||||
copyFile: typeof fs.copyFile
|
||||
watchFileOff: (path: string, key: string) => void
|
||||
writeFile: (
|
||||
path: string,
|
||||
@ -65,6 +66,7 @@ export interface IElectronAPI {
|
||||
VITE_KC_API_WS_MODELING_URL: string
|
||||
VITE_KC_API_BASE_URL: string
|
||||
VITE_KC_SITE_BASE_URL: string
|
||||
VITE_KC_SITE_APP_URL: string
|
||||
VITE_KC_SKIP_AUTH: string
|
||||
VITE_KC_CONNECTION_TIMEOUT_MS: string
|
||||
VITE_KC_DEV_TOKEN: string
|
||||
|
18
package.json
@ -103,11 +103,11 @@
|
||||
"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",
|
||||
"chrome:test": "PLATFORM=web NODE_ENV=development yarn playwright test --config=playwright.config.ts --project='Google Chrome' --grep-invert='@snapshot'",
|
||||
"tron:test": "NODE_ENV=development yarn playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
||||
"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",
|
||||
"tronb:vite:dev": "vite build -c vite.main.config.ts -m development && vite build -c vite.preload.config.ts -m development && vite build -c vite.renderer.config.ts -m development",
|
||||
"tronb:vite:prod": "vite build -c vite.main.config.ts && vite build -c vite.preload.config.ts && vite build -c vite.renderer.config.ts",
|
||||
"tronb:package:dev": "yarn tronb:vite:dev && electron-builder --config electron-builder.yml",
|
||||
"tronb:package:prod": "yarn tronb:vite:prod && electron-builder --config electron-builder.yml --publish always",
|
||||
"test-setup": "yarn install && yarn build:wasm",
|
||||
"test": "vitest --mode development",
|
||||
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
|
||||
@ -116,10 +116,10 @@
|
||||
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\" --quiet",
|
||||
"test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot' --quiet",
|
||||
"test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot' --quiet",
|
||||
"test:playwright:electron:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
||||
"test:playwright:electron:windows:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
||||
"test:playwright:electron:macos:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
||||
"test:playwright:electron:ubuntu:local": "yarn tron:package && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
|
||||
"test:playwright:electron:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
||||
"test:playwright:electron:windows:local": "yarn tronb:vite:dev && set NODE_ENV='development' && playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
||||
"test:playwright:electron:macos:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
||||
"test:playwright:electron:ubuntu:local": "yarn tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
|
||||
"test:unit:local": "yarn simpleserver:bg && yarn test:unit; kill-port 3000",
|
||||
"test:unit:kcl-samples:local": "yarn simpleserver:bg && yarn test:unit:kcl-samples; kill-port 3000"
|
||||
},
|
||||
@ -204,7 +204,7 @@
|
||||
"vite": "^5.4.12",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest": "^1.6.1",
|
||||
"vitest-webgl-canvas-mock": "^1.1.0",
|
||||
"wasm-pack": "^0.13.1",
|
||||
"ws": "^8.17.0",
|
||||
|
@ -29,7 +29,7 @@
|
||||
"rollup": "^4.29.1",
|
||||
"rollup-plugin-dts": "^6.1.1",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^2.1.8"
|
||||
"vitest": "^2.1.9"
|
||||
},
|
||||
"files": [
|
||||
"dist/"
|
||||
|
@ -313,62 +313,62 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50"
|
||||
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
|
||||
|
||||
"@vitest/expect@2.1.8":
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.8.tgz#13fad0e8d5a0bf0feb675dcf1d1f1a36a1773bc1"
|
||||
integrity sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==
|
||||
"@vitest/expect@2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.9.tgz#b566ea20d58ea6578d8dc37040d6c1a47ebe5ff8"
|
||||
integrity sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==
|
||||
dependencies:
|
||||
"@vitest/spy" "2.1.8"
|
||||
"@vitest/utils" "2.1.8"
|
||||
"@vitest/spy" "2.1.9"
|
||||
"@vitest/utils" "2.1.9"
|
||||
chai "^5.1.2"
|
||||
tinyrainbow "^1.2.0"
|
||||
|
||||
"@vitest/mocker@2.1.8":
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.8.tgz#51dec42ac244e949d20009249e033e274e323f73"
|
||||
integrity sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==
|
||||
"@vitest/mocker@2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.9.tgz#36243b27351ca8f4d0bbc4ef91594ffd2dc25ef5"
|
||||
integrity sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==
|
||||
dependencies:
|
||||
"@vitest/spy" "2.1.8"
|
||||
"@vitest/spy" "2.1.9"
|
||||
estree-walker "^3.0.3"
|
||||
magic-string "^0.30.12"
|
||||
|
||||
"@vitest/pretty-format@2.1.8", "@vitest/pretty-format@^2.1.8":
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.8.tgz#88f47726e5d0cf4ba873d50c135b02e4395e2bca"
|
||||
integrity sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==
|
||||
"@vitest/pretty-format@2.1.9", "@vitest/pretty-format@^2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz#434ff2f7611689f9ce70cd7d567eceb883653fdf"
|
||||
integrity sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==
|
||||
dependencies:
|
||||
tinyrainbow "^1.2.0"
|
||||
|
||||
"@vitest/runner@2.1.8":
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.8.tgz#b0e2dd29ca49c25e9323ea2a45a5125d8729759f"
|
||||
integrity sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==
|
||||
"@vitest/runner@2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.9.tgz#cc18148d2d797fd1fd5908d1f1851d01459be2f6"
|
||||
integrity sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==
|
||||
dependencies:
|
||||
"@vitest/utils" "2.1.8"
|
||||
"@vitest/utils" "2.1.9"
|
||||
pathe "^1.1.2"
|
||||
|
||||
"@vitest/snapshot@2.1.8":
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.8.tgz#d5dc204f4b95dc8b5e468b455dfc99000047d2de"
|
||||
integrity sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==
|
||||
"@vitest/snapshot@2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.9.tgz#24260b93f798afb102e2dcbd7e61c6dfa118df91"
|
||||
integrity sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==
|
||||
dependencies:
|
||||
"@vitest/pretty-format" "2.1.8"
|
||||
"@vitest/pretty-format" "2.1.9"
|
||||
magic-string "^0.30.12"
|
||||
pathe "^1.1.2"
|
||||
|
||||
"@vitest/spy@2.1.8":
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.8.tgz#bc41af3e1e6a41ae3b67e51f09724136b88fa447"
|
||||
integrity sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==
|
||||
"@vitest/spy@2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.9.tgz#cb28538c5039d09818b8bfa8edb4043c94727c60"
|
||||
integrity sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==
|
||||
dependencies:
|
||||
tinyspy "^3.0.2"
|
||||
|
||||
"@vitest/utils@2.1.8":
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.8.tgz#f8ef85525f3362ebd37fd25d268745108d6ae388"
|
||||
integrity sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==
|
||||
"@vitest/utils@2.1.9":
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.9.tgz#4f2486de8a54acf7ecbf2c5c24ad7994a680a6c1"
|
||||
integrity sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==
|
||||
dependencies:
|
||||
"@vitest/pretty-format" "2.1.8"
|
||||
"@vitest/pretty-format" "2.1.9"
|
||||
loupe "^3.1.2"
|
||||
tinyrainbow "^1.2.0"
|
||||
|
||||
@ -662,10 +662,10 @@ typescript@^5.7.2:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
|
||||
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
|
||||
|
||||
vite-node@2.1.8:
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.8.tgz#9495ca17652f6f7f95ca7c4b568a235e0c8dbac5"
|
||||
integrity sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==
|
||||
vite-node@2.1.9:
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.9.tgz#549710f76a643f1c39ef34bdb5493a944e4f895f"
|
||||
integrity sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==
|
||||
dependencies:
|
||||
cac "^6.7.14"
|
||||
debug "^4.3.7"
|
||||
@ -693,18 +693,18 @@ vite@^5.0.0:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vitest@^2.1.8:
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.8.tgz#2e6a00bc24833574d535c96d6602fb64163092fa"
|
||||
integrity sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==
|
||||
vitest@^2.1.9:
|
||||
version "2.1.9"
|
||||
resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.9.tgz#7d01ffd07a553a51c87170b5e80fea3da7fb41e7"
|
||||
integrity sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==
|
||||
dependencies:
|
||||
"@vitest/expect" "2.1.8"
|
||||
"@vitest/mocker" "2.1.8"
|
||||
"@vitest/pretty-format" "^2.1.8"
|
||||
"@vitest/runner" "2.1.8"
|
||||
"@vitest/snapshot" "2.1.8"
|
||||
"@vitest/spy" "2.1.8"
|
||||
"@vitest/utils" "2.1.8"
|
||||
"@vitest/expect" "2.1.9"
|
||||
"@vitest/mocker" "2.1.9"
|
||||
"@vitest/pretty-format" "^2.1.9"
|
||||
"@vitest/runner" "2.1.9"
|
||||
"@vitest/snapshot" "2.1.9"
|
||||
"@vitest/spy" "2.1.9"
|
||||
"@vitest/utils" "2.1.9"
|
||||
chai "^5.1.2"
|
||||
debug "^4.3.7"
|
||||
expect-type "^1.1.0"
|
||||
@ -716,7 +716,7 @@ vitest@^2.1.8:
|
||||
tinypool "^1.0.1"
|
||||
tinyrainbow "^1.2.0"
|
||||
vite "^5.0.0"
|
||||
vite-node "2.1.8"
|
||||
vite-node "2.1.9"
|
||||
why-is-node-running "^2.3.0"
|
||||
|
||||
w3c-keyname@^2.2.4:
|
||||
|
7
packages/codemirror-lsp-client/src/lib/utils.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* A safer type guard for arrays since the built-in Array.isArray() asserts `any[]`.
|
||||
*/
|
||||
export function isArray(val: any): val is unknown[] {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return Array.isArray(val)
|
||||
}
|
@ -2,6 +2,7 @@ import { Text } from '@codemirror/state'
|
||||
import { Marked } from '@ts-stack/markdown'
|
||||
|
||||
import type * as LSP from 'vscode-languageserver-protocol'
|
||||
import { isArray } from '../lib/utils'
|
||||
|
||||
// takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset
|
||||
export function deferExecution<T>(func: (args: T) => any, wait: number) {
|
||||
@ -45,7 +46,7 @@ export function offsetToPos(doc: Text, offset: number) {
|
||||
export function formatMarkdownContents(
|
||||
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
|
||||
): string {
|
||||
if (Array.isArray(contents)) {
|
||||
if (isArray(contents)) {
|
||||
return contents.map((c) => formatMarkdownContents(c) + '\n\n').join('')
|
||||
} else if (typeof contents === 'string') {
|
||||
return Marked.parse(contents)
|
||||
|
43
src/App.tsx
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useHotKeyListener } from './hooks/useHotKeyListener'
|
||||
import { Stream } from './components/Stream'
|
||||
import { AppHeader } from './components/AppHeader'
|
||||
@ -24,7 +24,12 @@ import { UnitsMenu } from 'components/UnitsMenu'
|
||||
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
||||
import { maybeWriteToDisk } from 'lib/telemetry'
|
||||
import { takeScreenshotOfVideoStreamCanvas } from 'lib/screenshot'
|
||||
import { writeProjectThumbnailFile } from 'lib/desktop'
|
||||
import { useRouteLoaderData } from 'react-router-dom'
|
||||
import { useEngineCommands } from 'components/EngineCommands'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { useToken } from 'machines/appMachine'
|
||||
maybeWriteToDisk()
|
||||
.then(() => {})
|
||||
.catch(() => {})
|
||||
@ -54,14 +59,20 @@ export function App() {
|
||||
|
||||
const projectName = project?.name || null
|
||||
const projectPath = project?.path || null
|
||||
|
||||
const [commands] = useEngineCommands()
|
||||
const [capturedCanvas, setCapturedCanvas] = useState(false)
|
||||
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
const lastCommandType = commands[commands.length - 1]?.type
|
||||
|
||||
useEffect(() => {
|
||||
onProjectOpen({ name: projectName, path: projectPath }, file || null)
|
||||
}, [projectName, projectPath])
|
||||
|
||||
useHotKeyListener()
|
||||
|
||||
const { auth, settings } = useSettingsAuthContext()
|
||||
const token = auth?.context?.token
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const token = useToken()
|
||||
|
||||
const coreDumpManager = useMemo(
|
||||
() => new CoreDumpManager(engineCommandManager, codeManager, token),
|
||||
@ -91,6 +102,32 @@ export function App() {
|
||||
|
||||
useEngineConnectionSubscriptions()
|
||||
|
||||
// Generate thumbnail.png when loading the app
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDesktop() &&
|
||||
!capturedCanvas &&
|
||||
lastCommandType === 'execution-done'
|
||||
) {
|
||||
setTimeout(() => {
|
||||
const projectDirectoryWithoutEndingSlash = loaderData?.project?.path
|
||||
if (!projectDirectoryWithoutEndingSlash) {
|
||||
return
|
||||
}
|
||||
const dataUrl: string = takeScreenshotOfVideoStreamCanvas()
|
||||
// zoom to fit command does not wait, wait 500ms to see if zoom to fit finishes
|
||||
writeProjectThumbnailFile(dataUrl, projectDirectoryWithoutEndingSlash)
|
||||
.then(() => {})
|
||||
.catch((e) => {
|
||||
console.error(
|
||||
`Failed to generate thumbnail for ${projectDirectoryWithoutEndingSlash}`
|
||||
)
|
||||
console.error(e)
|
||||
})
|
||||
}, 500)
|
||||
}
|
||||
}, [lastCommandType])
|
||||
|
||||
return (
|
||||
<div className="relative h-full flex flex-col" ref={ref}>
|
||||
<AppHeader
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { useAuthState } from 'machines/appMachine'
|
||||
import Loading from './components/Loading'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
|
||||
// Wrapper around protected routes, used in src/Router.tsx
|
||||
export const Auth = ({ children }: React.PropsWithChildren) => {
|
||||
const { auth } = useSettingsAuthContext()
|
||||
const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
|
||||
const authState = useAuthState()
|
||||
const isLoggingIn = authState.matches('checkIfLoggedIn')
|
||||
|
||||
return isLoggingIn ? (
|
||||
<Loading>
|
||||
|
@ -37,7 +37,6 @@ import { KclContextProvider } from 'lang/KclProvider'
|
||||
import { ASK_TO_OPEN_QUERY_PARAM, BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import toast from 'react-hot-toast'
|
||||
import { coreDump } from 'lang/wasm'
|
||||
@ -47,6 +46,7 @@ import { reportRejection } from 'lib/trap'
|
||||
import { RouteProvider } from 'components/RouteProvider'
|
||||
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
||||
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
|
||||
import { useToken } from 'machines/appMachine'
|
||||
|
||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||
|
||||
@ -203,8 +203,7 @@ export const Router = () => {
|
||||
}
|
||||
|
||||
function CoreDump() {
|
||||
const { auth } = useSettingsAuthContext()
|
||||
const token = auth?.context?.token
|
||||
const token = useToken()
|
||||
const coreDumpManager = useMemo(
|
||||
() => new CoreDumpManager(engineCommandManager, codeManager, token),
|
||||
[]
|
||||
|
@ -22,6 +22,7 @@ import {
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
export function Toolbar({
|
||||
className = '',
|
||||
@ -121,7 +122,7 @@ export function Toolbar({
|
||||
return toolbarConfig[currentMode].items.map((maybeIconConfig) => {
|
||||
if (maybeIconConfig === 'break') {
|
||||
return 'break'
|
||||
} else if (Array.isArray(maybeIconConfig)) {
|
||||
} else if (isArray(maybeIconConfig)) {
|
||||
return maybeIconConfig.map(resolveItemConfig)
|
||||
} else {
|
||||
return resolveItemConfig(maybeIconConfig)
|
||||
@ -180,7 +181,7 @@ export function Toolbar({
|
||||
className="h-5 w-[1px] block bg-chalkboard-30 dark:bg-chalkboard-80"
|
||||
/>
|
||||
)
|
||||
} else if (Array.isArray(maybeIconConfig)) {
|
||||
} else if (isArray(maybeIconConfig)) {
|
||||
// A button with a dropdown
|
||||
return (
|
||||
<ActionButtonDropdown
|
||||
|
@ -29,6 +29,7 @@ import * as TWEEN from '@tweenjs/tween.js'
|
||||
import { isQuaternionVertical } from './helpers'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
|
||||
import { CameraDragInteractionType_type } from '@kittycad/lib/dist/types/src/models'
|
||||
|
||||
const ORTHOGRAPHIC_CAMERA_SIZE = 20
|
||||
const FRAMES_TO_ANIMATE_IN = 30
|
||||
@ -406,7 +407,7 @@ export class CameraControls {
|
||||
.sub(this.mouseDownPosition)
|
||||
this.mouseDownPosition.copy(this.mouseNewPosition)
|
||||
|
||||
const interaction = this.getInteractionType(event)
|
||||
let interaction = this.getInteractionType(event)
|
||||
if (interaction === 'none') return
|
||||
|
||||
// If there's a valid interaction and the mouse is moving,
|
||||
@ -753,8 +754,6 @@ export class CameraControls {
|
||||
didChange = true
|
||||
}
|
||||
|
||||
this.safeLookAtTarget(this.camera.up)
|
||||
|
||||
// Update the camera's matrices
|
||||
this.camera.updateMatrixWorld()
|
||||
if (didChange || forceUpdate) {
|
||||
@ -1189,14 +1188,24 @@ export class CameraControls {
|
||||
this.deferReactUpdate(this.reactCameraProperties)
|
||||
Object.values(this._camChangeCallbacks).forEach((cb) => cb())
|
||||
}
|
||||
getInteractionType = (event: MouseEvent) =>
|
||||
_getInteractionType(
|
||||
getInteractionType = (
|
||||
event: MouseEvent
|
||||
): CameraDragInteractionType_type | 'none' => {
|
||||
const initialInteractionType = _getInteractionType(
|
||||
this.interactionGuards,
|
||||
event,
|
||||
this.enablePan,
|
||||
this.enableRotate,
|
||||
this.enableZoom
|
||||
)
|
||||
if (
|
||||
initialInteractionType === 'rotate' &&
|
||||
this.engineCommandManager.settings.cameraOrbit === 'trackball'
|
||||
) {
|
||||
return 'rotatetrackball'
|
||||
}
|
||||
return initialInteractionType
|
||||
}
|
||||
}
|
||||
|
||||
// Pure function helpers
|
||||
|
@ -2,11 +2,11 @@ import { Toolbar } from '../Toolbar'
|
||||
import UserSidebarMenu from 'components/UserSidebarMenu'
|
||||
import { type IndexLoaderData } from 'lib/types'
|
||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import styles from './AppHeader.module.css'
|
||||
import { RefreshButton } from 'components/RefreshButton'
|
||||
import { CommandBarOpenButton } from './CommandBarOpenButton'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { useUser } from 'machines/appMachine'
|
||||
|
||||
interface AppHeaderProps extends React.PropsWithChildren {
|
||||
showToolbar?: boolean
|
||||
@ -24,8 +24,7 @@ export const AppHeader = ({
|
||||
style,
|
||||
enableMenu = false,
|
||||
}: AppHeaderProps) => {
|
||||
const { auth } = useSettingsAuthContext()
|
||||
const user = auth?.context?.user
|
||||
const user = useUser()
|
||||
|
||||
return (
|
||||
<header
|
||||
|
@ -7,6 +7,7 @@ import { trap } from 'lib/trap'
|
||||
import { codeToIdSelections } from 'lib/selections'
|
||||
import { codeRefFromRange } from 'lang/std/artifactGraph'
|
||||
import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
export function AstExplorer() {
|
||||
const { context } = useModelingContext()
|
||||
@ -166,12 +167,12 @@ function DisplayObj({
|
||||
{Object.entries(obj).map(([key, value]) => {
|
||||
if (filterKeys.includes(key)) {
|
||||
return null
|
||||
} else if (Array.isArray(value)) {
|
||||
} else if (isArray(value)) {
|
||||
return (
|
||||
<li key={key}>
|
||||
{`${key}: [`}
|
||||
<DisplayBody
|
||||
body={value}
|
||||
body={value as any}
|
||||
filterKeys={filterKeys}
|
||||
node={node}
|
||||
/>
|
||||
|
@ -30,6 +30,7 @@ import {
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { useToken } from 'machines/appMachine'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -47,7 +48,8 @@ export const FileMachineProvider = ({
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const { settings, auth } = useSettingsAuthContext()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const token = useToken()
|
||||
const projectData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
|
||||
const { project, file } = projectData
|
||||
const [kclSamples, setKclSamples] = React.useState<KclSamplesManifestItem[]>(
|
||||
@ -122,23 +124,44 @@ export const FileMachineProvider = ({
|
||||
let createdName = input.name.trim() || DEFAULT_FILE_NAME
|
||||
let createdPath: string
|
||||
|
||||
if (input.makeDir) {
|
||||
if (
|
||||
(input.targetPathToClone &&
|
||||
(await window.electron.statIsDirectory(
|
||||
input.targetPathToClone
|
||||
))) ||
|
||||
input.makeDir
|
||||
) {
|
||||
let { name, path } = getNextDirName({
|
||||
entryName: createdName,
|
||||
baseDir: input.selectedDirectory.path,
|
||||
entryName: input.targetPathToClone
|
||||
? window.electron.path.basename(input.targetPathToClone)
|
||||
: createdName,
|
||||
baseDir: input.targetPathToClone
|
||||
? window.electron.path.dirname(input.targetPathToClone)
|
||||
: input.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
await window.electron.mkdir(createdPath)
|
||||
} else {
|
||||
const { name, path } = getNextFileName({
|
||||
entryName: createdName,
|
||||
baseDir: input.selectedDirectory.path,
|
||||
entryName: input.targetPathToClone
|
||||
? window.electron.path.basename(input.targetPathToClone)
|
||||
: createdName,
|
||||
baseDir: input.targetPathToClone
|
||||
? window.electron.path.dirname(input.targetPathToClone)
|
||||
: input.selectedDirectory.path,
|
||||
})
|
||||
createdName = name
|
||||
createdPath = path
|
||||
if (input.targetPathToClone) {
|
||||
await window.electron.copyFile(
|
||||
input.targetPathToClone,
|
||||
createdPath
|
||||
)
|
||||
} else {
|
||||
await window.electron.writeFile(createdPath, input.content ?? '')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Successfully created "${createdName}"`,
|
||||
@ -297,7 +320,7 @@ export const FileMachineProvider = ({
|
||||
const kclCommandMemo = useMemo(
|
||||
() =>
|
||||
kclCommands({
|
||||
authToken: auth?.context?.token ?? '',
|
||||
authToken: token ?? '',
|
||||
projectData,
|
||||
settings: {
|
||||
defaultUnit: settings?.context?.modeling.defaultUnit.current ?? 'mm',
|
||||
|
@ -153,6 +153,7 @@ const FileTreeItem = ({
|
||||
onClickDirectory,
|
||||
onCreateFile,
|
||||
onCreateFolder,
|
||||
onCloneFileOrFolder,
|
||||
newTreeEntry,
|
||||
level = 0,
|
||||
treeSelection,
|
||||
@ -171,6 +172,7 @@ const FileTreeItem = ({
|
||||
) => void
|
||||
onCreateFile: (name: string) => void
|
||||
onCreateFolder: (name: string) => void
|
||||
onCloneFileOrFolder: (path: string) => void
|
||||
newTreeEntry: TreeEntry
|
||||
level?: number
|
||||
treeSelection: FileEntry | undefined
|
||||
@ -403,6 +405,7 @@ const FileTreeItem = ({
|
||||
currentFile={currentFile}
|
||||
onCreateFile={onCreateFile}
|
||||
onCreateFolder={onCreateFolder}
|
||||
onCloneFileOrFolder={onCloneFileOrFolder}
|
||||
newTreeEntry={newTreeEntry}
|
||||
lastDirectoryClicked={lastDirectoryClicked}
|
||||
onClickDirectory={onClickDirectory}
|
||||
@ -441,6 +444,7 @@ const FileTreeItem = ({
|
||||
itemRef={itemRef}
|
||||
onRename={addCurrentItemToRenaming}
|
||||
onDelete={() => setIsConfirmingDelete(true)}
|
||||
onClone={() => onCloneFileOrFolder(fileOrDir.path)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@ -450,12 +454,14 @@ interface FileTreeContextMenuProps {
|
||||
itemRef: React.RefObject<HTMLElement>
|
||||
onRename: () => void
|
||||
onDelete: () => void
|
||||
onClone: () => void
|
||||
}
|
||||
|
||||
function FileTreeContextMenu({
|
||||
itemRef,
|
||||
onRename,
|
||||
onDelete,
|
||||
onClone,
|
||||
}: FileTreeContextMenuProps) {
|
||||
const platform = usePlatform()
|
||||
const metaKey = platform === 'macos' ? '⌘' : 'Ctrl'
|
||||
@ -478,6 +484,13 @@ function FileTreeContextMenu({
|
||||
>
|
||||
Delete
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem
|
||||
data-testid="context-menu-clone"
|
||||
onClick={onClone}
|
||||
hotkey=""
|
||||
>
|
||||
Clone
|
||||
</ContextMenuItem>,
|
||||
]}
|
||||
/>
|
||||
)
|
||||
@ -584,9 +597,22 @@ export const useFileTreeOperations = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function cloneFileOrDir(args: { path: string }) {
|
||||
send({
|
||||
type: 'Create file',
|
||||
data: {
|
||||
name: '',
|
||||
makeDir: false,
|
||||
shouldSetToRename: false,
|
||||
targetPathToClone: args.path,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
createFile,
|
||||
createFolder,
|
||||
cloneFileOrDir,
|
||||
newTreeEntry,
|
||||
}
|
||||
}
|
||||
@ -595,7 +621,8 @@ export const FileTree = ({
|
||||
className = '',
|
||||
onNavigateToFile: closePanel,
|
||||
}: FileTreeProps) => {
|
||||
const { createFile, createFolder, newTreeEntry } = useFileTreeOperations()
|
||||
const { createFile, createFolder, cloneFileOrDir, newTreeEntry } =
|
||||
useFileTreeOperations()
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
@ -611,6 +638,7 @@ export const FileTree = ({
|
||||
newTreeEntry={newTreeEntry}
|
||||
onCreateFile={(name: string) => createFile({ dryRun: false, name })}
|
||||
onCreateFolder={(name: string) => createFolder({ dryRun: false, name })}
|
||||
onCloneFileOrFolder={(path: string) => cloneFileOrDir({ path })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@ -620,10 +648,12 @@ export const FileTreeInner = ({
|
||||
onNavigateToFile,
|
||||
onCreateFile,
|
||||
onCreateFolder,
|
||||
onCloneFileOrFolder,
|
||||
newTreeEntry,
|
||||
}: {
|
||||
onCreateFile: (name: string) => void
|
||||
onCreateFolder: (name: string) => void
|
||||
onCloneFileOrFolder: (path: string) => void
|
||||
newTreeEntry: TreeEntry
|
||||
onNavigateToFile?: () => void
|
||||
}) => {
|
||||
@ -732,6 +762,7 @@ export const FileTreeInner = ({
|
||||
fileOrDir={fileOrDir}
|
||||
onCreateFile={onCreateFile}
|
||||
onCreateFolder={onCreateFolder}
|
||||
onCloneFileOrFolder={onCloneFileOrFolder}
|
||||
newTreeEntry={newTreeEntry}
|
||||
onClickDirectory={onClickDirectory}
|
||||
onNavigateToFile={onNavigateToFile_}
|
||||
|
@ -27,6 +27,7 @@ import { PROJECT_ENTRYPOINT } from 'lib/constants'
|
||||
import { err } from 'lib/trap'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { codeManager } from 'lib/singletons'
|
||||
import { useToken } from 'machines/appMachine'
|
||||
|
||||
function getWorkspaceFolders(): LSP.WorkspaceFolder[] {
|
||||
return []
|
||||
@ -69,8 +70,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [isKclLspReady, setIsKclLspReady] = useState(false)
|
||||
const [isCopilotLspReady, setIsCopilotLspReady] = useState(false)
|
||||
|
||||
const { auth } = useSettingsAuthContext()
|
||||
const token = auth?.context.token
|
||||
const token = useToken()
|
||||
const navigate = useNavigate()
|
||||
|
||||
// So this is a bit weird, we need to initialize the lsp server and client.
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { useEngineCommands } from './EngineCommands'
|
||||
import { Spinner } from './Spinner'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
|
||||
export const ModelStateIndicator = () => {
|
||||
const [commands] = useEngineCommands()
|
||||
|
||||
const lastCommandType = commands[commands.length - 1]?.type
|
||||
|
||||
let className = 'w-6 h-6 '
|
||||
|
@ -89,6 +89,7 @@ import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { promptToEditFlow } from 'lib/promptToEdit'
|
||||
import { kclEditorActor } from 'machines/kclEditorMachine'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { useToken } from 'machines/appMachine'
|
||||
|
||||
type MachineContext<T extends AnyStateMachine> = {
|
||||
state: StateFrom<T>
|
||||
@ -110,7 +111,6 @@ export const ModelingMachineProvider = ({
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const {
|
||||
auth,
|
||||
settings: {
|
||||
context: {
|
||||
app: { theme, enableSSAO, allowOrbitInSketchMode },
|
||||
@ -119,6 +119,7 @@ export const ModelingMachineProvider = ({
|
||||
cameraProjection,
|
||||
highlightEdges,
|
||||
showScaleGrid,
|
||||
cameraOrbit,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -127,7 +128,7 @@ export const ModelingMachineProvider = ({
|
||||
const navigate = useNavigate()
|
||||
const { context, send: fileMachineSend } = useFileContext()
|
||||
const { file } = useLoaderData() as IndexLoaderData
|
||||
const token = auth?.context?.token
|
||||
const token = useToken()
|
||||
const streamRef = useRef<HTMLDivElement>(null)
|
||||
const persistedContext = useMemo(() => getPersistedContext(), [])
|
||||
|
||||
@ -1154,6 +1155,7 @@ export const ModelingMachineProvider = ({
|
||||
enableSSAO: enableSSAO.current,
|
||||
showScaleGrid: showScaleGrid.current,
|
||||
cameraProjection: cameraProjection.current,
|
||||
cameraOrbit: cameraOrbit.current,
|
||||
},
|
||||
token
|
||||
)
|
||||
@ -1183,6 +1185,13 @@ export const ModelingMachineProvider = ({
|
||||
editorManager.selectionRanges = modelingState.context.selectionRanges
|
||||
}, [modelingState.context.selectionRanges])
|
||||
|
||||
// When changing camera modes reset the camera to the default orientation to correct
|
||||
// the up vector otherwise the conconical orientation for the camera modes will be
|
||||
// wrong
|
||||
useEffect(() => {
|
||||
sceneInfra.camControls.resetCameraPosition().catch(reportRejection)
|
||||
}, [cameraOrbit.current])
|
||||
|
||||
useEffect(() => {
|
||||
const onConnectionStateChanged = ({ detail }: CustomEvent) => {
|
||||
// If we are in sketch mode we need to exit it.
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
} from '@codemirror/state'
|
||||
import { EditorView } from '@codemirror/view'
|
||||
import { oneDark } from '@codemirror/theme-one-dark'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
//reference: https://github.com/sachinraja/rodemirror/blob/main/src/use-first-render.ts
|
||||
const useFirstRender = () => {
|
||||
@ -86,6 +87,18 @@ const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
|
||||
return <div ref={editor}></div>
|
||||
})
|
||||
|
||||
/**
|
||||
* The extensions type is quite weird. We need a special helper to preserve the
|
||||
* readonly array type.
|
||||
*
|
||||
* @see https://github.com/microsoft/TypeScript/issues/17002
|
||||
*/
|
||||
function isExtensionArray(
|
||||
extensions: Extension
|
||||
): extensions is readonly Extension[] {
|
||||
return isArray(extensions)
|
||||
}
|
||||
|
||||
export function useCodeMirror(props: UseCodeMirror) {
|
||||
const {
|
||||
onCreateEditor,
|
||||
@ -103,7 +116,7 @@ export function useCodeMirror(props: UseCodeMirror) {
|
||||
const isFirstRender = useFirstRender()
|
||||
|
||||
const targetExtensions = useMemo(() => {
|
||||
let exts = Array.isArray(extensions) ? extensions : []
|
||||
let exts = isExtensionArray(extensions) ? extensions : []
|
||||
if (theme === 'dark') {
|
||||
exts = [...exts, oneDark]
|
||||
} else if (theme === 'light') {
|
||||
|
@ -122,7 +122,8 @@ export const sidebarPanes: SidebarPane[] = [
|
||||
icon: 'folder',
|
||||
sidebarName: 'Project Files',
|
||||
Content: (props: { id: SidebarType; onClose: () => void }) => {
|
||||
const { createFile, createFolder, newTreeEntry } = useFileTreeOperations()
|
||||
const { createFile, createFolder, cloneFileOrDir, newTreeEntry } =
|
||||
useFileTreeOperations()
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -143,6 +144,7 @@ export const sidebarPanes: SidebarPane[] = [
|
||||
onCreateFolder={(name: string) =>
|
||||
createFolder({ dryRun: false, name })
|
||||
}
|
||||
onCloneFileOrFolder={(path: string) => cloneFileOrDir({ path })}
|
||||
newTreeEntry={newTreeEntry}
|
||||
/>
|
||||
</>
|
||||
|
@ -33,7 +33,7 @@ export const OpenInDesktopAppHandler = (props: React.PropsWithChildren) => {
|
||||
function onOpenInDesktopApp() {
|
||||
const newSearchParams = new URLSearchParams(globalThis.location.search)
|
||||
newSearchParams.delete(ASK_TO_OPEN_QUERY_PARAM)
|
||||
const newURL = `${ZOO_STUDIO_PROTOCOL}${globalThis.location.pathname.replace(
|
||||
const newURL = `${ZOO_STUDIO_PROTOCOL}://${globalThis.location.pathname.replace(
|
||||
'/',
|
||||
''
|
||||
)}${searchParams.size > 0 ? `?${newSearchParams.toString()}` : ''}`
|
||||
|
@ -2,7 +2,7 @@ import { FormEvent, useEffect, useRef, useState } from 'react'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { ActionButton } from '../ActionButton'
|
||||
import { FILE_EXT } from 'lib/constants'
|
||||
import { FILE_EXT, PROJECT_IMAGE_NAME } from 'lib/constants'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import Tooltip from '../Tooltip'
|
||||
import { DeleteConfirmationDialog } from './DeleteProjectDialog'
|
||||
@ -29,7 +29,7 @@ function ProjectCard({
|
||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||
const [numberOfFiles, setNumberOfFiles] = useState(1)
|
||||
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
||||
// const [imageUrl, setImageUrl] = useState('')
|
||||
const [imageUrl, setImageUrl] = useState('')
|
||||
|
||||
let inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
@ -53,18 +53,21 @@ function ProjectCard({
|
||||
setNumberOfFolders(project.directory_count)
|
||||
}
|
||||
|
||||
// async function setupImageUrl() {
|
||||
// const projectImagePath = await join(project.file.path, PROJECT_IMAGE_NAME)
|
||||
// if (await exists(projectImagePath)) {
|
||||
// const imageData = await readFile(projectImagePath)
|
||||
// const blob = new Blob([imageData], { type: 'image/jpg' })
|
||||
// const imageUrl = URL.createObjectURL(blob)
|
||||
// setImageUrl(imageUrl)
|
||||
// }
|
||||
// }
|
||||
async function setupImageUrl() {
|
||||
const projectImagePath = window.electron.path.join(
|
||||
project.path,
|
||||
PROJECT_IMAGE_NAME
|
||||
)
|
||||
if (await window.electron.exists(projectImagePath)) {
|
||||
const imageData = await window.electron.readFile(projectImagePath)
|
||||
const blob = new Blob([imageData], { type: 'image/png' })
|
||||
const imageUrl = URL.createObjectURL(blob)
|
||||
setImageUrl(imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
void getNumberOfFiles()
|
||||
// void setupImageUrl()
|
||||
void setupImageUrl()
|
||||
}, [project.kcl_file_count, project.directory_count])
|
||||
|
||||
useEffect(() => {
|
||||
@ -84,7 +87,7 @@ function ProjectCard({
|
||||
to={`${PATHS.FILE}/${encodeURIComponent(project.default_file)}`}
|
||||
className="flex flex-col flex-1 !no-underline !text-chalkboard-110 dark:!text-chalkboard-10 group-hover:!hue-rotate-0 min-h-[5em] divide-y divide-primary/40 dark:divide-chalkboard-80 group-hover:!divide-primary"
|
||||
>
|
||||
{/* <div className="h-36 relative overflow-hidden bg-gradient-to-b from-transparent to-primary/10 rounded-t-sm">
|
||||
<div className="h-36 relative overflow-hidden bg-gradient-to-b from-transparent to-primary/10 rounded-t-sm">
|
||||
{imageUrl && (
|
||||
<img
|
||||
src={imageUrl}
|
||||
@ -92,7 +95,7 @@ function ProjectCard({
|
||||
className="h-full w-full transition-transform group-hover:scale-105 object-cover"
|
||||
/>
|
||||
)}
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="pb-2 flex flex-col flex-grow flex-auto gap-2 rounded-b-sm">
|
||||
{isEditing ? (
|
||||
<ProjectCardRenameForm
|
||||
|
@ -19,7 +19,8 @@ import { commandBarActor } from 'machines/commandBarMachine'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import { copyFileShareLink } from 'lib/links'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { DEV } from 'env'
|
||||
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
|
||||
import { useToken } from 'machines/appMachine'
|
||||
|
||||
const ProjectSidebarMenu = ({
|
||||
project,
|
||||
@ -103,13 +104,15 @@ function ProjectMenuPopover({
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const { settings, auth } = useSettingsAuthContext()
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const token = useToken()
|
||||
const machineManager = useContext(MachineManagerContext)
|
||||
const commands = useSelector(commandBarActor, commandsSelector)
|
||||
|
||||
const { onProjectClose } = useLspContext()
|
||||
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
||||
const makeCommandInfo = { name: 'Make', groupId: 'modeling' }
|
||||
const shareCommandInfo = { name: 'share-file-link', groupId: 'code' }
|
||||
const findCommand = (obj: { name: string; groupId: string }) =>
|
||||
Boolean(
|
||||
commands.find((c) => c.name === obj.name && c.groupId === obj.groupId)
|
||||
@ -191,10 +194,10 @@ function ProjectMenuPopover({
|
||||
id: 'share-link',
|
||||
Element: 'button',
|
||||
children: 'Share link to file',
|
||||
disabled: !DEV,
|
||||
disabled: IS_NIGHTLY_OR_DEBUG || !findCommand(shareCommandInfo),
|
||||
onClick: async () => {
|
||||
await copyFileShareLink({
|
||||
token: auth?.context.token || '',
|
||||
token: token ?? '',
|
||||
code: codeManager.code,
|
||||
name: project?.name || '',
|
||||
units: settings.context.modeling.defaultUnit.current,
|
||||
|
@ -8,10 +8,10 @@ import Tooltip from './Tooltip'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { toSync } from 'lib/utils'
|
||||
import { useToken } from 'machines/appMachine'
|
||||
|
||||
export const RefreshButton = ({ children }: React.PropsWithChildren) => {
|
||||
const { auth } = useSettingsAuthContext()
|
||||
const token = auth?.context?.token
|
||||
const token = useToken()
|
||||
const coreDumpManager = useMemo(
|
||||
() => new CoreDumpManager(engineCommandManager, codeManager, token),
|
||||
[]
|
||||
|
@ -2,10 +2,12 @@ import { useEffect, useState, createContext, ReactNode } from 'react'
|
||||
import { useNavigation, useLocation } from 'react-router-dom'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { useAuthNavigation } from 'hooks/useAuthNavigation'
|
||||
|
||||
export const RouteProviderContext = createContext({})
|
||||
|
||||
export function RouteProvider({ children }: { children: ReactNode }) {
|
||||
useAuthNavigation()
|
||||
const [first, setFirstState] = useState(true)
|
||||
const navigation = useNavigation()
|
||||
const location = useLocation()
|
||||
|
@ -2,10 +2,7 @@ import { trap } from 'lib/trap'
|
||||
import { useMachine, useSelector } from '@xstate/react'
|
||||
import { useNavigate, useRouteLoaderData, useLocation } from 'react-router-dom'
|
||||
import { PATHS, BROWSER_PATH } from 'lib/paths'
|
||||
import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
|
||||
import withBaseUrl from '../lib/withBaseURL'
|
||||
import React, { createContext, useEffect, useState } from 'react'
|
||||
import useStateMachineCommands from '../hooks/useStateMachineCommands'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
@ -16,7 +13,6 @@ import {
|
||||
} from 'lib/theme'
|
||||
import decamelize from 'decamelize'
|
||||
import { Actor, AnyStateMachine, ContextFrom, Prop, StateFrom } from 'xstate'
|
||||
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
|
||||
import {
|
||||
kclManager,
|
||||
sceneInfra,
|
||||
@ -50,7 +46,6 @@ type MachineContext<T extends AnyStateMachine> = {
|
||||
}
|
||||
|
||||
type SettingsAuthContextType = {
|
||||
auth: MachineContext<typeof authMachine>
|
||||
settings: MachineContext<typeof settingsMachine>
|
||||
}
|
||||
|
||||
@ -370,40 +365,9 @@ export const SettingsAuthProviderBase = ({
|
||||
)
|
||||
}, [settingsState.context.textEditor.blinkingCursor.current])
|
||||
|
||||
// Auth machine setup
|
||||
const [authState, authSend, authActor] = useMachine(
|
||||
authMachine.provide({
|
||||
actions: {
|
||||
goToSignInPage: () => {
|
||||
navigate(PATHS.SIGN_IN)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
logout()
|
||||
},
|
||||
goToIndexPage: () => {
|
||||
if (location.pathname.includes(PATHS.SIGN_IN)) {
|
||||
navigate(PATHS.INDEX)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
useStateMachineCommands({
|
||||
machineId: 'auth',
|
||||
state: authState,
|
||||
send: authSend,
|
||||
commandBarConfig: authCommandBarConfig,
|
||||
actor: authActor,
|
||||
})
|
||||
|
||||
return (
|
||||
<SettingsAuthContext.Provider
|
||||
value={{
|
||||
auth: {
|
||||
state: authState,
|
||||
context: authState.context,
|
||||
send: authSend,
|
||||
},
|
||||
settings: {
|
||||
state: settingsState,
|
||||
context: settingsState.context,
|
||||
@ -417,12 +381,3 @@ export const SettingsAuthProviderBase = ({
|
||||
}
|
||||
|
||||
export default SettingsAuthProvider
|
||||
|
||||
export async function logout() {
|
||||
localStorage.removeItem(TOKEN_PERSIST_KEY)
|
||||
if (isDesktop()) return Promise.resolve(null)
|
||||
return fetch(withBaseUrl('/logout'), {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Fragment, useMemo, useState } from 'react'
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
|
||||
import Tooltip from './Tooltip'
|
||||
import usePlatform from 'hooks/usePlatform'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import { authActor } from 'machines/appMachine'
|
||||
|
||||
type User = Models['User_type']
|
||||
|
||||
@ -20,7 +20,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
|
||||
const displayedName = getDisplayName(user)
|
||||
const [imageLoadFailed, setImageLoadFailed] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const send = useSettingsAuthContext()?.auth?.send
|
||||
const send = authActor.send
|
||||
|
||||
// We filter this memoized list so that no orphan "break" elements are rendered.
|
||||
const userMenuItems = useMemo<(ActionButtonProps | 'break')[]>(
|
||||
|
@ -45,10 +45,10 @@ export const lineHighlightField = StateField.define({
|
||||
})
|
||||
|
||||
const matchDeco = Decoration.mark({
|
||||
class: 'bg-yellow-300/70',
|
||||
class: 'bg-yellow-300/70 dark:bg-blue-800/50',
|
||||
attributes: { 'data-testid': 'hover-highlight' },
|
||||
})
|
||||
const matchDeco2 = Decoration.mark({
|
||||
class: 'bg-yellow-200/40',
|
||||
class: 'bg-yellow-200/40 dark:bg-blue-700/50',
|
||||
attributes: { 'data-testid': 'hover-highlight' },
|
||||
})
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
import { Range, Extension, Text } from '@codemirror/state'
|
||||
import { NodeProp, Tree } from '@lezer/common'
|
||||
import { language, syntaxTree } from '@codemirror/language'
|
||||
import { isArray } from 'lib/utils'
|
||||
|
||||
interface PickerState {
|
||||
from: number
|
||||
@ -79,7 +80,7 @@ function discoverColorsInKCL(
|
||||
)
|
||||
|
||||
if (maybeWidgetOptions) {
|
||||
if (Array.isArray(maybeWidgetOptions)) {
|
||||
if (isArray(maybeWidgetOptions)) {
|
||||
console.error('Unexpected nested overlays')
|
||||
ret.push(...maybeWidgetOptions)
|
||||
} else {
|
||||
@ -150,7 +151,7 @@ function colorPickersDecorations(
|
||||
return
|
||||
}
|
||||
|
||||
if (!Array.isArray(maybeWidgetOptions)) {
|
||||
if (!isArray(maybeWidgetOptions)) {
|
||||
widgets.push(
|
||||
Decoration.widget({
|
||||
widget: new ColorPickerWidget(maybeWidgetOptions),
|
||||
|
@ -10,6 +10,7 @@ export const VITE_KC_API_WS_MODELING_URL = env.VITE_KC_API_WS_MODELING_URL as
|
||||
| undefined
|
||||
export const VITE_KC_API_BASE_URL = env.VITE_KC_API_BASE_URL as string
|
||||
export const VITE_KC_SITE_BASE_URL = env.VITE_KC_SITE_BASE_URL as string
|
||||
export const VITE_KC_SITE_APP_URL = env.VITE_KC_SITE_APP_URL as string
|
||||
export const VITE_KC_SKIP_AUTH = env.VITE_KC_SKIP_AUTH as string | undefined
|
||||
export const VITE_KC_CONNECTION_TIMEOUT_MS =
|
||||
env.VITE_KC_CONNECTION_TIMEOUT_MS as string | undefined
|
||||
|
29
src/hooks/useAuthNavigation.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { PATHS } from 'lib/paths'
|
||||
import { useAuthState } from 'machines/appMachine'
|
||||
import { useEffect } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
/**
|
||||
* A simple hook that listens to the auth state of the app and navigates
|
||||
* accordingly.
|
||||
*/
|
||||
export function useAuthNavigation() {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const authState = useAuthState()
|
||||
|
||||
// Subscribe to the auth state of the app and navigate accordingly.
|
||||
useEffect(() => {
|
||||
if (
|
||||
authState.matches('loggedIn') &&
|
||||
location.pathname.includes(PATHS.SIGN_IN)
|
||||
) {
|
||||
navigate(PATHS.INDEX)
|
||||
} else if (
|
||||
authState.matches('loggedOut') &&
|
||||
!location.pathname.includes(PATHS.SIGN_IN)
|
||||
) {
|
||||
navigate(PATHS.SIGN_IN)
|
||||
}
|
||||
}, [authState])
|
||||
}
|
@ -16,14 +16,15 @@ export function useSetupEngineManager(
|
||||
streamRef: React.RefObject<HTMLDivElement>,
|
||||
modelingSend: ReturnType<typeof useModelingContext>['send'],
|
||||
modelingContext: ReturnType<typeof useModelingContext>['context'],
|
||||
settings = {
|
||||
settings: SettingsViaQueryString = {
|
||||
pool: null,
|
||||
theme: Themes.System,
|
||||
highlightEdges: true,
|
||||
enableSSAO: true,
|
||||
showScaleGrid: false,
|
||||
cameraProjection: 'perspective',
|
||||
} as SettingsViaQueryString,
|
||||
cameraOrbit: 'spherical',
|
||||
},
|
||||
token?: string
|
||||
) {
|
||||
const networkContext = useNetworkContext()
|
||||
|
@ -57,6 +57,7 @@ import { ExtrudeFacePlane } from 'machines/modelingMachine'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { KclExpressionWithVariable } from 'lib/commandTypes'
|
||||
import { findKwArg } from './util'
|
||||
import { deleteEdgeTreatment } from './modifyAst/addEdgeTreatment'
|
||||
|
||||
export function startSketchOnDefault(
|
||||
node: Node<Program>,
|
||||
@ -1467,6 +1468,8 @@ export async function deleteFromSelection(
|
||||
}
|
||||
// await prom
|
||||
return astClone
|
||||
} else if (selection.artifact?.type === 'edgeCut') {
|
||||
return deleteEdgeTreatment(astClone, selection)
|
||||
} else if (varDec.node.init.type === 'PipeExpression') {
|
||||
const pipeBody = varDec.node.init.body
|
||||
if (
|
||||
|
@ -21,13 +21,19 @@ import {
|
||||
FilletParameters,
|
||||
ChamferParameters,
|
||||
EdgeTreatmentParameters,
|
||||
deleteEdgeTreatment,
|
||||
} from './addEdgeTreatment'
|
||||
import { getNodeFromPath } from '../queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { createLiteral } from 'lang/modifyAst'
|
||||
import { err } from 'lib/trap'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import {
|
||||
codeManager,
|
||||
editorManager,
|
||||
engineCommandManager,
|
||||
kclManager,
|
||||
} from 'lib/singletons'
|
||||
import { VITE_KC_DEV_TOKEN } from 'env'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
import { codeRefFromRange } from 'lang/std/artifactGraph'
|
||||
@ -55,6 +61,13 @@ afterAll(() => {
|
||||
engineCommandManager.tearDown()
|
||||
})
|
||||
|
||||
const dependencies = {
|
||||
kclManager,
|
||||
engineCommandManager,
|
||||
editorManager,
|
||||
codeManager,
|
||||
}
|
||||
|
||||
const runGetPathToExtrudeForSegmentSelectionTest = async (
|
||||
code: string,
|
||||
selectedSegmentSnippet: string,
|
||||
@ -133,7 +146,8 @@ const runGetPathToExtrudeForSegmentSelectionTest = async (
|
||||
const pathResult = getPathToExtrudeForSegmentSelection(
|
||||
ast,
|
||||
selection,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
dependencies
|
||||
)
|
||||
if (err(pathResult)) return pathResult
|
||||
const { pathToExtrudeNode } = pathResult
|
||||
@ -290,8 +304,13 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
|
||||
otherSelections: [],
|
||||
}
|
||||
|
||||
// apply edge treatment to seleciton
|
||||
const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
|
||||
// apply edge treatment to selection
|
||||
const result = modifyAstWithEdgeTreatmentAndTag(
|
||||
ast,
|
||||
selection,
|
||||
parameters,
|
||||
dependencies
|
||||
)
|
||||
if (err(result)) {
|
||||
return result
|
||||
}
|
||||
@ -301,6 +320,46 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
|
||||
|
||||
expect(newCode).toContain(expectedCode)
|
||||
}
|
||||
const runDeleteEdgeTreatmentTest = async (
|
||||
code: string,
|
||||
edgeTreatmentSnippet: string,
|
||||
expectedCode: string
|
||||
) => {
|
||||
// parse ast
|
||||
const ast = assertParse(code)
|
||||
|
||||
// update artifact graph
|
||||
await kclManager.executeAst({ ast })
|
||||
const artifactGraph = engineCommandManager.artifactGraph
|
||||
|
||||
// define snippet range
|
||||
const edgeTreatmentRange = topLevelRange(
|
||||
code.indexOf(edgeTreatmentSnippet),
|
||||
code.indexOf(edgeTreatmentSnippet) + edgeTreatmentSnippet.length
|
||||
)
|
||||
|
||||
// find artifact
|
||||
const maybeArtifact = [...artifactGraph].find(([, artifact]) => {
|
||||
if (!('codeRef' in artifact)) return false
|
||||
return isOverlap(artifact.codeRef.range, edgeTreatmentRange)
|
||||
})
|
||||
|
||||
// build selection
|
||||
const selection: Selection = {
|
||||
codeRef: codeRefFromRange(edgeTreatmentRange, ast),
|
||||
artifact: maybeArtifact ? maybeArtifact[1] : undefined,
|
||||
}
|
||||
|
||||
// delete edge treatment
|
||||
const result = await deleteEdgeTreatment(ast, selection)
|
||||
if (err(result)) {
|
||||
return result
|
||||
}
|
||||
|
||||
// recast and check
|
||||
const newCode = recast(result)
|
||||
expect(newCode).toContain(expectedCode)
|
||||
}
|
||||
const createFilletParameters = (radiusValue: number): FilletParameters => ({
|
||||
type: EdgeTreatmentType.Fillet,
|
||||
radius: {
|
||||
@ -577,6 +636,191 @@ extrude002 = extrude(sketch002, length = -25)
|
||||
)
|
||||
})
|
||||
})
|
||||
describe(`Testing deleteEdgeTreatment with ${edgeTreatmentType}s`, () => {
|
||||
// simple cases
|
||||
it(`should delete a piped ${edgeTreatmentType} from a single segment`, async () => {
|
||||
const code = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, 10], %)
|
||||
|> line(end = [20, 0])
|
||||
|> line(end = [0, -20])
|
||||
|> line(end = [-20, 0], tag = $seg01)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -15)
|
||||
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)`
|
||||
const edgeTreatmentSnippet = `${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)`
|
||||
const expectedCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, 10], %)
|
||||
|> line(end = [20, 0])
|
||||
|> line(end = [0, -20])
|
||||
|> line(end = [-20, 0], tag = $seg01)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -15)`
|
||||
|
||||
await runDeleteEdgeTreatmentTest(
|
||||
code,
|
||||
edgeTreatmentSnippet,
|
||||
expectedCode
|
||||
)
|
||||
})
|
||||
it(`should delete a non-piped ${edgeTreatmentType} from a single segment`, async () => {
|
||||
const code = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, 10], %)
|
||||
|> line(end = [20, 0])
|
||||
|> line(end = [0, -20])
|
||||
|> line(end = [-20, 0], tag = $seg01)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -15)
|
||||
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, extrude001)`
|
||||
const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, extrude001)`
|
||||
const expectedCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, 10], %)
|
||||
|> line(end = [20, 0])
|
||||
|> line(end = [0, -20])
|
||||
|> line(end = [-20, 0], tag = $seg01)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -15)`
|
||||
|
||||
await runDeleteEdgeTreatmentTest(
|
||||
code,
|
||||
edgeTreatmentSnippet,
|
||||
expectedCode
|
||||
)
|
||||
})
|
||||
// getOppositeEdge and getNextAdjacentEdge cases
|
||||
it(`should delete a piped ${edgeTreatmentType} tagged with getOppositeEdge`, async () => {
|
||||
const code = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, 10], %)
|
||||
|> line(end = [20, 0])
|
||||
|> line(end = [0, -20])
|
||||
|> line(end = [-20, 0], tag = $seg01)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -15)
|
||||
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [getOppositeEdge(seg01)] }, extrude001)`
|
||||
const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [getOppositeEdge(seg01)] }, extrude001)`
|
||||
const expectedCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, 10], %)
|
||||
|> line(end = [20, 0])
|
||||
|> line(end = [0, -20])
|
||||
|> line(end = [-20, 0], tag = $seg01)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -15)`
|
||||
|
||||
await runDeleteEdgeTreatmentTest(
|
||||
code,
|
||||
edgeTreatmentSnippet,
|
||||
expectedCode
|
||||
)
|
||||
})
|
||||
it(`should delete a non-piped ${edgeTreatmentType} tagged with getNextAdjacentEdge`, async () => {
|
||||
const code = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, 10], %)
|
||||
|> line(end = [20, 0])
|
||||
|> line(end = [0, -20])
|
||||
|> line(end = [-20, 0], tag = $seg01)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -15)
|
||||
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [getNextAdjacentEdge(seg01)] }, extrude001)`
|
||||
const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}({ ${parameterName} = 3, tags = [getNextAdjacentEdge(seg01)] }, extrude001)`
|
||||
const expectedCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, 10], %)
|
||||
|> line(end = [20, 0])
|
||||
|> line(end = [0, -20])
|
||||
|> line(end = [-20, 0], tag = $seg01)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -15)`
|
||||
|
||||
await runDeleteEdgeTreatmentTest(
|
||||
code,
|
||||
edgeTreatmentSnippet,
|
||||
expectedCode
|
||||
)
|
||||
})
|
||||
// cases with several edge treatments
|
||||
it(`should delete a piped ${edgeTreatmentType} from a body with multiple treatments`, async () => {
|
||||
const code = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, 10], %)
|
||||
|> line(end = [20, 0], tag = $seg01)
|
||||
|> line(end = [0, -20])
|
||||
|> line(end = [-20, 0], tag = $seg02)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -15)
|
||||
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)
|
||||
|> fillet({ radius = 5, tags = [getOppositeEdge(seg02)] }, %)
|
||||
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 6, tags = [seg02] }, extrude001)
|
||||
chamfer001 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)] }, extrude001)`
|
||||
const edgeTreatmentSnippet = `${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)`
|
||||
const expectedCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, 10], %)
|
||||
|> line(end = [20, 0], tag = $seg01)
|
||||
|> line(end = [0, -20])
|
||||
|> line(end = [-20, 0], tag = $seg02)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -15)
|
||||
|> fillet({
|
||||
radius = 5,
|
||||
tags = [getOppositeEdge(seg02)]
|
||||
}, %)
|
||||
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 6, tags = [seg02] }, extrude001)
|
||||
chamfer001 = chamfer({
|
||||
length = 5,
|
||||
tags = [getOppositeEdge(seg01)]
|
||||
}, extrude001)`
|
||||
|
||||
await runDeleteEdgeTreatmentTest(
|
||||
code,
|
||||
edgeTreatmentSnippet,
|
||||
expectedCode
|
||||
)
|
||||
})
|
||||
it(`should delete a non-piped ${edgeTreatmentType} from a body with multiple treatments`, async () => {
|
||||
const code = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, 10], %)
|
||||
|> line(end = [20, 0], tag = $seg01)
|
||||
|> line(end = [0, -20])
|
||||
|> line(end = [-20, 0], tag = $seg02)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -15)
|
||||
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)
|
||||
|> fillet({ radius = 5, tags = [getOppositeEdge(seg02)] }, %)
|
||||
fillet001 = ${edgeTreatmentType}({ ${parameterName} = 6, tags = [seg02] }, extrude001)
|
||||
chamfer001 = chamfer({ length = 5, tags = [getOppositeEdge(seg01)] }, extrude001)`
|
||||
const edgeTreatmentSnippet = `fillet001 = ${edgeTreatmentType}({ ${parameterName} = 6, tags = [seg02] }, extrude001)`
|
||||
const expectedCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, 10], %)
|
||||
|> line(end = [20, 0], tag = $seg01)
|
||||
|> line(end = [0, -20])
|
||||
|> line(end = [-20, 0], tag = $seg02)
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
extrude001 = extrude(sketch001, length = -15)
|
||||
|> ${edgeTreatmentType}({ ${parameterName} = 3, tags = [seg01] }, %)
|
||||
|> fillet({
|
||||
radius = 5,
|
||||
tags = [getOppositeEdge(seg02)]
|
||||
}, %)
|
||||
chamfer001 = chamfer({
|
||||
length = 5,
|
||||
tags = [getOppositeEdge(seg01)]
|
||||
}, extrude001)`
|
||||
|
||||
await runDeleteEdgeTreatmentTest(
|
||||
code,
|
||||
edgeTreatmentSnippet,
|
||||
expectedCode
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
Identifier,
|
||||
ObjectExpression,
|
||||
PathToNode,
|
||||
PipeExpression,
|
||||
Program,
|
||||
VariableDeclaration,
|
||||
VariableDeclarator,
|
||||
@ -35,15 +36,14 @@ import {
|
||||
import { err, trap } from 'lib/trap'
|
||||
import { Selection, Selections } from 'lib/selections'
|
||||
import { KclCommandValue } from 'lib/commandTypes'
|
||||
import { isArray } from 'lib/utils'
|
||||
import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph'
|
||||
import {
|
||||
kclManager,
|
||||
engineCommandManager,
|
||||
editorManager,
|
||||
codeManager,
|
||||
} from 'lib/singletons'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { findKwArg } from 'lang/util'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import EditorManager from 'editor/manager'
|
||||
import CodeManager from 'lang/codeManager'
|
||||
|
||||
// Edge Treatment Types
|
||||
export enum EdgeTreatmentType {
|
||||
@ -65,21 +65,38 @@ export type EdgeTreatmentParameters = ChamferParameters | FilletParameters
|
||||
export async function applyEdgeTreatmentToSelection(
|
||||
ast: Node<Program>,
|
||||
selection: Selections,
|
||||
parameters: EdgeTreatmentParameters
|
||||
parameters: EdgeTreatmentParameters,
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
engineCommandManager: EngineCommandManager
|
||||
editorManager: EditorManager
|
||||
codeManager: CodeManager
|
||||
}
|
||||
): Promise<void | Error> {
|
||||
// 1. clone and modify with edge treatment and tag
|
||||
const result = modifyAstWithEdgeTreatmentAndTag(ast, selection, parameters)
|
||||
const result = modifyAstWithEdgeTreatmentAndTag(
|
||||
ast,
|
||||
selection,
|
||||
parameters,
|
||||
dependencies
|
||||
)
|
||||
if (err(result)) return result
|
||||
const { modifiedAst, pathToEdgeTreatmentNode } = result
|
||||
|
||||
// 2. update ast
|
||||
await updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode)
|
||||
await updateAstAndFocus(modifiedAst, pathToEdgeTreatmentNode, dependencies)
|
||||
}
|
||||
|
||||
export function modifyAstWithEdgeTreatmentAndTag(
|
||||
ast: Node<Program>,
|
||||
selections: Selections,
|
||||
parameters: EdgeTreatmentParameters
|
||||
parameters: EdgeTreatmentParameters,
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
engineCommandManager: EngineCommandManager
|
||||
editorManager: EditorManager
|
||||
codeManager: CodeManager
|
||||
}
|
||||
):
|
||||
| { modifiedAst: Node<Program>; pathToEdgeTreatmentNode: Array<PathToNode> }
|
||||
| Error {
|
||||
@ -89,7 +106,7 @@ export function modifyAstWithEdgeTreatmentAndTag(
|
||||
const astResult = insertParametersIntoAst(clonedAst, parameters)
|
||||
if (err(astResult)) return astResult
|
||||
|
||||
const artifactGraph = engineCommandManager.artifactGraph
|
||||
const artifactGraph = dependencies.engineCommandManager.artifactGraph
|
||||
|
||||
// Step 1: modify ast with tags and group them by extrude nodes (bodies)
|
||||
const extrudeToTagsMap: Map<
|
||||
@ -102,7 +119,8 @@ export function modifyAstWithEdgeTreatmentAndTag(
|
||||
const result = getPathToExtrudeForSegmentSelection(
|
||||
clonedAstForGetExtrude,
|
||||
selection,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
dependencies
|
||||
)
|
||||
if (err(result)) return result
|
||||
const { pathToSegmentNode, pathToExtrudeNode } = result
|
||||
@ -258,7 +276,13 @@ function insertParametersIntoAst(
|
||||
export function getPathToExtrudeForSegmentSelection(
|
||||
ast: Program,
|
||||
selection: Selection,
|
||||
artifactGraph: ArtifactGraph
|
||||
artifactGraph: ArtifactGraph,
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
engineCommandManager: EngineCommandManager
|
||||
editorManager: EditorManager
|
||||
codeManager: CodeManager
|
||||
}
|
||||
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
|
||||
const pathToSegmentNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
@ -274,7 +298,7 @@ export function getPathToExtrudeForSegmentSelection(
|
||||
const sketchVar = varDecNode.node.declaration.id.name
|
||||
|
||||
const sketch = sketchFromKclValue(
|
||||
kclManager.programMemory.get(sketchVar),
|
||||
dependencies.kclManager.programMemory.get(sketchVar),
|
||||
sketchVar
|
||||
)
|
||||
if (trap(sketch)) return sketch
|
||||
@ -293,16 +317,28 @@ export function getPathToExtrudeForSegmentSelection(
|
||||
|
||||
async function updateAstAndFocus(
|
||||
modifiedAst: Node<Program>,
|
||||
pathToEdgeTreatmentNode: Array<PathToNode>
|
||||
pathToEdgeTreatmentNode: Array<PathToNode>,
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
engineCommandManager: EngineCommandManager
|
||||
editorManager: EditorManager
|
||||
codeManager: CodeManager
|
||||
}
|
||||
): Promise<void> {
|
||||
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
||||
const updatedAst = await dependencies.kclManager.updateAst(
|
||||
modifiedAst,
|
||||
true,
|
||||
{
|
||||
focusPath: pathToEdgeTreatmentNode,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(updatedAst.newAst)
|
||||
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(
|
||||
updatedAst.newAst
|
||||
)
|
||||
|
||||
if (updatedAst?.selections) {
|
||||
editorManager.selectRange(updatedAst?.selections)
|
||||
dependencies.editorManager.selectRange(updatedAst?.selections)
|
||||
}
|
||||
}
|
||||
|
||||
@ -782,3 +818,142 @@ export const isTagUsedInEdgeTreatment = ({
|
||||
|
||||
return edges
|
||||
}
|
||||
|
||||
// Delete Edge Treatment
|
||||
export async function deleteEdgeTreatment(
|
||||
ast: Node<Program>,
|
||||
selection: Selection
|
||||
): Promise<Node<Program> | Error> {
|
||||
/**
|
||||
* Deletes an edge treatment (fillet or chamfer)
|
||||
* from the AST based on the selection.
|
||||
* Handles both standalone treatments
|
||||
* and those within a PipeExpression.
|
||||
*
|
||||
* Supported cases:
|
||||
* [+] fillet and chamfer
|
||||
* [+] piped and non-piped edge treatments
|
||||
* [-] delete single tag from array of tags (currently whole expression is deleted)
|
||||
* [-] multiple selections with different edge treatments (currently single selection is supported)
|
||||
*/
|
||||
|
||||
// 1. Validate Selection Type
|
||||
const { artifact } = selection
|
||||
if (!artifact || artifact.type !== 'edgeCut') {
|
||||
return new Error('Selection is not an edge cut')
|
||||
}
|
||||
|
||||
const { subType: edgeTreatmentType } = artifact
|
||||
if (
|
||||
!edgeTreatmentType ||
|
||||
!['fillet', 'chamfer'].includes(edgeTreatmentType)
|
||||
) {
|
||||
return new Error('Unsupported or missing edge treatment type')
|
||||
}
|
||||
|
||||
// 2. Clone ast and retrieve the VariableDeclarator
|
||||
const astClone = structuredClone(ast)
|
||||
const varDec = getNodeFromPath<VariableDeclarator>(
|
||||
ast,
|
||||
selection?.codeRef?.pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(varDec)) return varDec
|
||||
|
||||
// 3: Check if edge treatment is in a pipe
|
||||
const inPipe = varDec.node.init.type === 'PipeExpression'
|
||||
|
||||
// 4A. Handle standalone edge treatment
|
||||
if (!inPipe) {
|
||||
const varDecPathStep = varDec.shallowPath[1]
|
||||
|
||||
if (!isArray(varDecPathStep) || typeof varDecPathStep[0] !== 'number') {
|
||||
return new Error(
|
||||
'Invalid shallowPath structure: expected a number at shallowPath[1][0]'
|
||||
)
|
||||
}
|
||||
|
||||
const varDecIndex: number = varDecPathStep[0]
|
||||
|
||||
// Remove entire VariableDeclarator from the ast
|
||||
astClone.body.splice(varDecIndex, 1)
|
||||
return astClone
|
||||
}
|
||||
|
||||
// 4B. Handle edge treatment within pipe
|
||||
if (inPipe) {
|
||||
// Retrieve the CallExpression path
|
||||
const callExp =
|
||||
getNodeFromPath<CallExpression>(
|
||||
ast,
|
||||
selection?.codeRef?.pathToNode,
|
||||
'CallExpression'
|
||||
) ?? null
|
||||
if (err(callExp)) return callExp
|
||||
|
||||
const shallowPath = callExp.shallowPath
|
||||
|
||||
// Initialize variables to hold the PipeExpression path and callIndex
|
||||
let pipeExpressionPath: PathToNode | null = null
|
||||
let callIndex: number | null = null
|
||||
|
||||
// Iterate through the shallowPath to find the PipeExpression and callIndex
|
||||
for (let i = 0; i < shallowPath.length - 1; i++) {
|
||||
const [key, value] = shallowPath[i]
|
||||
|
||||
if (key === 'body' && value === 'PipeExpression') {
|
||||
pipeExpressionPath = shallowPath.slice(0, i + 1)
|
||||
|
||||
const nextStep = shallowPath[i + 1]
|
||||
if (
|
||||
nextStep &&
|
||||
nextStep[1] === 'index' &&
|
||||
typeof nextStep[0] === 'number'
|
||||
) {
|
||||
callIndex = nextStep[0]
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!pipeExpressionPath) {
|
||||
return new Error('PipeExpression not found in path')
|
||||
}
|
||||
|
||||
if (callIndex === null) {
|
||||
return new Error('Failed to extract CallExpression index')
|
||||
}
|
||||
// Retrieve the PipeExpression node
|
||||
const pipeExpressionNode = getNodeFromPath<PipeExpression>(
|
||||
astClone,
|
||||
pipeExpressionPath,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(pipeExpressionNode)) return pipeExpressionNode
|
||||
|
||||
// Ensure that the PipeExpression.body is an array
|
||||
if (!isArray(pipeExpressionNode.node.body)) {
|
||||
return new Error('PipeExpression body is not an array')
|
||||
}
|
||||
|
||||
// Remove the CallExpression at the specified index
|
||||
pipeExpressionNode.node.body.splice(callIndex, 1)
|
||||
|
||||
// Remove VariableDeclarator if PipeExpression.body is empty
|
||||
if (pipeExpressionNode.node.body.length === 0) {
|
||||
const varDecPathStep = varDec.shallowPath[1]
|
||||
if (!isArray(varDecPathStep) || typeof varDecPathStep[0] !== 'number') {
|
||||
return new Error(
|
||||
'Invalid shallowPath structure: expected a number at shallowPath[1][0]'
|
||||
)
|
||||
}
|
||||
const varDecIndex: number = varDecPathStep[0]
|
||||
astClone.body.splice(varDecIndex, 1)
|
||||
}
|
||||
|
||||
return astClone
|
||||
}
|
||||
|
||||
return Error('Delete fillets not implemented')
|
||||
}
|
||||
|
@ -19,17 +19,28 @@ import {
|
||||
createVariableDeclaration,
|
||||
} from 'lang/modifyAst'
|
||||
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
import { EngineCommandManager } from 'lang/std/engineConnection'
|
||||
import EditorManager from 'editor/manager'
|
||||
import CodeManager from 'lang/codeManager'
|
||||
|
||||
export function addShell({
|
||||
node,
|
||||
selection,
|
||||
artifactGraph,
|
||||
thickness,
|
||||
dependencies,
|
||||
}: {
|
||||
node: Node<Program>
|
||||
selection: Selections
|
||||
artifactGraph: ArtifactGraph
|
||||
thickness: Expr
|
||||
dependencies: {
|
||||
kclManager: KclManager
|
||||
engineCommandManager: EngineCommandManager
|
||||
editorManager: EditorManager
|
||||
codeManager: CodeManager
|
||||
}
|
||||
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
|
||||
const modifiedAst = structuredClone(node)
|
||||
|
||||
@ -42,7 +53,8 @@ export function addShell({
|
||||
const extrudeLookupResult = getPathToExtrudeForSegmentSelection(
|
||||
clonedAstForGetExtrude,
|
||||
graphSelection,
|
||||
artifactGraph
|
||||
artifactGraph,
|
||||
dependencies
|
||||
)
|
||||
if (err(extrudeLookupResult)) {
|
||||
return new Error("Couldn't find extrude")
|
||||
|
@ -5,7 +5,11 @@ import {
|
||||
PathToNode,
|
||||
Identifier,
|
||||
topLevelRange,
|
||||
PipeExpression,
|
||||
CallExpression,
|
||||
VariableDeclarator,
|
||||
} from './wasm'
|
||||
import { ProgramMemory } from 'lang/wasm'
|
||||
import {
|
||||
findAllPreviousVariables,
|
||||
isNodeSafeToReplace,
|
||||
@ -25,9 +29,11 @@ import {
|
||||
createCallExpression,
|
||||
createLiteral,
|
||||
createPipeSubstitution,
|
||||
createCallExpressionStdLib,
|
||||
} from './modifyAst'
|
||||
import { err } from 'lib/trap'
|
||||
import { codeRefFromRange } from './std/artifactGraph'
|
||||
import { addCallExpressionsToPipe, addCloseToPipe } from 'lang/std/sketch'
|
||||
|
||||
beforeAll(async () => {
|
||||
await initPromise
|
||||
@ -680,3 +686,115 @@ myNestedVar = [
|
||||
expect(pathToNode).toEqual(pathToNode2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Testing specific sketch getNodeFromPath workflow', () => {
|
||||
it('should parse the code', () => {
|
||||
const openSketch = `sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0.02, 0.22], %)
|
||||
|> xLine(0.39, %)
|
||||
|> line([0.02, -0.17], %)
|
||||
|> yLine(-0.15, %)
|
||||
|> line([-0.21, -0.02], %)
|
||||
|> xLine(-0.15, %)
|
||||
|> line([-0.02, 0.21], %)
|
||||
|> line([-0.08, 0.05], %)`
|
||||
const ast = assertParse(openSketch)
|
||||
expect(ast.start).toEqual(0)
|
||||
expect(ast.end).toEqual(227)
|
||||
})
|
||||
it('should find the location to add new lineTo', () => {
|
||||
const openSketch = `sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0.02, 0.22], %)
|
||||
|> xLine(0.39, %)
|
||||
|> line([0.02, -0.17], %)
|
||||
|> yLine(-0.15, %)
|
||||
|> line([-0.21, -0.02], %)
|
||||
|> xLine(-0.15, %)
|
||||
|> line([-0.02, 0.21], %)
|
||||
|> line([-0.08, 0.05], %)`
|
||||
const ast = assertParse(openSketch)
|
||||
|
||||
const sketchSnippet = `startProfileAt([0.02, 0.22], %)`
|
||||
const sketchRange = topLevelRange(
|
||||
openSketch.indexOf(sketchSnippet),
|
||||
openSketch.indexOf(sketchSnippet) + sketchSnippet.length
|
||||
)
|
||||
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
|
||||
const modifiedAst = addCallExpressionsToPipe({
|
||||
node: ast,
|
||||
programMemory: ProgramMemory.empty(),
|
||||
pathToNode: sketchPathToNode,
|
||||
expressions: [
|
||||
createCallExpressionStdLib(
|
||||
'lineTo', // We are forcing lineTo!
|
||||
[
|
||||
createArrayExpression([
|
||||
createCallExpressionStdLib('profileStartX', [
|
||||
createPipeSubstitution(),
|
||||
]),
|
||||
createCallExpressionStdLib('profileStartY', [
|
||||
createPipeSubstitution(),
|
||||
]),
|
||||
]),
|
||||
createPipeSubstitution(),
|
||||
]
|
||||
),
|
||||
],
|
||||
})
|
||||
if (err(modifiedAst)) throw modifiedAst
|
||||
const recasted = recast(modifiedAst)
|
||||
const expectedCode = `sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0.02, 0.22], %)
|
||||
|> xLine(0.39, %)
|
||||
|> line([0.02, -0.17], %)
|
||||
|> yLine(-0.15, %)
|
||||
|> line([-0.21, -0.02], %)
|
||||
|> xLine(-0.15, %)
|
||||
|> line([-0.02, 0.21], %)
|
||||
|> line([-0.08, 0.05], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
`
|
||||
expect(recasted).toEqual(expectedCode)
|
||||
})
|
||||
it('it should find the location to add close', () => {
|
||||
const openSketch = `sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0.02, 0.22], %)
|
||||
|> xLine(0.39, %)
|
||||
|> line([0.02, -0.17], %)
|
||||
|> yLine(-0.15, %)
|
||||
|> line([-0.21, -0.02], %)
|
||||
|> xLine(-0.15, %)
|
||||
|> line([-0.02, 0.21], %)
|
||||
|> line([-0.08, 0.05], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
`
|
||||
const ast = assertParse(openSketch)
|
||||
const sketchSnippet = `startProfileAt([0.02, 0.22], %)`
|
||||
const sketchRange = topLevelRange(
|
||||
openSketch.indexOf(sketchSnippet),
|
||||
openSketch.indexOf(sketchSnippet) + sketchSnippet.length
|
||||
)
|
||||
const sketchPathToNode = getNodePathFromSourceRange(ast, sketchRange)
|
||||
const modifiedAst = addCloseToPipe({
|
||||
node: ast,
|
||||
programMemory: ProgramMemory.empty(),
|
||||
pathToNode: sketchPathToNode,
|
||||
})
|
||||
|
||||
if (err(modifiedAst)) throw modifiedAst
|
||||
const recasted = recast(modifiedAst)
|
||||
const expectedCode = `sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([0.02, 0.22], %)
|
||||
|> xLine(0.39, %)
|
||||
|> line([0.02, -0.17], %)
|
||||
|> yLine(-0.15, %)
|
||||
|> line([-0.21, -0.02], %)
|
||||
|> xLine(-0.15, %)
|
||||
|> line([-0.02, 0.21], %)
|
||||
|> line([-0.08, 0.05], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close()
|
||||
`
|
||||
expect(recasted).toEqual(expectedCode)
|
||||
})
|
||||
})
|
||||
|
@ -22,11 +22,12 @@ import {
|
||||
topLevelRange,
|
||||
VariableDeclaration,
|
||||
VariableDeclarator,
|
||||
recast,
|
||||
} from './wasm'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
|
||||
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
|
||||
import { getAngle } from '../lib/utils'
|
||||
import { getAngle, isArray } from '../lib/utils'
|
||||
import { ARG_TAG, getArgForEnd, getFirstArg } from './std/sketch'
|
||||
import {
|
||||
getConstraintLevelFromSourceRange,
|
||||
@ -79,7 +80,28 @@ export function getNodeFromPath<T>(
|
||||
deepPath: successfulPaths,
|
||||
}
|
||||
}
|
||||
return new Error('not an object')
|
||||
const stackTraceError = new Error()
|
||||
const sourceCode = recast(node)
|
||||
const levels = stackTraceError.stack?.split('\n')
|
||||
const aFewFunctionNames: string[] = []
|
||||
let tree = ''
|
||||
levels?.forEach((val, index) => {
|
||||
const fnName = val.trim().split(' ')[1]
|
||||
const ending = index === levels.length - 1 ? ' ' : ' > '
|
||||
tree += fnName + ending
|
||||
if (index < 3) {
|
||||
aFewFunctionNames.push(fnName)
|
||||
}
|
||||
})
|
||||
const error = new Error(
|
||||
`Failed to stopAt ${stopAt}, ${aFewFunctionNames
|
||||
.filter((a) => a)
|
||||
.join(' > ')}`
|
||||
)
|
||||
console.error(tree)
|
||||
console.error(sourceCode)
|
||||
console.error(error.stack)
|
||||
return error
|
||||
}
|
||||
parent = currentNode
|
||||
parentEdge = pathItem[0]
|
||||
@ -90,7 +112,7 @@ export function getNodeFromPath<T>(
|
||||
}
|
||||
if (
|
||||
typeof stopAt !== 'undefined' &&
|
||||
(Array.isArray(stopAt)
|
||||
(isArray(stopAt)
|
||||
? stopAt.includes(currentNode.type)
|
||||
: currentNode.type === stopAt)
|
||||
) {
|
||||
@ -145,6 +167,7 @@ export function getNodeFromPathCurry(
|
||||
type KCLNode = Node<
|
||||
| Expr
|
||||
| ExpressionStatement
|
||||
| ImportStatement
|
||||
| VariableDeclaration
|
||||
| VariableDeclarator
|
||||
| ReturnStatement
|
||||
@ -241,10 +264,14 @@ export function traverse(
|
||||
// hmm this smell
|
||||
_traverse(_node.object, [...pathToNode, ['object', 'MemberExpression']])
|
||||
_traverse(_node.property, [...pathToNode, ['property', 'MemberExpression']])
|
||||
} else if ('body' in _node && Array.isArray(_node.body)) {
|
||||
_node.body.forEach((expression, index) =>
|
||||
} else if (_node.type === 'ImportStatement') {
|
||||
// Do nothing.
|
||||
} else if ('body' in _node && isArray(_node.body)) {
|
||||
// TODO: Program should have a type field, but it currently doesn't.
|
||||
const program = node as Node<Program>
|
||||
program.body.forEach((expression, index) => {
|
||||
_traverse(expression, [...pathToNode, ['body', ''], [index, 'index']])
|
||||
)
|
||||
})
|
||||
}
|
||||
option?.leave?.(_node)
|
||||
}
|
||||
|
@ -248,6 +248,8 @@ class EngineConnection extends EventTarget {
|
||||
mediaStream?: MediaStream
|
||||
idleMode: boolean = false
|
||||
promise?: Promise<void>
|
||||
sdpAnswer?: Models['RtcSessionDescription_type']
|
||||
triggeredStart = false
|
||||
|
||||
onIceCandidate = function (
|
||||
this: RTCPeerConnection,
|
||||
@ -553,6 +555,7 @@ class EngineConnection extends EventTarget {
|
||||
* did not establish.
|
||||
*/
|
||||
connect(reconnecting?: boolean): Promise<void> {
|
||||
const that = this
|
||||
return new Promise((resolve) => {
|
||||
if (this.isConnecting() || this.isReady()) {
|
||||
return
|
||||
@ -583,8 +586,38 @@ class EngineConnection extends EventTarget {
|
||||
},
|
||||
}
|
||||
|
||||
const initiateConnectingExclusive = () => {
|
||||
if (that.triggeredStart) return
|
||||
that.triggeredStart = true
|
||||
|
||||
// Start connecting.
|
||||
that.state = {
|
||||
type: EngineConnectionStateType.Connecting,
|
||||
value: {
|
||||
type: ConnectingType.WebRTCConnecting,
|
||||
},
|
||||
}
|
||||
|
||||
// As soon as this is set, RTCPeerConnection tries to
|
||||
// establish a connection.
|
||||
// @ts-expect-error: Have to ignore because dom.ts doesn't have the right type
|
||||
void that.pc?.setRemoteDescription(that.sdpAnswer)
|
||||
|
||||
that.state = {
|
||||
type: EngineConnectionStateType.Connecting,
|
||||
value: {
|
||||
type: ConnectingType.SetRemoteDescription,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
this.onIceCandidate = (event: RTCPeerConnectionIceEvent) => {
|
||||
console.log('icecandidate', event.candidate)
|
||||
|
||||
// This is null when the ICE gathering state is done.
|
||||
// Windows ONLY uses this to signal it's done!
|
||||
if (event.candidate === null) {
|
||||
initiateConnectingExclusive()
|
||||
return
|
||||
}
|
||||
|
||||
@ -595,7 +628,6 @@ class EngineConnection extends EventTarget {
|
||||
},
|
||||
}
|
||||
|
||||
// Request a candidate to use
|
||||
this.send({
|
||||
type: 'trickle_ice',
|
||||
candidate: {
|
||||
@ -605,8 +637,38 @@ class EngineConnection extends EventTarget {
|
||||
usernameFragment: event.candidate.usernameFragment || undefined,
|
||||
},
|
||||
})
|
||||
|
||||
// Sometimes the remote end doesn't report the end of candidates.
|
||||
// They have 3 seconds to.
|
||||
setTimeout(() => {
|
||||
initiateConnectingExclusive()
|
||||
}, 3000)
|
||||
}
|
||||
this.pc?.addEventListener?.('icecandidate', this.onIceCandidate)
|
||||
this.pc?.addEventListener?.(
|
||||
'icegatheringstatechange',
|
||||
function (_event) {
|
||||
console.log('icegatheringstatechange', this.iceGatheringState)
|
||||
|
||||
if (this.iceGatheringState !== 'complete') return
|
||||
initiateConnectingExclusive()
|
||||
}
|
||||
)
|
||||
|
||||
this.pc?.addEventListener?.(
|
||||
'iceconnectionstatechange',
|
||||
function (_event) {
|
||||
console.log('iceconnectionstatechange', this.iceConnectionState)
|
||||
console.log('iceconnectionstatechange', this.iceGatheringState)
|
||||
}
|
||||
)
|
||||
this.pc?.addEventListener?.('negotiationneeded', function (_event) {
|
||||
console.log('negotiationneeded', this.iceConnectionState)
|
||||
console.log('negotiationneeded', this.iceGatheringState)
|
||||
})
|
||||
this.pc?.addEventListener?.('signalingstatechange', function (event) {
|
||||
console.log('signalingstatechange', this.signalingState)
|
||||
})
|
||||
|
||||
this.onIceCandidateError = (_event: Event) => {
|
||||
const event = _event as RTCPeerConnectionIceErrorEvent
|
||||
@ -634,6 +696,8 @@ class EngineConnection extends EventTarget {
|
||||
})
|
||||
)
|
||||
break
|
||||
case 'connecting':
|
||||
break
|
||||
case 'disconnected':
|
||||
case 'failed':
|
||||
this.pc?.removeEventListener('icecandidate', this.onIceCandidate)
|
||||
@ -1126,25 +1190,8 @@ class EngineConnection extends EventTarget {
|
||||
},
|
||||
}
|
||||
|
||||
// As soon as this is set, RTCPeerConnection tries to
|
||||
// establish a connection.
|
||||
// @ts-ignore
|
||||
// Have to ignore because dom.ts doesn't have the right type
|
||||
void this.pc?.setRemoteDescription(answer)
|
||||
this.sdpAnswer = answer
|
||||
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Connecting,
|
||||
value: {
|
||||
type: ConnectingType.SetRemoteDescription,
|
||||
},
|
||||
}
|
||||
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Connecting,
|
||||
value: {
|
||||
type: ConnectingType.WebRTCConnecting,
|
||||
},
|
||||
}
|
||||
break
|
||||
|
||||
case 'trickle_ice':
|
||||
@ -1235,6 +1282,7 @@ class EngineConnection extends EventTarget {
|
||||
if (closedPc && closedUDC && closedWS) {
|
||||
// Do not notify the rest of the program that we have cut off anything.
|
||||
this.state = { type: EngineConnectionStateType.Disconnected }
|
||||
this.triggeredStart = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1389,6 +1437,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
enableSSAO: true,
|
||||
showScaleGrid: false,
|
||||
cameraProjection: 'perspective',
|
||||
cameraOrbit: 'spherical',
|
||||
}
|
||||
}
|
||||
|
||||
@ -1437,6 +1486,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
enableSSAO: true,
|
||||
showScaleGrid: false,
|
||||
cameraProjection: 'orthographic',
|
||||
cameraOrbit: 'spherical',
|
||||
},
|
||||
// When passed, use a completely separate connecting code path that simply
|
||||
// opens a websocket and this is a function that is called when connected.
|
||||
@ -1999,7 +2049,7 @@ export class EngineCommandManager extends EventTarget {
|
||||
.catch((e) => {
|
||||
// TODO: Previously was never caught, we are not rejecting these pendingCommands but this needs to be handled at some point.
|
||||
/*noop*/
|
||||
return null
|
||||
return e
|
||||
})
|
||||
}
|
||||
/**
|
||||
|
@ -60,7 +60,7 @@ import {
|
||||
mutateObjExpProp,
|
||||
findUniqueName,
|
||||
} from 'lang/modifyAst'
|
||||
import { roundOff, getLength, getAngle } from 'lib/utils'
|
||||
import { roundOff, getLength, getAngle, isArray } from 'lib/utils'
|
||||
import { err } from 'lib/trap'
|
||||
import { perpendicularDistance } from 'sketch-helpers'
|
||||
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
|
||||
@ -96,7 +96,7 @@ export function createFirstArg(
|
||||
sketchFn: ToolTip,
|
||||
val: Expr | [Expr, Expr] | [Expr, Expr, Expr]
|
||||
): Expr | Error {
|
||||
if (Array.isArray(val)) {
|
||||
if (isArray(val)) {
|
||||
if (
|
||||
[
|
||||
'angledLine',
|
||||
|
@ -57,7 +57,7 @@ import {
|
||||
getSketchSegmentFromPathToNode,
|
||||
getSketchSegmentFromSourceRange,
|
||||
} from './sketchConstraints'
|
||||
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
|
||||
import { getAngle, roundOff, normaliseAngle, isArray } from '../../lib/utils'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { findKwArg, findKwArgAny } from 'lang/util'
|
||||
|
||||
@ -122,7 +122,7 @@ function createCallWrapper(
|
||||
tag?: Expr,
|
||||
valueUsedInTransform?: number
|
||||
): CreatedSketchExprResult {
|
||||
if (Array.isArray(val)) {
|
||||
if (isArray(val)) {
|
||||
if (tooltip === 'line') {
|
||||
const labeledArgs = [createLabeledArg('end', createArrayExpression(val))]
|
||||
if (tag) {
|
||||
@ -1330,12 +1330,12 @@ export function getRemoveConstraintsTransform(
|
||||
|
||||
// check if the function has no constraints
|
||||
const isTwoValFree =
|
||||
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
if (isTwoValFree) {
|
||||
return false
|
||||
}
|
||||
const isOneValFree =
|
||||
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
!isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
if (isOneValFree) {
|
||||
return transformInfo
|
||||
}
|
||||
@ -1649,7 +1649,7 @@ export function getConstraintType(
|
||||
// and for one val sketch functions that the arg is NOT locked down
|
||||
// these conditions should have been checked previously.
|
||||
// completely locked down or not locked down at all does not depend on the fnName so we can check that first
|
||||
const isArr = Array.isArray(val)
|
||||
const isArr = isArray(val)
|
||||
if (!isArr) {
|
||||
if (fnName === 'xLine') return 'yRelative'
|
||||
if (fnName === 'yLine') return 'xRelative'
|
||||
@ -2113,9 +2113,9 @@ export function getConstraintLevelFromSourceRange(
|
||||
|
||||
// check if the function has no constraints
|
||||
const isTwoValFree =
|
||||
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
const isOneValFree =
|
||||
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
!isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
|
||||
|
||||
if (isTwoValFree) return { level: 'free', range: range }
|
||||
if (isOneValFree) return { level: 'partial', range: range }
|
||||
@ -2128,7 +2128,7 @@ export function isLiteralArrayOrStatic(
|
||||
): boolean {
|
||||
if (!val) return false
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
if (isArray(val)) {
|
||||
const a = val[0]
|
||||
const b = val[1]
|
||||
return isLiteralArrayOrStatic(a) && isLiteralArrayOrStatic(b)
|
||||
@ -2142,7 +2142,7 @@ export function isLiteralArrayOrStatic(
|
||||
export function isNotLiteralArrayOrStatic(
|
||||
val: Expr | [Expr, Expr] | [Expr, Expr, Expr]
|
||||
): boolean {
|
||||
if (Array.isArray(val)) {
|
||||
if (isArray(val)) {
|
||||
const a = val[0]
|
||||
const b = val[1]
|
||||
return isNotLiteralArrayOrStatic(a) && isNotLiteralArrayOrStatic(b)
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
NumericSuffix,
|
||||
} from './wasm'
|
||||
import { filterArtifacts } from 'lang/std/artifactGraph'
|
||||
import { isOverlap } from 'lib/utils'
|
||||
import { isArray, isOverlap } from 'lib/utils'
|
||||
|
||||
export function updatePathToNodeFromMap(
|
||||
oldPath: PathToNode,
|
||||
@ -40,8 +40,8 @@ export function isCursorInSketchCommandRange(
|
||||
predicate: (artifact) => {
|
||||
return selectionRanges.graphSelections.some(
|
||||
(selection) =>
|
||||
Array.isArray(selection?.codeRef?.range) &&
|
||||
Array.isArray(artifact?.codeRef?.range) &&
|
||||
isArray(selection?.codeRef?.range) &&
|
||||
isArray(artifact?.codeRef?.range) &&
|
||||
isOverlap(selection?.codeRef?.range, artifact.codeRef.range)
|
||||
)
|
||||
},
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
default_project_settings,
|
||||
base64_decode,
|
||||
clear_scene_and_bust_cache,
|
||||
change_kcl_settings,
|
||||
reloadModule,
|
||||
} from 'lib/wasm_lib_wrapper'
|
||||
|
||||
@ -56,6 +57,7 @@ import { ArtifactGraph as RustArtifactGraph } from 'wasm-lib/kcl/bindings/Artifa
|
||||
import { Artifact } from './std/artifactGraph'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { NumericSuffix } from 'wasm-lib/kcl/bindings/NumericSuffix'
|
||||
import { MetaSettings } from 'wasm-lib/kcl/bindings/MetaSettings'
|
||||
|
||||
export type { Artifact } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
export type { ArtifactCommand } from 'wasm-lib/kcl/bindings/Artifact'
|
||||
@ -156,6 +158,12 @@ export function isTopLevelModule(range: SourceRange): boolean {
|
||||
return range[2] === 0
|
||||
}
|
||||
|
||||
function firstSourceRange(error: RustKclError): SourceRange {
|
||||
return error.sourceRanges.length > 0
|
||||
? sourceRangeFromRust(error.sourceRanges[0])
|
||||
: defaultSourceRange()
|
||||
}
|
||||
|
||||
export const wasmUrl = () => {
|
||||
// For when we're in electron (file based) or web server (network based)
|
||||
// For some reason relative paths don't work as expected. Otherwise we would
|
||||
@ -253,7 +261,7 @@ export const parse = (code: string | Error): ParseResult | Error => {
|
||||
return new KCLError(
|
||||
parsed.kind,
|
||||
parsed.msg,
|
||||
sourceRangeFromRust(parsed.sourceRanges[0]),
|
||||
firstSourceRange(parsed),
|
||||
[],
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
@ -620,7 +628,7 @@ export const executor = async (
|
||||
const kclError = new KCLError(
|
||||
parsed.error.kind,
|
||||
parsed.error.msg,
|
||||
sourceRangeFromRust(parsed.error.sourceRanges[0]),
|
||||
firstSourceRange(parsed.error),
|
||||
parsed.operations,
|
||||
parsed.artifactCommands,
|
||||
rustArtifactGraphToMap(parsed.artifactGraph)
|
||||
@ -689,7 +697,7 @@ export const modifyAstForSketch = async (
|
||||
const kclError = new KCLError(
|
||||
parsed.kind,
|
||||
parsed.msg,
|
||||
sourceRangeFromRust(parsed.sourceRanges[0]),
|
||||
firstSourceRange(parsed),
|
||||
[],
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
@ -760,7 +768,7 @@ export function programMemoryInit(): ProgramMemory | Error {
|
||||
return new KCLError(
|
||||
parsed.kind,
|
||||
parsed.msg,
|
||||
sourceRangeFromRust(parsed.sourceRanges[0]),
|
||||
firstSourceRange(parsed),
|
||||
[],
|
||||
[],
|
||||
defaultArtifactGraph()
|
||||
@ -848,3 +856,17 @@ export function base64Decode(base64: string): ArrayBuffer | Error {
|
||||
return new Error('Caught error decoding base64 string: ' + e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the meta settings for the kcl file.
|
||||
/// Returns the new kcl string with the updated settings.
|
||||
export function changeKclSettings(
|
||||
kcl: string,
|
||||
settings: MetaSettings
|
||||
): string | Error {
|
||||
try {
|
||||
return change_kcl_settings(kcl, JSON.stringify(settings))
|
||||
} catch (e) {
|
||||
console.error('Caught error changing kcl settings: ' + e)
|
||||
return new Error('Caught error changing kcl settings: ' + e)
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,14 @@
|
||||
import { StateMachineCommandSetConfig } from 'lib/commandTypes'
|
||||
import { authMachine } from 'machines/authMachine'
|
||||
import { Command } from 'lib/commandTypes'
|
||||
import { authActor } from 'machines/appMachine'
|
||||
import { ACTOR_IDS } from 'machines/machineConstants'
|
||||
|
||||
type AuthCommandSchema = {}
|
||||
|
||||
export const authCommandBarConfig: StateMachineCommandSetConfig<
|
||||
typeof authMachine,
|
||||
AuthCommandSchema
|
||||
> = {
|
||||
'Log in': {
|
||||
hide: 'both',
|
||||
},
|
||||
'Log out': {
|
||||
args: [],
|
||||
export const authCommands: Command[] = [
|
||||
{
|
||||
groupId: ACTOR_IDS.AUTH,
|
||||
name: 'log-out',
|
||||
displayName: 'Log out',
|
||||
icon: 'arrowLeft',
|
||||
needsReview: false,
|
||||
onSubmit: () => authActor.send({ type: 'Log out' }),
|
||||
},
|
||||
}
|
||||
]
|
||||
|
@ -308,7 +308,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
description:
|
||||
'Create a 3D body by moving a sketch region along an arbitrary path.',
|
||||
icon: 'sweep',
|
||||
status: 'development',
|
||||
needsReview: false,
|
||||
args: {
|
||||
target: {
|
||||
@ -317,8 +316,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
required: true,
|
||||
skip: true,
|
||||
multiple: false,
|
||||
warningMessage:
|
||||
'The sweep workflow is new and under tested. Please break it and report issues.',
|
||||
},
|
||||
trajectory: {
|
||||
inputType: 'selection',
|
||||
@ -368,7 +365,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
Revolve: {
|
||||
description: 'Create a 3D body by rotating a sketch region about an axis.',
|
||||
icon: 'revolve',
|
||||
status: 'development',
|
||||
needsReview: true,
|
||||
args: {
|
||||
selection: {
|
||||
@ -377,8 +373,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
multiple: false, // TODO: multiple selection
|
||||
required: true,
|
||||
skip: true,
|
||||
warningMessage:
|
||||
'The revolve workflow is new and under tested. Please break it and report issues.',
|
||||
},
|
||||
axisOrEdge: {
|
||||
inputType: 'options',
|
||||
|
19
src/lib/commandBarConfigs/validators.test.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { parseEngineErrorMessage } from './validators'
|
||||
|
||||
describe('parseEngineErrorMessage', () => {
|
||||
it('takes an engine error string and parses its json message', () => {
|
||||
const engineError =
|
||||
'engine error: [{"error_code":"internal_engine","message":"Trajectory curve must be G1 continuous (with continuous tangents)"}]'
|
||||
const message = parseEngineErrorMessage(engineError)
|
||||
expect(message).toEqual(
|
||||
'Trajectory curve must be G1 continuous (with continuous tangents)'
|
||||
)
|
||||
})
|
||||
|
||||
it('retuns undefined on strings with different formats', () => {
|
||||
const s1 = 'engine error: []'
|
||||
const s2 = 'blabla'
|
||||
expect(parseEngineErrorMessage(s1)).toBeUndefined()
|
||||
expect(parseEngineErrorMessage(s2)).toBeUndefined()
|
||||
})
|
||||
})
|
@ -3,6 +3,7 @@ import { engineCommandManager } from 'lib/singletons'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { CommandBarContext } from 'machines/commandBarMachine'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { ApiError_type } from '@kittycad/lib/dist/types/src/models'
|
||||
|
||||
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
|
||||
for (let tries = 0; tries < numberOfRetries; tries++) {
|
||||
@ -46,6 +47,20 @@ function isSelections(selections: unknown): selections is Selections {
|
||||
)
|
||||
}
|
||||
|
||||
export function parseEngineErrorMessage(engineError: string) {
|
||||
const parts = engineError.split('engine error: ')
|
||||
if (parts.length < 2) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const errors = JSON.parse(parts[1]) as ApiError_type[]
|
||||
if (!errors[0]) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return errors[0].message
|
||||
}
|
||||
|
||||
export const revolveAxisValidator = async ({
|
||||
data,
|
||||
context,
|
||||
@ -83,7 +98,7 @@ export const revolveAxisValidator = async ({
|
||||
value: 360,
|
||||
}
|
||||
|
||||
const revolveAboutEdgeCommand = async () => {
|
||||
const command = async () => {
|
||||
return await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
@ -92,17 +107,18 @@ export const revolveAxisValidator = async ({
|
||||
angle: angleInDegrees,
|
||||
edge_id: edgeSelection,
|
||||
target: sketchSelection,
|
||||
tolerance: 0.0001,
|
||||
// Gotcha: Playwright will fail with larger tolerances, need to use a smaller one.
|
||||
tolerance: 1e-7,
|
||||
},
|
||||
})
|
||||
}
|
||||
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
|
||||
if (attemptRevolve?.success) {
|
||||
const result = await dryRunWrapper(command)
|
||||
if (result?.success) {
|
||||
return true
|
||||
} else {
|
||||
// return error message for the toast
|
||||
return 'Unable to revolve with selected edge'
|
||||
}
|
||||
|
||||
const reason = parseEngineErrorMessage(result) || 'unknown'
|
||||
return `Unable to revolve with the current selection. Reason: ${reason}`
|
||||
}
|
||||
|
||||
export const loftValidator = async ({
|
||||
@ -128,7 +144,7 @@ export const loftValidator = async ({
|
||||
return 'Unable to loft, selection contains less than two solid2ds'
|
||||
}
|
||||
|
||||
const loftCommand = async () => {
|
||||
const command = async () => {
|
||||
// TODO: check what to do with these
|
||||
const DEFAULT_V_DEGREE = 2
|
||||
const DEFAULT_TOLERANCE = 2
|
||||
@ -145,13 +161,13 @@ export const loftValidator = async ({
|
||||
},
|
||||
})
|
||||
}
|
||||
const attempt = await dryRunWrapper(loftCommand)
|
||||
if (attempt?.success) {
|
||||
const result = await dryRunWrapper(command)
|
||||
if (result?.success) {
|
||||
return true
|
||||
} else {
|
||||
// return error message for the toast
|
||||
return 'Unable to loft with selected sketches'
|
||||
}
|
||||
|
||||
const reason = parseEngineErrorMessage(result) || 'unknown'
|
||||
return `Unable to loft with the current selection. Reason: ${reason}`
|
||||
}
|
||||
|
||||
export const shellValidator = async ({
|
||||
@ -180,7 +196,7 @@ export const shellValidator = async ({
|
||||
return "Unable to shell, couldn't find the solid"
|
||||
}
|
||||
|
||||
const shellCommand = async () => {
|
||||
const command = async () => {
|
||||
// TODO: figure out something better than an arbitrarily small value
|
||||
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
|
||||
const DEFAULT_HOLLOW = false
|
||||
@ -200,12 +216,13 @@ export const shellValidator = async ({
|
||||
})
|
||||
}
|
||||
|
||||
const attemptShell = await dryRunWrapper(shellCommand)
|
||||
if (attemptShell?.success) {
|
||||
const result = await dryRunWrapper(command)
|
||||
if (result?.success) {
|
||||
return true
|
||||
}
|
||||
|
||||
return 'Unable to shell with the provided selection'
|
||||
const reason = parseEngineErrorMessage(result) || 'unknown'
|
||||
return `Unable to shell with the current selection. Reason: ${reason}`
|
||||
}
|
||||
|
||||
export const sweepValidator = async ({
|
||||
@ -241,7 +258,7 @@ export const sweepValidator = async ({
|
||||
}
|
||||
const target = targetArtifact.pathId
|
||||
|
||||
const sweepCommand = async () => {
|
||||
const command = async () => {
|
||||
// TODO: second look on defaults here
|
||||
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
|
||||
const DEFAULT_SECTIONAL = false
|
||||
@ -261,10 +278,11 @@ export const sweepValidator = async ({
|
||||
})
|
||||
}
|
||||
|
||||
const attemptSweep = await dryRunWrapper(sweepCommand)
|
||||
if (attemptSweep?.success) {
|
||||
const result = await dryRunWrapper(command)
|
||||
if (result?.success) {
|
||||
return true
|
||||
}
|
||||
|
||||
return 'Unable to sweep with the provided selection'
|
||||
const reason = parseEngineErrorMessage(result) || 'unknown'
|
||||
return `Unable to sweep with the current selection. Reason: ${reason}`
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export const FILE_EXT = '.kcl'
|
||||
/** Default file to open when a project is opened */
|
||||
export const PROJECT_ENTRYPOINT = `main${FILE_EXT}` as const
|
||||
/** Thumbnail file name */
|
||||
export const PROJECT_IMAGE_NAME = `main.jpg` as const
|
||||
export const PROJECT_IMAGE_NAME = `thumbnail.png` as const
|
||||
/** The localStorage key for last-opened projects */
|
||||
export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const
|
||||
/** The default name given to new kcl files in a project */
|
||||
@ -68,8 +68,6 @@ export const KCL_DEFAULT_DEGREE = `360`
|
||||
/** localStorage key for the playwright test-specific app settings file */
|
||||
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
|
||||
|
||||
export const DEFAULT_HOST = 'https://api.zoo.dev'
|
||||
export const PROD_APP_URL = 'https://app.zoo.dev'
|
||||
export const SETTINGS_FILE_NAME = 'settings.toml'
|
||||
export const TOKEN_FILE_NAME = 'token.txt'
|
||||
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
|
||||
@ -145,7 +143,7 @@ export const VIEW_NAMES_SEMANTIC = {
|
||||
export const SIDEBAR_BUTTON_SUFFIX = '-pane-button'
|
||||
|
||||
/** Custom URL protocol our desktop registers */
|
||||
export const ZOO_STUDIO_PROTOCOL = 'zoo-studio:'
|
||||
export const ZOO_STUDIO_PROTOCOL = 'zoo-studio'
|
||||
|
||||
/**
|
||||
* A query parameter that triggers a modal
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
import {
|
||||
PROJECT_ENTRYPOINT,
|
||||
PROJECT_FOLDER,
|
||||
PROJECT_IMAGE_NAME,
|
||||
PROJECT_SETTINGS_FILE_NAME,
|
||||
SETTINGS_FILE_NAME,
|
||||
TELEMETRY_FILE_NAME,
|
||||
@ -625,3 +626,19 @@ export const getUser = async (
|
||||
}
|
||||
return Promise.reject(new Error('unreachable'))
|
||||
}
|
||||
|
||||
export const writeProjectThumbnailFile = async (
|
||||
dataUrl: string,
|
||||
projectDirectoryPath: string
|
||||
) => {
|
||||
const filePath = window.electron.path.join(
|
||||
projectDirectoryPath,
|
||||
PROJECT_IMAGE_NAME
|
||||
)
|
||||
const data = atob(dataUrl.substring('data:image/png;base64,'.length))
|
||||
const asArray = new Uint8Array(data.length)
|
||||
for (let i = 0, len = data.length; i < len; ++i) {
|
||||
asArray[i] = data.charCodeAt(i)
|
||||
}
|
||||
return window.electron.writeFile(filePath, asArray)
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ filletSketch = startSketchOn('XZ')
|
||||
}, %)
|
||||
|
||||
// Sketch the bend
|
||||
filletExtrude = extrude(-width, filletSketch)
|
||||
filletExtrude = extrude(filletSketch, length = -width)
|
||||
|
||||
// Create a custom plane for the leg that sits on the wall
|
||||
customPlane = {
|
||||
@ -102,7 +102,7 @@ bracketLeg2Sketch = startSketchOn(customPlane)
|
||||
}, %), %)
|
||||
|
||||
// Extrude the second leg
|
||||
bracketLeg2Extrude = extrude(-thickness, bracketLeg2Sketch)
|
||||
bracketLeg2Extrude = extrude(bracketLeg2Sketch, length = -thickness)
|
||||
|> fillet({
|
||||
radius = extFilletRadius,
|
||||
tags = [
|
||||
|
@ -2,13 +2,12 @@ import { CommandBarOverwriteWarning } from 'components/CommandBarOverwriteWarnin
|
||||
import { Command, CommandArgumentOption } from './commandTypes'
|
||||
import { codeManager, kclManager } from './singletons'
|
||||
import { isDesktop } from './isDesktop'
|
||||
import { FILE_EXT, PROJECT_SETTINGS_FILE_NAME } from './constants'
|
||||
import { FILE_EXT } from './constants'
|
||||
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
||||
import { parseProjectSettings } from 'lang/wasm'
|
||||
import { err, reportRejection } from './trap'
|
||||
import { projectConfigurationToSettingsPayload } from './settings/settingsUtils'
|
||||
import { copyFileShareLink } from './links'
|
||||
import { reportRejection } from './trap'
|
||||
import { IndexLoaderData } from './types'
|
||||
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
|
||||
import { copyFileShareLink } from './links'
|
||||
|
||||
interface OnSubmitProps {
|
||||
sampleName: string
|
||||
@ -68,23 +67,9 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
||||
const sampleCodeUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
|
||||
projectPathPart
|
||||
)}/${encodeURIComponent(primaryKclFile)}`
|
||||
const sampleSettingsFileUrl = `https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/${encodeURIComponent(
|
||||
projectPathPart
|
||||
)}/${PROJECT_SETTINGS_FILE_NAME}`
|
||||
|
||||
Promise.allSettled([fetch(sampleCodeUrl), fetch(sampleSettingsFileUrl)])
|
||||
.then((results) => {
|
||||
const a =
|
||||
'value' in results[0] ? results[0].value : results[0].reason
|
||||
const b =
|
||||
'value' in results[1] ? results[1].value : results[1].reason
|
||||
return [a, b]
|
||||
})
|
||||
.then(
|
||||
async ([
|
||||
codeResponse,
|
||||
settingsResponse,
|
||||
]): Promise<OnSubmitProps> => {
|
||||
fetch(sampleCodeUrl)
|
||||
.then(async (codeResponse): Promise<OnSubmitProps> => {
|
||||
if (!codeResponse.ok) {
|
||||
console.error(
|
||||
'Failed to fetch sample code:',
|
||||
@ -93,31 +78,12 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
||||
return Promise.reject(new Error('Failed to fetch sample code'))
|
||||
}
|
||||
const code = await codeResponse.text()
|
||||
|
||||
// It's possible that a sample doesn't have a project.toml
|
||||
// associated with it.
|
||||
let projectSettingsPayload: ReturnType<
|
||||
typeof projectConfigurationToSettingsPayload
|
||||
> = {}
|
||||
if (settingsResponse.ok) {
|
||||
const parsedProjectSettings = parseProjectSettings(
|
||||
await settingsResponse.text()
|
||||
)
|
||||
if (!err(parsedProjectSettings)) {
|
||||
projectSettingsPayload =
|
||||
projectConfigurationToSettingsPayload(parsedProjectSettings)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sampleName: data.sample.split('/')[0] + FILE_EXT,
|
||||
code,
|
||||
method: data.method,
|
||||
sampleUnits:
|
||||
projectSettingsPayload.modeling?.defaultUnit || 'mm',
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
.then((props) => {
|
||||
if (props?.code) {
|
||||
commandProps.specialPropsForSampleCommand
|
||||
@ -168,21 +134,22 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
||||
},
|
||||
},
|
||||
},
|
||||
// {
|
||||
// name: 'share-file-link',
|
||||
// displayName: 'Share file',
|
||||
// description: 'Create a link that contains a copy of the current file.',
|
||||
// groupId: 'code',
|
||||
// needsReview: false,
|
||||
// icon: 'link',
|
||||
// onSubmit: () => {
|
||||
// copyFileShareLink({
|
||||
// token: commandProps.authToken,
|
||||
// code: codeManager.code,
|
||||
// name: commandProps.projectData.project?.name || '',
|
||||
// units: commandProps.settings.defaultUnit,
|
||||
// }).catch(reportRejection)
|
||||
// },
|
||||
// },
|
||||
{
|
||||
name: 'share-file-link',
|
||||
displayName: 'Share file',
|
||||
hide: IS_NIGHTLY_OR_DEBUG ? undefined : 'desktop',
|
||||
description: 'Create a link that contains a copy of the current file.',
|
||||
groupId: 'code',
|
||||
needsReview: false,
|
||||
icon: 'link',
|
||||
onSubmit: () => {
|
||||
copyFileShareLink({
|
||||
token: commandProps.authToken,
|
||||
code: codeManager.code,
|
||||
name: commandProps.projectData.project?.name || '',
|
||||
units: commandProps.settings.defaultUnit,
|
||||
}).catch(reportRejection)
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { VITE_KC_SITE_APP_URL } from 'env'
|
||||
import { createCreateFileUrl } from './links'
|
||||
|
||||
describe(`link creation tests`, () => {
|
||||
@ -8,7 +9,7 @@ describe(`link creation tests`, () => {
|
||||
|
||||
// Converted with external online tools
|
||||
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
|
||||
const expectedLink = `http://localhost:3000/?create-file=true&name=test&units=mm&code=${expectedEncodedCode}&ask-open-desktop=true`
|
||||
const expectedLink = `${VITE_KC_SITE_APP_URL}/?create-file=true&name=test&units=mm&code=${expectedEncodedCode}&ask-open-desktop=true`
|
||||
|
||||
const result = createCreateFileUrl({ code, name, units })
|
||||
expect(result.toString()).toBe(expectedLink)
|
||||
|