Compare commits
111 Commits
Author | SHA1 | Date | |
---|---|---|---|
f8f44743fa | |||
f262eda12a | |||
9e1136195a | |||
4ff07ddaee | |||
1e565379a7 | |||
76e34ac4da | |||
4cd427bf91 | |||
f321ecdff0 | |||
d114ab798c | |||
69fec37107 | |||
8ca8c49cc3 | |||
b25fc302fd | |||
648b37c1dd | |||
18e5da5ca4 | |||
46524cda10 | |||
4585671a5d | |||
e7203b9e7a | |||
ab375f4b92 | |||
04ed6f52ee | |||
2332338ca1 | |||
41b97de3d1 | |||
8ef31a0be1 | |||
3adb42b5f2 | |||
20016b101e | |||
8d9dbf36c3 | |||
440704ed9f | |||
2261217a5d | |||
10da986649 | |||
10789d9c3c | |||
67cc4f5835 | |||
2692f2b73a | |||
965cb18059 | |||
a022b8ef6c | |||
4d24bf7c94 | |||
9a537da183 | |||
df81b76b8b | |||
ac3f7ab712 | |||
dac91d3b79 | |||
0698432abf | |||
0592d3b5da | |||
31e4d60045 | |||
c0817b00e4 | |||
4ea1d16fb6 | |||
d049bf33e8 | |||
7b11047d07 | |||
412e9568f2 | |||
9be208e5e1 | |||
842ef5ede9 | |||
3f855d7bad | |||
0a1a6e50cf | |||
d4e955289c | |||
c147a219f4 | |||
38513a1e25 | |||
c0c5c790ca | |||
8b60f75220 | |||
f91ad4331f | |||
59103a2118 | |||
9737c2550a | |||
bf9d01a8dd | |||
702e322f90 | |||
e82830754d | |||
7806377a5a | |||
859afa2fd8 | |||
0a5f3093fc | |||
b65f7939f6 | |||
c35dea5e07 | |||
fc66d4745f | |||
b313d26c2a | |||
00b94ead62 | |||
0531ea1ce9 | |||
5f9a4887c1 | |||
da7dfa16d8 | |||
363ae10658 | |||
ac4a6c84cf | |||
c6fad2e2dc | |||
013cb10961 | |||
6261083cb1 | |||
2b0ba37ed0 | |||
96174f3cf6 | |||
aed62ff912 | |||
9334d64608 | |||
4fa7d2d8c8 | |||
3e615dfdbc | |||
c9860af29f | |||
23a42f0195 | |||
a77fa639f3 | |||
0a5ad7c95b | |||
4a654523d2 | |||
73a7e2bfd6 | |||
eb0850fea9 | |||
029f76f273 | |||
28b5f7080c | |||
5b1dcfecd6 | |||
f89d191425 | |||
2f4e4b62a8 | |||
5ebd5c8dbb | |||
a9ceaf2678 | |||
c8afd3399b | |||
5dda4828c6 | |||
72acab752c | |||
81df38ad1c | |||
0576a2bef1 | |||
4b2f6b4647 | |||
69edaa4183 | |||
2eb7c382bf | |||
38913ecb98 | |||
debd06129f | |||
d38bd342a0 | |||
f026f10335 | |||
895d7ebc6d | |||
65edf17a44 |
@ -1,3 +1,3 @@
|
||||
[codespell]
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall
|
||||
skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts,./packages/codemirror-lang-kcl/test/all.test.ts
|
||||
ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser
|
||||
skip: **/target,node_modules,build,dist,./out,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./packages/codemirror-lang-kcl/test/all.test.ts,tsconfig.tsbuildinfo
|
||||
|
28
.eslintrc
@ -5,16 +5,32 @@
|
||||
},
|
||||
"plugins": [
|
||||
"css-modules",
|
||||
"jest",
|
||||
"jsx-a11y",
|
||||
"react",
|
||||
"react-hooks",
|
||||
"suggest-no-throw",
|
||||
"testing-library",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest",
|
||||
"plugin:css-modules/recommended"
|
||||
"plugin:css-modules/recommended",
|
||||
"plugin:jsx-a11y/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": "error",
|
||||
"jsx-a11y/click-events-have-key-events": "off",
|
||||
"jsx-a11y/no-autofocus": "off",
|
||||
"jsx-a11y/no-noninteractive-element-interactions": "off",
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
"name": "isNaN",
|
||||
"message": "Use Number.isNaN() instead."
|
||||
}
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
@ -25,6 +41,9 @@
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["e2e/**/*.ts"], // Update the pattern based on your file structure
|
||||
"extends": [
|
||||
"plugin:testing-library/react"
|
||||
],
|
||||
"rules": {
|
||||
"suggest-no-throw/suggest-no-throw": "off",
|
||||
"testing-library/prefer-screen-queries": "off",
|
||||
@ -33,6 +52,9 @@
|
||||
},
|
||||
{
|
||||
"files": ["src/**/*.test.ts"],
|
||||
"extends": [
|
||||
"plugin:testing-library/react"
|
||||
],
|
||||
"rules": {
|
||||
"suggest-no-throw/suggest-no-throw": "off",
|
||||
}
|
||||
|
2
.github/ci-cd-scripts/playwright-electron.sh
vendored
@ -21,7 +21,7 @@ if [[ ! -f "test-results/.last-run.json" ]]; then
|
||||
fi
|
||||
|
||||
retry=1
|
||||
max_retrys=4
|
||||
max_retrys=5
|
||||
|
||||
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
|
||||
while [[ $retry -le $max_retrys ]]; do
|
||||
|
17
.github/dependabot.yml
vendored
@ -6,23 +6,29 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: 'npm' # See documentation for possible values
|
||||
directory: '/' # Location of package manifests
|
||||
directories:
|
||||
- '/'
|
||||
- '/packages/codemirror-lang-kcl/'
|
||||
- '/packages/codemirror-lsp-client/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
interval: weekly
|
||||
day: monday
|
||||
reviewers:
|
||||
- franknoirot
|
||||
- irev-dev
|
||||
- package-ecosystem: 'github-actions' # See documentation for possible values
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
interval: weekly
|
||||
day: monday
|
||||
reviewers:
|
||||
- adamchalmers
|
||||
- jessfraz
|
||||
- package-ecosystem: 'cargo' # See documentation for possible values
|
||||
directory: '/src/wasm-lib/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
interval: weekly
|
||||
day: monday
|
||||
reviewers:
|
||||
- adamchalmers
|
||||
- jessfraz
|
||||
@ -30,3 +36,6 @@ updates:
|
||||
serde-dependencies:
|
||||
patterns:
|
||||
- "serde*"
|
||||
wasm-bindgen-deps:
|
||||
patterns:
|
||||
- "wasm-bindgen*"
|
||||
|
2
.github/workflows/build-and-store-wasm.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
|
||||
# Upload the WASM bundle as an artifact
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wasm-bundle
|
||||
path: src/wasm-lib/pkg
|
||||
|
8
.github/workflows/build-apps.yml
vendored
@ -126,7 +126,13 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn' # Set this to npm, yarn or pnpm.
|
||||
|
||||
- run: yarn install
|
||||
- name: yarn install
|
||||
# Windows is picky sometimes and fails on fetch. Step takes about ~30s
|
||||
uses: nick-fields/retry@v3.0.0
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: yarn install
|
||||
|
||||
- run: yarn tronb:vite
|
||||
|
||||
|
44
.github/workflows/cargo-bench.yml
vendored
@ -1,44 +0,0 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '**.rs'
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- .github/workflows/cargo-bench.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.rs'
|
||||
- '**/Cargo.toml'
|
||||
- '**/Cargo.lock'
|
||||
- '**/rust-toolchain.toml'
|
||||
- .github/workflows/cargo-bench.yml
|
||||
workflow_dispatch:
|
||||
permissions: read-all
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
name: cargo bench
|
||||
jobs:
|
||||
cargo-bench:
|
||||
name: Benchmark with iai
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cargo install cargo-criterion
|
||||
sudo apt update
|
||||
sudo apt install -y valgrind
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.6.1
|
||||
- name: Benchmark kcl library
|
||||
shell: bash
|
||||
run: |-
|
||||
cd src/wasm-lib/kcl; cargo bench --all-features -- iai
|
||||
env:
|
||||
KITTYCAD_API_TOKEN: ${{secrets.KITTYCAD_API_TOKEN}}
|
||||
|
32
.github/workflows/codemirror-lang-kcl.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
name: CodeMirror Lang KCL
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
yarn-unit-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- run: yarn install
|
||||
working-directory: packages/codemirror-lang-kcl
|
||||
|
||||
- run: yarn tsc
|
||||
working-directory: packages/codemirror-lang-kcl
|
||||
|
||||
- name: run unit tests
|
||||
run: yarn test
|
||||
working-directory: packages/codemirror-lang-kcl
|
56
.github/workflows/e2e-tests.yml
vendored
@ -126,20 +126,20 @@ jobs:
|
||||
- name: build electron
|
||||
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 }}
|
||||
# - 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:
|
||||
@ -162,20 +162,20 @@ jobs:
|
||||
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 }}
|
||||
# - 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'
|
||||
|
2
.gitignore
vendored
@ -44,7 +44,7 @@ e2e/playwright/temp3.png
|
||||
e2e/playwright/export-snapshots/*
|
||||
!e2e/playwright/export-snapshots/*.png
|
||||
|
||||
|
||||
/kcl-samples
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
|
43
docs/kcl/helixRevolutions.md
Normal file
@ -4,14 +4,16 @@ excerpt: "Import a CAD file."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
**WARNING:** This function is deprecated.
|
||||
|
||||
Import a CAD file.
|
||||
|
||||
**DEPRECATED** Prefer to use import statements.
|
||||
|
||||
For formats lacking unit data (such as STL, OBJ, or PLY files), the default unit of measurement is millimeters. Alternatively you may specify the unit by passing your desired measurement unit in the options parameter. When importing a GLTF file, the bin file will be imported as well. Import paths are relative to the current project directory.
|
||||
|
||||
Note: The import command currently only works when using the native Modeling App.
|
||||
|
||||
For importing KCL functions using the `import` statement, see the docs on [KCL modules](/docs/kcl/modules).
|
||||
|
||||
```js
|
||||
import(file_path: String, options?: ImportFormat) -> ImportedGeometry
|
||||
```
|
||||
|
@ -48,11 +48,10 @@ layout: manual
|
||||
* [`getOppositeEdge`](kcl/getOppositeEdge)
|
||||
* [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge)
|
||||
* [`helix`](kcl/helix)
|
||||
* [`helixRevolutions`](kcl/helixRevolutions)
|
||||
* [`hole`](kcl/hole)
|
||||
* [`hollow`](kcl/hollow)
|
||||
* [`import`](kcl/import)
|
||||
* [`inch`](kcl/inch)
|
||||
* [`int`](kcl/int)
|
||||
* [`lastSegX`](kcl/lastSegX)
|
||||
* [`lastSegY`](kcl/lastSegY)
|
||||
* [`legAngX`](kcl/legAngX)
|
||||
@ -81,6 +80,7 @@ layout: manual
|
||||
* [`pi`](kcl/pi)
|
||||
* [`polar`](kcl/polar)
|
||||
* [`polygon`](kcl/polygon)
|
||||
* [`pop`](kcl/pop)
|
||||
* [`pow`](kcl/pow)
|
||||
* [`profileStart`](kcl/profileStart)
|
||||
* [`profileStartX`](kcl/profileStartX)
|
||||
|
@ -4,6 +4,8 @@ excerpt: "Convert a number to an integer."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
**WARNING:** This function is deprecated.
|
||||
|
||||
Convert a number to an integer.
|
||||
|
||||
DEPRECATED use floor(), ceil(), or round().
|
||||
|
39
docs/kcl/pop.md
Normal file
25465
docs/kcl/std.json
@ -1,19 +1,19 @@
|
||||
---
|
||||
title: "AxisOrEdgeReference"
|
||||
excerpt: "Axis or tagged edge."
|
||||
title: "Axis2dOrEdgeReference"
|
||||
excerpt: "A 2D axis or tagged edge."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Axis or tagged edge.
|
||||
A 2D axis or tagged edge.
|
||||
|
||||
|
||||
|
||||
|
||||
**This schema accepts any of the following:**
|
||||
|
||||
Axis and origin.
|
||||
2D axis and origin.
|
||||
|
||||
[`AxisAndOrigin`](/docs/kcl/types/AxisAndOrigin)
|
||||
[`AxisAndOrigin2d`](/docs/kcl/types/AxisAndOrigin2d)
|
||||
|
||||
|
||||
|
42
docs/kcl/types/Axis3dOrEdgeReference.md
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
title: "Axis3dOrEdgeReference"
|
||||
excerpt: "A 3D axis or tagged edge."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A 3D axis or tagged edge.
|
||||
|
||||
|
||||
|
||||
|
||||
**This schema accepts any of the following:**
|
||||
|
||||
3D axis and origin.
|
||||
|
||||
[`AxisAndOrigin3d`](/docs/kcl/types/AxisAndOrigin3d)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Tagged edge.
|
||||
|
||||
[`EdgeReference`](/docs/kcl/types/EdgeReference)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
---
|
||||
title: "AxisAndOrigin"
|
||||
excerpt: "Axis and origin."
|
||||
title: "AxisAndOrigin2d"
|
||||
excerpt: "A 2D axis and origin."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Axis and origin.
|
||||
A 2D axis and origin.
|
||||
|
||||
|
||||
|
105
docs/kcl/types/AxisAndOrigin3d.md
Normal file
@ -0,0 +1,105 @@
|
||||
---
|
||||
title: "AxisAndOrigin3d"
|
||||
excerpt: "A 3D axis and origin."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A 3D axis and origin.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**This schema accepts exactly one of the following:**
|
||||
|
||||
X-axis.
|
||||
|
||||
**enum:** `X`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Y-axis.
|
||||
|
||||
**enum:** `Y`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Z-axis.
|
||||
|
||||
**enum:** `Z`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Flip the X-axis.
|
||||
|
||||
**enum:** `-X`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Flip the Y-axis.
|
||||
|
||||
**enum:** `-Y`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
Flip the Z-axis.
|
||||
|
||||
**enum:** `-Z`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `custom` |`object`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
28
docs/kcl/types/Face.md
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
title: "Face"
|
||||
excerpt: "A face."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A face.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `id` |`string`| The id of the face. | No |
|
||||
| `value` |`string`| The tag of the face. | No |
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A face. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
26
docs/kcl/types/Helix.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
title: "Helix"
|
||||
excerpt: "A helix."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A helix.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `value` |`string`| The id of the helix. | No |
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
---
|
||||
title: "HelixData"
|
||||
excerpt: "Data for helices."
|
||||
excerpt: "Data for a helix."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data for helices.
|
||||
Data for a helix.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
@ -19,6 +19,8 @@ Data for helices.
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No |
|
||||
| `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No |
|
||||
| `length` |`number`| Length of the helix. This is not necessary if the helix is created around an edge. If not given the length of the edge is used. | No |
|
||||
| `radius` |`number`| Radius of the helix. | No |
|
||||
| `axis` |[`Axis3dOrEdgeReference`](/docs/kcl/types/Axis3dOrEdgeReference)| Axis to use as mirror. | No |
|
||||
|
||||
|
||||
|
24
docs/kcl/types/HelixRevolutionsData.md
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
title: "HelixRevolutionsData"
|
||||
excerpt: "Data for helix revolutions."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
Data for helix revolutions.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | No |
|
||||
| `length` |`number`| Length of the helix. If this argument is not provided, the height of the solid is used. | No |
|
||||
|
||||
|
26
docs/kcl/types/HelixValue.md
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
title: "HelixValue"
|
||||
excerpt: "A helix."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A helix.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `value` |`string`| The id of the helix. | No |
|
||||
| `revolutions` |`number`| Number of revolutions. | No |
|
||||
| `angleStart` |`number`| Start angle (in degrees). | No |
|
||||
| `ccw` |`boolean`| Is the helix rotation counter clockwise? | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A helix. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
@ -168,7 +168,6 @@ Any KCL value.
|
||||
|
||||
|
||||
----
|
||||
A plane.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
@ -181,17 +180,10 @@ A plane.
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: [`Plane`](/docs/kcl/types/Plane)| | No |
|
||||
| `id` |`string`| The id of the plane. | No |
|
||||
| `value` |[`PlaneType`](/docs/kcl/types/PlaneType)| Any KCL value. | No |
|
||||
| `origin` |[`Point3d`](/docs/kcl/types/Point3d)| Origin of the plane. | No |
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
| `value` |[`Plane`](/docs/kcl/types/Plane)| Any KCL value. | No |
|
||||
|
||||
|
||||
----
|
||||
A face.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
@ -203,14 +195,8 @@ A face.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Face`| | No |
|
||||
| `id` |`string`| The id of the face. | No |
|
||||
| `value` |`string`| The tag of the face. | No |
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
| `type` |enum: [`Face`](/docs/kcl/types/Face)| | No |
|
||||
| `value` |[`Face`](/docs/kcl/types/Face)| Any KCL value. | No |
|
||||
|
||||
|
||||
----
|
||||
@ -246,7 +232,6 @@ A face.
|
||||
|
||||
|
||||
----
|
||||
An solid is a collection of extrude surfaces.
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
@ -259,14 +244,7 @@ An solid is a collection of extrude surfaces.
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: [`Solid`](/docs/kcl/types/Solid)| | No |
|
||||
| `id` |`string`| The id of the solid. | No |
|
||||
| `value` |`[` [`ExtrudeSurface`](/docs/kcl/types/ExtrudeSurface) `]`| The extrude surfaces. | No |
|
||||
| `sketch` |[`Sketch`](/docs/kcl/types/Sketch)| The sketch. | No |
|
||||
| `height` |`number`| The height of the solid. | No |
|
||||
| `startCapId` |`string`| The id of the extrusion start cap | No |
|
||||
| `endCapId` |`string`| The id of the extrusion end cap | No |
|
||||
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
| `value` |[`Solid`](/docs/kcl/types/Solid)| Any KCL value. | No |
|
||||
|
||||
|
||||
----
|
||||
@ -285,6 +263,22 @@ An solid is a collection of extrude surfaces.
|
||||
| `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: [`Helix`](/docs/kcl/types/Helix)| | No |
|
||||
| `value` |[`Helix`](/docs/kcl/types/Helix)| Any KCL value. | No |
|
||||
|
||||
|
||||
----
|
||||
Data for an imported geometry.
|
||||
|
||||
|
@ -16,6 +16,6 @@ Data for a mirror.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis to use as mirror. | No |
|
||||
| `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis to use as mirror. | No |
|
||||
|
||||
|
||||
|
@ -22,6 +22,7 @@ A plane.
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A plane. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
|
@ -17,7 +17,7 @@ Data for revolution surfaces.
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `angle` |`number` (**maximum:** 360.0) (**minimum:** -360.0)| Angle to revolve (in degrees). Default is 360. | No |
|
||||
| `axis` |[`AxisOrEdgeReference`](/docs/kcl/types/AxisOrEdgeReference)| Axis of revolution. | No |
|
||||
| `axis` |[`Axis2dOrEdgeReference`](/docs/kcl/types/Axis2dOrEdgeReference)| Axis of revolution. | No |
|
||||
| `tolerance` |`number`| Tolerance for the revolve operation. | No |
|
||||
|
||||
|
||||
|
@ -21,6 +21,7 @@ A sketch is a collection of paths.
|
||||
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
|
||||
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
|
||||
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch is a collection of paths. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
||||
|
@ -30,6 +30,7 @@ A sketch is a collection of paths.
|
||||
| `on` |[`SketchSurface`](/docs/kcl/types/SketchSurface)| What the sketch is on (can be a plane or a face). | No |
|
||||
| `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No |
|
||||
| `tags` |`object`| Tag identifiers that have been declared in this sketch. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch or a group of sketches. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
||||
|
@ -31,6 +31,7 @@ A plane.
|
||||
| `xAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s X axis be? | No |
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the plane’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
@ -54,6 +55,7 @@ A face.
|
||||
| `yAxis` |[`Point3d`](/docs/kcl/types/Point3d)| What should the face’s Y axis be? | No |
|
||||
| `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No |
|
||||
| `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A sketch type. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| | No |
|
||||
|
||||
|
||||
|
@ -23,6 +23,7 @@ An solid is a collection of extrude surfaces.
|
||||
| `startCapId` |`string`| The id of the extrusion start cap | No |
|
||||
| `endCapId` |`string`| The id of the extrusion end cap | No |
|
||||
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| An solid is a collection of extrude surfaces. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
||||
|
@ -32,6 +32,7 @@ An solid is a collection of extrude surfaces.
|
||||
| `startCapId` |`string`| The id of the extrusion start cap | No |
|
||||
| `endCapId` |`string`| The id of the extrusion end cap | No |
|
||||
| `edgeCuts` |`[` [`EdgeCut`](/docs/kcl/types/EdgeCut) `]`| Chamfers or fillets on this solid. | No |
|
||||
| `units` |[`UnitLen`](/docs/kcl/types/UnitLen)| A solid or a group of solids. | No |
|
||||
| `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No |
|
||||
|
||||
|
||||
|
@ -16,7 +16,7 @@ Data for a sweep.
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `path` |[`Sketch`](/docs/kcl/types/Sketch)| The path to sweep along. | No |
|
||||
| `path` |[`SweepPath`](/docs/kcl/types/SweepPath)| The path to sweep along. | No |
|
||||
| `sectional` |`boolean`| If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components. | No |
|
||||
| `tolerance` |`number`| Tolerance for the sweep operation. | No |
|
||||
|
||||
|
42
docs/kcl/types/SweepPath.md
Normal file
@ -0,0 +1,42 @@
|
||||
---
|
||||
title: "SweepPath"
|
||||
excerpt: "A path to sweep along."
|
||||
layout: manual
|
||||
---
|
||||
|
||||
A path to sweep along.
|
||||
|
||||
|
||||
|
||||
|
||||
**This schema accepts any of the following:**
|
||||
|
||||
A path to sweep along.
|
||||
|
||||
[`Sketch`](/docs/kcl/types/Sketch)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
A path to sweep along.
|
||||
|
||||
[`Helix`](/docs/kcl/types/Helix)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
|
107
docs/kcl/types/UnitLen.md
Normal file
@ -0,0 +1,107 @@
|
||||
---
|
||||
title: "UnitLen"
|
||||
excerpt: ""
|
||||
layout: manual
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**This schema accepts exactly one of the following:**
|
||||
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Mm`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Cm`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `M`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Inches`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Feet`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
**Type:** `object`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description | Required |
|
||||
|----------|------|-------------|----------|
|
||||
| `type` |enum: `Yards`| | No |
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
@ -280,7 +280,7 @@ test(
|
||||
|
||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
||||
await expect(page.getByText('New Project')).toBeVisible()
|
||||
await expect(page.getByText('Create project')).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Opening the router-template project should load', async () => {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { test, expect } from './zoo-test'
|
||||
|
||||
import { getUtils } from './test-utils'
|
||||
import * as fsp from 'fs/promises'
|
||||
import { executorInputPath, getUtils } from './test-utils'
|
||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||
import path from 'path'
|
||||
|
||||
test.describe('Command bar tests', () => {
|
||||
test('Extrude from command bar selects extrude line after', async ({
|
||||
@ -45,46 +46,6 @@ test.describe('Command bar tests', () => {
|
||||
)
|
||||
})
|
||||
|
||||
// TODO: fix this test after the electron migration
|
||||
test.fixme('Fillet from command bar', async ({ page, homePage }) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-5, -5], %)
|
||||
|> line([0, 10], %)
|
||||
|> line([10, 0], %)
|
||||
|> line([0, -10], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude001 = extrude(-10, sketch001)`
|
||||
)
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const selectSegment = () => page.getByText(`line([0, -10], %)`).click()
|
||||
|
||||
await selectSegment()
|
||||
await page.waitForTimeout(100)
|
||||
await page.getByRole('button', { name: 'Fillet' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Enter') // skip selection
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Enter') // accept default radius
|
||||
await page.waitForTimeout(100)
|
||||
await page.keyboard.press('Enter') // submit
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.locator('.cm-activeLine')).toContainText(
|
||||
`fillet({ radius = ${KCL_DEFAULT_LENGTH}, tags = [seg01] }, %)`
|
||||
)
|
||||
})
|
||||
|
||||
test('Command bar can change a setting, and switch back and forth between arguments', async ({
|
||||
page,
|
||||
homePage,
|
||||
@ -345,4 +306,132 @@ test.describe('Command bar tests', () => {
|
||||
await arcToolCommand.click()
|
||||
await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true')
|
||||
})
|
||||
|
||||
test(`Reacts to query param to open "import from URL" command`, async ({
|
||||
page,
|
||||
cmdBar,
|
||||
editor,
|
||||
homePage,
|
||||
}) => {
|
||||
await test.step(`Prepare and navigate to home page with query params`, async () => {
|
||||
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
|
||||
await homePage.expectState({
|
||||
projectCards: [],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await page.goto(page.url() + targetURL)
|
||||
expect(page.url()).toContain(targetURL)
|
||||
})
|
||||
|
||||
await test.step(`Submit the command`, async () => {
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Import file from URL',
|
||||
currentArgKey: 'method',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Method: '',
|
||||
Name: 'test',
|
||||
Code: '1 line',
|
||||
},
|
||||
highlightedHeaderArg: 'method',
|
||||
})
|
||||
await cmdBar.selectOption({ name: 'New Project' }).click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
commandName: 'Import file from URL',
|
||||
headerArguments: {
|
||||
Method: 'New project',
|
||||
Name: 'test',
|
||||
Code: '1 line',
|
||||
},
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
|
||||
await editor.expectEditor.toContain('extrusionDistance = 12')
|
||||
})
|
||||
})
|
||||
|
||||
test(`"import from URL" can add to existing project`, async ({
|
||||
page,
|
||||
cmdBar,
|
||||
editor,
|
||||
homePage,
|
||||
toolbar,
|
||||
context,
|
||||
}) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const testProjectDir = path.join(dir, 'testProjectDir')
|
||||
await Promise.all([fsp.mkdir(testProjectDir, { recursive: true })])
|
||||
await Promise.all([
|
||||
fsp.copyFile(
|
||||
executorInputPath('cylinder.kcl'),
|
||||
path.join(testProjectDir, 'main.kcl')
|
||||
),
|
||||
])
|
||||
})
|
||||
await test.step(`Prepare and navigate to home page with query params`, async () => {
|
||||
const targetURL = `?create-file&name=test&units=mm&code=ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D&ask-open-desktop`
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
fileCount: 1,
|
||||
title: 'testProjectDir',
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await page.goto(page.url() + targetURL)
|
||||
expect(page.url()).toContain(targetURL)
|
||||
})
|
||||
|
||||
await test.step(`Submit the command`, async () => {
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Import file from URL',
|
||||
currentArgKey: 'method',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Method: '',
|
||||
Name: 'test',
|
||||
Code: '1 line',
|
||||
},
|
||||
highlightedHeaderArg: 'method',
|
||||
})
|
||||
await cmdBar.selectOption({ name: 'Existing Project' }).click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Import file from URL',
|
||||
currentArgKey: 'projectName',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Method: 'Existing project',
|
||||
Name: 'test',
|
||||
ProjectName: '',
|
||||
Code: '1 line',
|
||||
},
|
||||
highlightedHeaderArg: 'projectName',
|
||||
})
|
||||
await cmdBar.selectOption({ name: 'testProjectDir' }).click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
commandName: 'Import file from URL',
|
||||
headerArguments: {
|
||||
Method: 'Existing project',
|
||||
ProjectName: 'testProjectDir',
|
||||
Name: 'test',
|
||||
Code: '1 line',
|
||||
},
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
|
||||
await editor.expectEditor.toContain('extrusionDistance = 12')
|
||||
await toolbar.openPane('files')
|
||||
await toolbar.expectFileTreeState(['main.kcl', 'test.kcl'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -38,14 +38,14 @@ test.describe('Debug pane', () => {
|
||||
// Set the code in the code editor.
|
||||
await u.codeLocator.click()
|
||||
await page.keyboard.type(code, { delay: 0 })
|
||||
// Scroll to the feature tree.
|
||||
// Scroll to the artifact graph.
|
||||
await tree.scrollIntoViewIfNeeded()
|
||||
// Expand the feature tree.
|
||||
await tree.getByText('Feature Tree').click()
|
||||
// Expand the artifact graph.
|
||||
await tree.getByText('Artifact Graph').click()
|
||||
// Just expanded the details, making the element taller, so scroll again.
|
||||
await tree.getByText('Plane').first().scrollIntoViewIfNeeded()
|
||||
})
|
||||
// Extract the artifact IDs from the debug feature tree.
|
||||
// Extract the artifact IDs from the debug artifact graph.
|
||||
const initialSegmentIds = await segment.innerText({ timeout: 5_000 })
|
||||
// The artifact ID should include a UUID.
|
||||
expect(initialSegmentIds).toMatch(
|
||||
|
@ -4,7 +4,6 @@ import { expect } from '@playwright/test'
|
||||
type CmdBarSerialised =
|
||||
| {
|
||||
stage: 'commandBarClosed'
|
||||
// TODO no more properties needed but needs to be implemented in _serialiseCmdBar
|
||||
}
|
||||
| {
|
||||
stage: 'pickCommand'
|
||||
@ -37,6 +36,9 @@ export class CmdBarFixture {
|
||||
}
|
||||
|
||||
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
|
||||
if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) {
|
||||
return { stage: 'commandBarClosed' }
|
||||
}
|
||||
const reviewForm = this.page.locator('#review-form')
|
||||
const getHeaderArgs = async () => {
|
||||
const inputs = await this.page.getByTestId('cmd-bar-input-tab').all()
|
||||
@ -135,4 +137,27 @@ export class CmdBarFixture {
|
||||
await promptEditCommand.first().click()
|
||||
}
|
||||
}
|
||||
|
||||
get cmdSearchInput() {
|
||||
return this.page.getByTestId('cmd-bar-search')
|
||||
}
|
||||
|
||||
get argumentInput() {
|
||||
return this.page.getByTestId('cmd-bar-arg-value')
|
||||
}
|
||||
|
||||
get cmdOptions() {
|
||||
return this.page.getByTestId('cmd-bar-option')
|
||||
}
|
||||
|
||||
chooseCommand = async (commandName: string) => {
|
||||
await this.cmdOptions.getByText(commandName).click()
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an option from the command bar
|
||||
*/
|
||||
selectOption = (options: Parameters<typeof this.page.getByRole>[1]) => {
|
||||
return this.page.getByRole('option', options)
|
||||
}
|
||||
}
|
||||
|
@ -121,18 +121,23 @@ export class AuthenticatedTronApp {
|
||||
|
||||
export const fixtures = {
|
||||
cmdBar: async ({ page }: { page: Page }, use: any) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
await use(new CmdBarFixture(page))
|
||||
},
|
||||
editor: async ({ page }: { page: Page }, use: any) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
await use(new EditorFixture(page))
|
||||
},
|
||||
toolbar: async ({ page }: { page: Page }, use: any) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
await use(new ToolbarFixture(page))
|
||||
},
|
||||
scene: async ({ page }: { page: Page }, use: any) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
await use(new SceneFixture(page))
|
||||
},
|
||||
homePage: async ({ page }: { page: Page }, use: any) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
await use(new HomePageFixture(page))
|
||||
},
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ export class HomePageFixture {
|
||||
.toEqual(expectedState)
|
||||
}
|
||||
|
||||
createAndGoToProject = async (projectTitle: string) => {
|
||||
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
|
||||
await expect(this.projectSection).not.toHaveText('Loading your Projects...')
|
||||
await this.projectButtonNew.click()
|
||||
await this.projectTextName.click()
|
||||
|
@ -36,7 +36,8 @@ type DragFromHandler = (
|
||||
|
||||
export class SceneFixture {
|
||||
public page: Page
|
||||
|
||||
public streamWrapper!: Locator
|
||||
public loadingIndicator!: Locator
|
||||
private exeIndicator!: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
@ -64,6 +65,8 @@ export class SceneFixture {
|
||||
this.page = page
|
||||
|
||||
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
|
||||
this.streamWrapper = page.getByTestId('stream')
|
||||
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
|
||||
}
|
||||
|
||||
makeMouseHelpers = (
|
||||
|
@ -14,6 +14,9 @@ export class ToolbarFixture {
|
||||
|
||||
extrudeButton!: Locator
|
||||
loftButton!: Locator
|
||||
sweepButton!: Locator
|
||||
filletButton!: Locator
|
||||
chamferButton!: Locator
|
||||
shellButton!: Locator
|
||||
offsetPlaneButton!: Locator
|
||||
startSketchBtn!: Locator
|
||||
@ -40,6 +43,9 @@ export class ToolbarFixture {
|
||||
this.page = page
|
||||
this.extrudeButton = page.getByTestId('extrude')
|
||||
this.loftButton = page.getByTestId('loft')
|
||||
this.sweepButton = page.getByTestId('sweep')
|
||||
this.filletButton = page.getByTestId('fillet3d')
|
||||
this.chamferButton = page.getByTestId('chamfer3d')
|
||||
this.shellButton = page.getByTestId('shell')
|
||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||
this.startSketchBtn = page.getByTestId('sketch')
|
||||
@ -57,6 +63,10 @@ export class ToolbarFixture {
|
||||
this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
|
||||
}
|
||||
|
||||
get logoLink() {
|
||||
return this.page.getByTestId('app-logo')
|
||||
}
|
||||
|
||||
startSketchPlaneSelection = async () =>
|
||||
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
|
||||
|
||||
|
@ -756,6 +756,17 @@ test(`Offset plane point-and-click`, async ({
|
||||
})
|
||||
await scene.expectPixelColor([74, 74, 74], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step('Delete offset plane via feature tree selection', async () => {
|
||||
await editor.closePane()
|
||||
const operationButton = await toolbar.getFeatureTreeOperation(
|
||||
'Offset Plane',
|
||||
0
|
||||
)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await scene.expectPixelColor([50, 51, 96], testPoint, 15)
|
||||
})
|
||||
})
|
||||
|
||||
const loftPointAndClickCases = [
|
||||
@ -818,12 +829,6 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
|
||||
})
|
||||
await selectSketches()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: { Selection: '2 faces' },
|
||||
commandName: 'Loft',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
} else {
|
||||
await test.step(`Preselect the two sketches`, async () => {
|
||||
@ -833,12 +838,6 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
|
||||
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
|
||||
await toolbar.loftButton.click()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: { Selection: '2 faces' },
|
||||
commandName: 'Loft',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
}
|
||||
|
||||
@ -851,6 +850,666 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => {
|
||||
})
|
||||
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step('Delete loft via feature tree selection', async () => {
|
||||
await editor.closePane()
|
||||
const operationButton = await toolbar.getFeatureTreeOperation('Loft', 0)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
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 ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||
plane001 = offsetPlane('XZ', 50)
|
||||
sketch002 = startSketchOn(plane001)
|
||||
|> circle({ center = [0, 0], radius = 20 }, %)
|
||||
loft001 = loft([sketch001, sketch002])
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
|
||||
// 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)
|
||||
|
||||
await test.step(`Delete loft`, async () => {
|
||||
// Check for loft
|
||||
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
|
||||
await clickOnSketch1()
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`
|
||||
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||
`)
|
||||
await page.keyboard.press('Backspace')
|
||||
// Check for sketch 1
|
||||
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step('Delete sketch002', async () => {
|
||||
await page.waitForTimeout(1000)
|
||||
await clickOnSketch2()
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`
|
||||
|> circle({ center = [0, 0], radius = 20 }, %)
|
||||
`)
|
||||
await page.keyboard.press('Backspace')
|
||||
// Check for plane001
|
||||
await scene.expectPixelColor([228, 228, 228], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step('Delete plane001', async () => {
|
||||
await page.waitForTimeout(1000)
|
||||
await clickOnSketch2()
|
||||
await expect(page.locator('.cm-activeLine')).toHaveText(`
|
||||
plane001 = offsetPlane('XZ', 50)
|
||||
`)
|
||||
await page.keyboard.press('Backspace')
|
||||
// Check for sketch 1
|
||||
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
|
||||
})
|
||||
})
|
||||
|
||||
test(`Sweep point-and-click`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('YZ')
|
||||
|> circle({
|
||||
center = [0, 0],
|
||||
radius = 500
|
||||
}, %)
|
||||
sketch002 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> xLine(-500, %)
|
||||
|> tangentialArcTo([-2000, 500], %)
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
// 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 sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
|
||||
|
||||
await test.step(`Look for sketch001`, async () => {
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step(`Go through the command bar flow`, async () => {
|
||||
await toolbar.sweepButton.click()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'target',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Target: '',
|
||||
Trajectory: '',
|
||||
},
|
||||
highlightedHeaderArg: 'target',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch1()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'trajectory',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Target: '1 face',
|
||||
Trajectory: '',
|
||||
},
|
||||
highlightedHeaderArg: 'trajectory',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch2()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.waitForTimeout(500)
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||
await scene.expectPixelColor([135, 64, 73], testPoint, 15)
|
||||
await toolbar.openPane('code')
|
||||
await editor.expectEditor.toContain(sweepDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: [sweepDeclaration],
|
||||
highlightedCode: '',
|
||||
})
|
||||
await toolbar.closePane('code')
|
||||
})
|
||||
|
||||
await test.step('Delete sweep via feature tree selection', async () => {
|
||||
await toolbar.openPane('feature-tree')
|
||||
await page.waitForTimeout(500)
|
||||
const operationButton = await toolbar.getFeatureTreeOperation('Sweep', 0)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await page.waitForTimeout(500)
|
||||
await toolbar.closePane('feature-tree')
|
||||
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
|
||||
})
|
||||
})
|
||||
|
||||
test(`Sweep point-and-click failing validation`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('YZ')
|
||||
|> circle({
|
||||
center = [0, 0],
|
||||
radius = 500
|
||||
}, %)
|
||||
sketch002 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> xLine(-500, %)
|
||||
|> lineTo([-2000, 500], %)
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
// 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)
|
||||
|
||||
await test.step(`Look for sketch001`, async () => {
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step(`Go through the command bar flow and fail validation with a toast`, async () => {
|
||||
await toolbar.sweepButton.click()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'target',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Target: '',
|
||||
Trajectory: '',
|
||||
},
|
||||
highlightedHeaderArg: 'target',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch1()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'trajectory',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Target: '1 face',
|
||||
Trajectory: '',
|
||||
},
|
||||
highlightedHeaderArg: 'trajectory',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch2()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(
|
||||
page.getByText('Unable to sweep with the provided selection')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test(`Fillet point-and-click`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
// Code samples
|
||||
const initialCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-12, -6], %)
|
||||
|> line([0, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -12], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude001 = extrude(-12, sketch001)
|
||||
`
|
||||
const firstFilletDeclaration = 'fillet({ radius = 5, tags = [seg01] }, %)'
|
||||
const secondFilletDeclaration =
|
||||
'fillet({ radius = 5, tags = [getOppositeEdge(seg01)] }, %)'
|
||||
|
||||
// Locators
|
||||
const firstEdgeLocation = { x: 600, y: 193 }
|
||||
const secondEdgeLocation = { x: 600, y: 383 }
|
||||
const bodyLocation = { x: 630, y: 290 }
|
||||
const [clickOnFirstEdge] = scene.makeMouseHelpers(
|
||||
firstEdgeLocation.x,
|
||||
firstEdgeLocation.y
|
||||
)
|
||||
const [clickOnSecondEdge] = scene.makeMouseHelpers(
|
||||
secondEdgeLocation.x,
|
||||
secondEdgeLocation.y
|
||||
)
|
||||
|
||||
// Colors
|
||||
const edgeColorWhite: [number, number, number] = [248, 248, 248]
|
||||
const edgeColorYellow: [number, number, number] = [251, 251, 40] // Mac:B=67 Ubuntu:B=12
|
||||
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,
|
||||
secondEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
|
||||
// wait for stream to load
|
||||
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
|
||||
})
|
||||
|
||||
// Test 1: Command bar flow with preselected edges
|
||||
await test.step(`Select first edge`, async () => {
|
||||
await scene.expectPixelColor(
|
||||
edgeColorWhite,
|
||||
firstEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
await clickOnFirstEdge()
|
||||
await scene.expectPixelColor(
|
||||
edgeColorYellow,
|
||||
firstEdgeLocation,
|
||||
highTolerance // Ubuntu color mismatch can require high tolerance
|
||||
)
|
||||
})
|
||||
|
||||
await test.step(`Apply fillet to the preselected edge`, async () => {
|
||||
await page.waitForTimeout(100)
|
||||
await toolbar.filletButton.click()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
highlightedHeaderArg: 'selection',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Radius: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
highlightedHeaderArg: 'radius',
|
||||
currentArgKey: 'radius',
|
||||
currentArgValue: '5',
|
||||
headerArguments: {
|
||||
Selection: '1 face',
|
||||
Radius: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
headerArguments: {
|
||||
Selection: '1 face',
|
||||
Radius: '5',
|
||||
},
|
||||
stage: 'review',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor`, async () => {
|
||||
await editor.expectEditor.toContain(firstFilletDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: ['|>fillet({radius=5,tags=[seg01]},%)'],
|
||||
highlightedCode: '',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step(`Confirm scene has changed`, async () => {
|
||||
await scene.expectPixelColor(filletColor, firstEdgeLocation, lowTolerance)
|
||||
})
|
||||
|
||||
// Test 2: Command bar flow without preselected edges
|
||||
await test.step(`Open fillet UI without selecting edges`, async () => {
|
||||
await page.waitForTimeout(100)
|
||||
await toolbar.filletButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Radius: '',
|
||||
},
|
||||
highlightedHeaderArg: 'selection',
|
||||
commandName: 'Fillet',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step(`Select second edge`, async () => {
|
||||
await scene.expectPixelColor(
|
||||
edgeColorWhite,
|
||||
secondEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
await clickOnSecondEdge()
|
||||
await scene.expectPixelColor(
|
||||
edgeColorYellow,
|
||||
secondEdgeLocation,
|
||||
highTolerance // Ubuntu color mismatch can require high tolerance
|
||||
)
|
||||
})
|
||||
|
||||
await test.step(`Apply fillet to the second edge`, async () => {
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
highlightedHeaderArg: 'selection',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Radius: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
highlightedHeaderArg: 'radius',
|
||||
currentArgKey: 'radius',
|
||||
currentArgValue: '5',
|
||||
headerArguments: {
|
||||
Selection: '1 sweepEdge',
|
||||
Radius: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Fillet',
|
||||
headerArguments: {
|
||||
Selection: '1 sweepEdge',
|
||||
Radius: '5',
|
||||
},
|
||||
stage: 'review',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor`, async () => {
|
||||
await editor.expectEditor.toContain(secondFilletDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: ['radius=5,'],
|
||||
highlightedCode: '',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step(`Confirm scene has changed`, async () => {
|
||||
await scene.expectPixelColor(
|
||||
backgroundColor,
|
||||
secondEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test(`Chamfer point-and-click`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
// Code samples
|
||||
const initialCode = `sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-12, -6], %)
|
||||
|> line([0, 12], %)
|
||||
|> line([24, 0], %)
|
||||
|> line([0, -12], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
extrude001 = extrude(-12, sketch001)
|
||||
`
|
||||
const firstChamferDeclaration = 'chamfer({ length = 5, tags = [seg01] }, %)'
|
||||
const secondChamferDeclaration =
|
||||
'chamfer({ length = 5, tags = [getOppositeEdge(seg01)] }, %)'
|
||||
|
||||
// Locators
|
||||
const firstEdgeLocation = { x: 600, y: 193 }
|
||||
const secondEdgeLocation = { x: 600, y: 383 }
|
||||
const bodyLocation = { x: 630, y: 290 }
|
||||
const [clickOnFirstEdge] = scene.makeMouseHelpers(
|
||||
firstEdgeLocation.x,
|
||||
firstEdgeLocation.y
|
||||
)
|
||||
const [clickOnSecondEdge] = scene.makeMouseHelpers(
|
||||
secondEdgeLocation.x,
|
||||
secondEdgeLocation.y
|
||||
)
|
||||
|
||||
// Colors
|
||||
const edgeColorWhite: [number, number, number] = [248, 248, 248]
|
||||
const edgeColorYellow: [number, number, number] = [251, 251, 40] // Mac:B=67 Ubuntu:B=12
|
||||
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,
|
||||
secondEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
|
||||
// wait for stream to load
|
||||
await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
|
||||
})
|
||||
|
||||
// Test 1: Command bar flow with preselected edges
|
||||
await test.step(`Select first edge`, async () => {
|
||||
await scene.expectPixelColor(
|
||||
edgeColorWhite,
|
||||
firstEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
await clickOnFirstEdge()
|
||||
await scene.expectPixelColor(
|
||||
edgeColorYellow,
|
||||
firstEdgeLocation,
|
||||
highTolerance // Ubuntu color mismatch can require high tolerance
|
||||
)
|
||||
})
|
||||
|
||||
await test.step(`Apply chamfer to the preselected edge`, async () => {
|
||||
await page.waitForTimeout(100)
|
||||
await toolbar.chamferButton.click()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Chamfer',
|
||||
highlightedHeaderArg: 'selection',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Length: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Chamfer',
|
||||
highlightedHeaderArg: 'length',
|
||||
currentArgKey: 'length',
|
||||
currentArgValue: '5',
|
||||
headerArguments: {
|
||||
Selection: '1 face',
|
||||
Length: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Chamfer',
|
||||
headerArguments: {
|
||||
Selection: '1 face',
|
||||
Length: '5',
|
||||
},
|
||||
stage: 'review',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor`, async () => {
|
||||
await editor.expectEditor.toContain(firstChamferDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: ['|>chamfer({length=5,tags=[seg01]},%)'],
|
||||
highlightedCode: '',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step(`Confirm scene has changed`, async () => {
|
||||
await scene.expectPixelColor(chamferColor, firstEdgeLocation, lowTolerance)
|
||||
})
|
||||
|
||||
// Test 2: Command bar flow without preselected edges
|
||||
await test.step(`Open chamfer UI without selecting edges`, async () => {
|
||||
await page.waitForTimeout(100)
|
||||
await toolbar.chamferButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Length: '',
|
||||
},
|
||||
highlightedHeaderArg: 'selection',
|
||||
commandName: 'Chamfer',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step(`Select second edge`, async () => {
|
||||
await scene.expectPixelColor(
|
||||
edgeColorWhite,
|
||||
secondEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
await clickOnSecondEdge()
|
||||
await scene.expectPixelColor(
|
||||
edgeColorYellow,
|
||||
secondEdgeLocation,
|
||||
highTolerance // Ubuntu color mismatch can require high tolerance
|
||||
)
|
||||
})
|
||||
|
||||
await test.step(`Apply chamfer to the second edge`, async () => {
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Chamfer',
|
||||
highlightedHeaderArg: 'selection',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Length: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Chamfer',
|
||||
highlightedHeaderArg: 'length',
|
||||
currentArgKey: 'length',
|
||||
currentArgValue: '5',
|
||||
headerArguments: {
|
||||
Selection: '1 sweepEdge',
|
||||
Length: '',
|
||||
},
|
||||
stage: 'arguments',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Chamfer',
|
||||
headerArguments: {
|
||||
Selection: '1 sweepEdge',
|
||||
Length: '5',
|
||||
},
|
||||
stage: 'review',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor`, async () => {
|
||||
await editor.expectEditor.toContain(secondChamferDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: ['length=5,'],
|
||||
highlightedCode: '',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step(`Confirm scene has changed`, async () => {
|
||||
await scene.expectPixelColor(
|
||||
backgroundColor,
|
||||
secondEdgeLocation,
|
||||
lowTolerance
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -907,6 +1566,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
|
||||
await clickOnCap()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
@ -927,6 +1587,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
|
||||
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
|
||||
await toolbar.shellButton.click()
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
@ -1008,6 +1669,7 @@ extrude001 = extrude(40, sketch001)
|
||||
await page.waitForTimeout(500)
|
||||
await page.keyboard.up('Shift')
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
@ -1030,4 +1692,162 @@ extrude001 = extrude(40, sketch001)
|
||||
})
|
||||
await scene.expectPixelColor([49, 49, 49], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step('Delete shell via feature tree selection', async () => {
|
||||
await editor.closePane()
|
||||
const operationButton = await toolbar.getFeatureTreeOperation('Shell', 0)
|
||||
await operationButton.click({ button: 'left' })
|
||||
await page.keyboard.press('Backspace')
|
||||
await scene.expectPixelColor([99, 99, 99], testPoint, 15)
|
||||
})
|
||||
})
|
||||
|
||||
const shellSketchOnFacesCases = [
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 100 }, %)
|
||||
|> extrude(100, %)
|
||||
|
||||
sketch002 = startSketchOn(sketch001, 'END')
|
||||
|> circle({ center = [0, 0], radius = 50 }, %)
|
||||
|> extrude(50, %)
|
||||
`,
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
|> circle({ center = [0, 0], radius = 100 }, %)
|
||||
extrude001 = extrude(100, sketch001)
|
||||
|
||||
sketch002 = startSketchOn(extrude001, 'END')
|
||||
|> circle({ center = [0, 0], radius = 50 }, %)
|
||||
extrude002 = extrude(50, sketch002)
|
||||
`,
|
||||
]
|
||||
shellSketchOnFacesCases.forEach((initialCode, index) => {
|
||||
const hasExtrudesInPipe = index === 0
|
||||
test(`Shell point-and-click sketch on face (extrudes in pipes: ${hasExtrudesInPipe})`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 550, y: 295 }
|
||||
const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
const shellDeclaration = `shell001 = shell({ faces = ['end'], thickness = 5 }, ${
|
||||
hasExtrudesInPipe ? 'sketch002' : 'extrude002'
|
||||
})`
|
||||
|
||||
await test.step(`Look for the grey of the shape`, async () => {
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor([128, 128, 128], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step(`Go through the command bar flow, selecting a cap and keeping default thickness`, async () => {
|
||||
await toolbar.shellButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Thickness: '',
|
||||
},
|
||||
highlightedHeaderArg: 'selection',
|
||||
commandName: 'Shell',
|
||||
})
|
||||
await clickOnCap()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
Selection: '1 cap',
|
||||
Thickness: '5',
|
||||
},
|
||||
commandName: 'Shell',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||
await toolbar.openPane('code')
|
||||
await editor.expectEditor.toContain(shellDeclaration)
|
||||
await editor.expectState({
|
||||
diagnostics: [],
|
||||
activeLines: [shellDeclaration],
|
||||
highlightedCode: '',
|
||||
})
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor([73, 73, 73], testPoint, 15)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Shell dry-run validation rejects sweeps`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn('YZ')
|
||||
|> circle({
|
||||
center = [0, 0],
|
||||
radius = 500
|
||||
}, %)
|
||||
sketch002 = startSketchOn('XZ')
|
||||
|> startProfileAt([0, 0], %)
|
||||
|> xLine(-2000, %)
|
||||
sweep001 = sweep({ path = sketch002 }, sketch001)
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 500, y: 250 }
|
||||
const [clickOnSweep] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
|
||||
await test.step(`Confirm sweep exists`, async () => {
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor([231, 231, 231], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step(`Go through the Shell flow and fail validation with a toast`, async () => {
|
||||
await toolbar.shellButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Thickness: '',
|
||||
},
|
||||
highlightedHeaderArg: 'selection',
|
||||
commandName: 'Shell',
|
||||
})
|
||||
await clickOnSweep()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(
|
||||
page.getByText('Unable to shell with the provided selection')
|
||||
).toBeVisible()
|
||||
await page.waitForTimeout(1000)
|
||||
})
|
||||
})
|
||||
|
@ -115,7 +115,7 @@ test(
|
||||
)
|
||||
|
||||
test(
|
||||
'yyyyyyyyy open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
|
||||
'open a file in a project works and renders, open another file in different project with errors, it should clear the scene',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
@ -172,7 +172,7 @@ test(
|
||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
||||
await expect(page.getByText('broken-code')).toBeVisible()
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
await expect(page.getByText('New Project')).toBeVisible()
|
||||
await expect(page.getByText('Create project')).toBeVisible()
|
||||
})
|
||||
await test.step('opening broken code project should clear the scene and show the error', async () => {
|
||||
// Go back home.
|
||||
@ -199,7 +199,7 @@ test(
|
||||
)
|
||||
|
||||
test(
|
||||
'aaayyyyyyyy open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
|
||||
'open a file in a project works and renders, open another file in different project that is empty, it should clear the scene',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
@ -253,7 +253,7 @@ test(
|
||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
||||
await expect(page.getByText('empty')).toBeVisible()
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
await expect(page.getByText('New Project')).toBeVisible()
|
||||
await expect(page.getByText('Create project')).toBeVisible()
|
||||
})
|
||||
await test.step('opening empty code project should clear the scene', async () => {
|
||||
// Go back home.
|
||||
@ -276,7 +276,7 @@ test(
|
||||
)
|
||||
|
||||
test(
|
||||
'nooooooooooooo open a file in a project works and renders, open empty file, it should clear the scene',
|
||||
'open a file in a project works and renders, open empty file, it should clear the scene',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
@ -985,6 +985,126 @@ test.describe(`Project management commands`, () => {
|
||||
})
|
||||
}
|
||||
)
|
||||
test(`Create a new project with a colliding name`, async ({
|
||||
context,
|
||||
homePage,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const projectName = 'test-project'
|
||||
await test.step(`Setup`, async () => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const projectDir = path.join(dir, projectName)
|
||||
await Promise.all([fsp.mkdir(projectDir, { recursive: true })])
|
||||
await Promise.all([
|
||||
fsp.copyFile(
|
||||
executorInputPath('router-template-slate.kcl'),
|
||||
path.join(projectDir, 'main.kcl')
|
||||
),
|
||||
])
|
||||
})
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
title: projectName,
|
||||
fileCount: 1,
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Create a new project with the same name', async () => {
|
||||
await cmdBar.openCmdBar()
|
||||
await cmdBar.chooseCommand('create project')
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Create project',
|
||||
currentArgKey: 'name',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Name: '',
|
||||
},
|
||||
highlightedHeaderArg: 'name',
|
||||
})
|
||||
await cmdBar.argumentInput.fill(projectName)
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Check the project was created with a non-colliding name`, async () => {
|
||||
await toolbar.logoLink.click()
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
title: projectName + '-1',
|
||||
fileCount: 1,
|
||||
},
|
||||
{
|
||||
title: projectName,
|
||||
fileCount: 1,
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Create another project with the same name', async () => {
|
||||
await cmdBar.openCmdBar()
|
||||
await cmdBar.chooseCommand('create project')
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Create project',
|
||||
currentArgKey: 'name',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Name: '',
|
||||
},
|
||||
highlightedHeaderArg: 'name',
|
||||
})
|
||||
await cmdBar.argumentInput.fill(projectName)
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
|
||||
await test.step(`Check the second project was created with a non-colliding name`, async () => {
|
||||
await toolbar.logoLink.click()
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
title: projectName + '-2',
|
||||
fileCount: 1,
|
||||
},
|
||||
{
|
||||
title: projectName + '-1',
|
||||
fileCount: 1,
|
||||
},
|
||||
{
|
||||
title: projectName,
|
||||
fileCount: 1,
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test(`Create a few projects using the default project name`, async ({
|
||||
homePage,
|
||||
toolbar,
|
||||
}) => {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
await test.step(`Create project ${i}`, async () => {
|
||||
await homePage.expectState({
|
||||
projectCards: Array.from({ length: i }, (_, i) => ({
|
||||
title: `project-${i.toString().padStart(3, '0')}`,
|
||||
fileCount: 1,
|
||||
})).toReversed(),
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
await homePage.createAndGoToProject()
|
||||
await toolbar.logoLink.click()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
test(
|
||||
@ -1391,7 +1511,7 @@ extrude001 = extrude(200, sketch001)`)
|
||||
await page.getByTestId('app-logo').click()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'New project' })
|
||||
page.getByRole('button', { name: 'Create project' })
|
||||
).toBeVisible()
|
||||
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
@ -1405,7 +1525,7 @@ extrude001 = extrude(200, sketch001)`)
|
||||
test(
|
||||
'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
async ({ context, page, cmdBar, homePage }, testInfo) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
await Promise.all([
|
||||
fsp.mkdir(path.join(dir, 'router-template-slate'), { recursive: true }),
|
||||
@ -1443,11 +1563,38 @@ test(
|
||||
|
||||
const pointOnModel = { x: 630, y: 280 }
|
||||
|
||||
await test.step('Opening the bracket project should load the stream', async () => {
|
||||
// expect to see the text bracket
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
await test.step('Opening the bracket project via command palette should load the stream', async () => {
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
title: 'bracket',
|
||||
fileCount: 1,
|
||||
},
|
||||
{
|
||||
title: 'router-template-slate',
|
||||
fileCount: 1,
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
|
||||
await page.getByText('bracket').click()
|
||||
await cmdBar.openCmdBar()
|
||||
await cmdBar.chooseCommand('open project')
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Open project',
|
||||
currentArgKey: 'name',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Name: '',
|
||||
},
|
||||
highlightedHeaderArg: 'name',
|
||||
})
|
||||
await cmdBar.argumentInput.fill('brac')
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'commandBarClosed',
|
||||
})
|
||||
|
||||
await u.waitForPageLoad()
|
||||
|
||||
@ -1465,10 +1612,10 @@ test(
|
||||
|
||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
||||
await expect(page.getByText('New Project')).toBeVisible()
|
||||
await expect(page.getByText('Create project')).toBeVisible()
|
||||
})
|
||||
|
||||
await test.step('Opening the router-template project should load the stream', async () => {
|
||||
await test.step('Opening the router-template project via link should load the stream', async () => {
|
||||
// expect to see the text bracket
|
||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
||||
|
||||
@ -1485,16 +1632,26 @@ test(
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
|
||||
await test.step('Opening the router-template project should load the stream', async () => {
|
||||
await test.step('The projects on the home page should still be normal', async () => {
|
||||
await page.getByTestId('project-sidebar-toggle').click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Go to Home' })
|
||||
).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Go to Home' }).click()
|
||||
|
||||
await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible()
|
||||
await expect(page.getByText('router-template-slate')).toBeVisible()
|
||||
await expect(page.getByText('New Project')).toBeVisible()
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{
|
||||
title: 'bracket',
|
||||
fileCount: 1,
|
||||
},
|
||||
{
|
||||
title: 'router-template-slate',
|
||||
fileCount: 1,
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
@ -1885,3 +2042,48 @@ test.fixme(
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'project name with foreign characters should open',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const bracketDir = path.join(dir, 'اَلْعَرَبِيَّةُ')
|
||||
await fsp.mkdir(bracketDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
||||
path.join(bracketDir, 'main.kcl')
|
||||
)
|
||||
|
||||
await fsp.writeFile(path.join(bracketDir, 'empty.kcl'), '')
|
||||
})
|
||||
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
const u = await getUtils(page)
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
const pointOnModel = { x: 630, y: 280 }
|
||||
|
||||
await test.step('Opening the اَلْعَرَبِيَّةُ project should load the stream', async () => {
|
||||
// expect to see the text bracket
|
||||
await expect(page.getByText('اَلْعَرَبِيَّةُ')).toBeVisible()
|
||||
|
||||
await page.getByText('اَلْعَرَبِيَّةُ').click()
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Start Sketch' })
|
||||
).toBeEnabled({
|
||||
timeout: 20_000,
|
||||
})
|
||||
|
||||
// gray at this pixel means the stream has loaded in the most
|
||||
// user way we can verify it (pixel color)
|
||||
await expect
|
||||
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeLessThan(15)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -614,6 +614,38 @@ extrude001 = extrude(50, sketch001)
|
||||
await expect(gizmo).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test(`Refreshing the app doesn't cause the stream to pause on long-executing files`, async ({
|
||||
context,
|
||||
homePage,
|
||||
scene,
|
||||
toolbar,
|
||||
viewport,
|
||||
}) => {
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const legoDir = path.join(dir, 'lego')
|
||||
await fsp.mkdir(legoDir, { recursive: true })
|
||||
await fsp.copyFile(
|
||||
executorInputPath('lego.kcl'),
|
||||
path.join(legoDir, 'main.kcl')
|
||||
)
|
||||
})
|
||||
|
||||
await test.step(`Test setup`, async () => {
|
||||
await homePage.openProject('lego')
|
||||
await toolbar.closePane('code')
|
||||
})
|
||||
await test.step(`Waiting for the loading spinner to disappear`, async () => {
|
||||
await scene.loadingIndicator.waitFor({ state: 'detached' })
|
||||
})
|
||||
await test.step(`The part should start loading quickly, not waiting until execution is complete`, async () => {
|
||||
await scene.expectPixelColor(
|
||||
[143, 143, 143],
|
||||
{ x: (viewport?.width ?? 1200) / 2, y: (viewport?.height ?? 500) / 2 },
|
||||
15
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
async function clickExportButton(page: Page) {
|
||||
|
@ -34,15 +34,15 @@ test.describe('Sketch tests', () => {
|
||||
screwRadius = 3
|
||||
wireRadius = 2
|
||||
wireOffset = 0.5
|
||||
|
||||
|
||||
screwHole = startSketchOn('XY')
|
||||
${startProfileAt1}
|
||||
|> arc({
|
||||
radius = screwRadius,
|
||||
angle_start = 0,
|
||||
angle_end = 360
|
||||
angleStart = 0,
|
||||
angleEnd = 360
|
||||
}, %)
|
||||
|
||||
|
||||
part001 = startSketchOn('XY')
|
||||
${startProfileAt2}
|
||||
|> xLine(width * .5, %)
|
||||
@ -51,7 +51,7 @@ test.describe('Sketch tests', () => {
|
||||
|> close(%)
|
||||
|> hole(screwHole, %)
|
||||
|> extrude(thickness, %)
|
||||
|
||||
|
||||
part002 = startSketchOn('-XZ')
|
||||
${startProfileAt3}
|
||||
|> xLine(width / 4, %)
|
||||
@ -60,8 +60,8 @@ test.describe('Sketch tests', () => {
|
||||
|> yLine(wireOffset, %)
|
||||
|> arc({
|
||||
radius = wireRadius,
|
||||
angle_start = 0,
|
||||
angle_end = 180
|
||||
angleStart = 0,
|
||||
angleEnd = 180
|
||||
}, %)
|
||||
|> yLine(-wireOffset, %)
|
||||
|> xLine(-width / 4, %)
|
||||
@ -99,6 +99,7 @@ test.describe('Sketch tests', () => {
|
||||
test('Can delete most of a sketch and the line tool will still work', async ({
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
@ -112,12 +113,13 @@ test.describe('Sketch tests', () => {
|
||||
})
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
await expect(async () => {
|
||||
await page.getByText('tangentialArcTo([24.95, -5.38], %)').click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Edit Sketch' })
|
||||
).toBeEnabled({ timeout: 1000 })
|
||||
).toBeEnabled({ timeout: 2000 })
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
}).toPass({ timeout: 40_000, intervals: [1_000] })
|
||||
|
||||
@ -1063,7 +1065,7 @@ test.describe('Sketch tests', () => {
|
||||
`lugHeadLength = 0.25
|
||||
lugDiameter = 0.5
|
||||
lugLength = 2
|
||||
|
||||
|
||||
fn lug = (origin, length, diameter, plane) => {
|
||||
lugSketch = startSketchOn(plane)
|
||||
|> startProfileAt([origin[0] + lugDiameter / 2, origin[1]], %)
|
||||
@ -1072,10 +1074,10 @@ test.describe('Sketch tests', () => {
|
||||
|> yLineTo(0, %)
|
||||
|> close(%)
|
||||
|> revolve({ axis = "Y" }, %)
|
||||
|
||||
|
||||
return lugSketch
|
||||
}
|
||||
|
||||
|
||||
lug([0, 0], 10, .5, "XY")`
|
||||
)
|
||||
})
|
||||
@ -1127,14 +1129,14 @@ test.describe('Sketch tests', () => {
|
||||
`fn in2mm = (inches) => {
|
||||
return inches * 25.4
|
||||
}
|
||||
|
||||
|
||||
const railTop = in2mm(.748)
|
||||
const railSide = in2mm(.024)
|
||||
const railBaseWidth = in2mm(.612)
|
||||
const railWideWidth = in2mm(.835)
|
||||
const railBaseLength = in2mm(.200)
|
||||
const railClampable = in2mm(.200)
|
||||
|
||||
|
||||
const rail = startSketchOn('XZ')
|
||||
|> startProfileAt([
|
||||
-railTop / 2,
|
||||
@ -1405,3 +1407,46 @@ test.describe(`Click based selection don't brick the app when clicked out of ran
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Regression test for https://github.com/KittyCAD/modeling-app/issues/4372
|
||||
test.describe('Redirecting to home page and back to the original file should clear sketch DOM elements', () => {
|
||||
test('Can redirect to home page and back to original file and have a cleared DOM', async ({
|
||||
context,
|
||||
page,
|
||||
scene,
|
||||
toolbar,
|
||||
editor,
|
||||
homePage,
|
||||
}) => {
|
||||
// We seed the scene with a single offset plane
|
||||
await context.addInitScript(() => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
` sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([256.85, 14.41], %)
|
||||
|> lineTo([0, 211.07], %)
|
||||
`
|
||||
)
|
||||
})
|
||||
await homePage.goToModelingScene()
|
||||
await scene.waitForExecutionDone()
|
||||
|
||||
const [objClick] = scene.makeMouseHelpers(634, 274)
|
||||
await objClick()
|
||||
|
||||
// Enter sketch mode
|
||||
await toolbar.editSketch()
|
||||
|
||||
await expect(page.getByText('323.49')).toBeVisible()
|
||||
|
||||
// Open navigation side bar
|
||||
await page.getByTestId('project-sidebar-toggle').click()
|
||||
const goToHome = page.getByRole('button', {
|
||||
name: 'Go to Home',
|
||||
})
|
||||
|
||||
await goToHome.click()
|
||||
await homePage.openProject('testDefault')
|
||||
await expect(page.getByText('323.49')).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
|
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: 59 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 145 KiB |
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 129 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
@ -1078,7 +1078,7 @@ export async function createProject({
|
||||
returnHome?: boolean
|
||||
}) {
|
||||
await test.step(`Create project and navigate to it`, async () => {
|
||||
await page.getByRole('button', { name: 'New project' }).click()
|
||||
await page.getByRole('button', { name: 'Create project' }).click()
|
||||
await page.getByRole('textbox', { name: 'Name' }).fill(name)
|
||||
await page.getByRole('button', { name: 'Continue' }).click()
|
||||
|
||||
|
@ -8,8 +8,8 @@ import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
|
||||
|
||||
test.describe('Testing in-app sample loading', () => {
|
||||
/**
|
||||
* Note this test implicitly depends on the KCL sample "car-wheel.kcl",
|
||||
* its title, and its units settings. https://github.com/KittyCAD/kcl-samples/blob/main/car-wheel/car-wheel.kcl
|
||||
* Note this test implicitly depends on the KCL sample "a-parametric-bearing-pillow-block",
|
||||
* its title, and its units settings. https://github.com/KittyCAD/kcl-samples/blob/main/a-parametric-bearing-pillow-block/main.kcl
|
||||
*/
|
||||
test('Web: should overwrite current code, cannot create new file', async ({
|
||||
editor,
|
||||
@ -29,8 +29,8 @@ test.describe('Testing in-app sample loading', () => {
|
||||
|
||||
// Locators and constants
|
||||
const newSample = {
|
||||
file: 'car-wheel' + FILE_EXT,
|
||||
title: 'Car Wheel',
|
||||
file: 'a-parametric-bearing-pillow-block' + FILE_EXT,
|
||||
title: 'A Parametric Bearing Pillow Block',
|
||||
}
|
||||
const commandBarButton = page.getByRole('button', { name: 'Commands' })
|
||||
const samplesCommandOption = page.getByRole('option', {
|
||||
@ -75,8 +75,8 @@ test.describe('Testing in-app sample loading', () => {
|
||||
|
||||
/**
|
||||
* Note this test implicitly depends on the KCL samples:
|
||||
* "car-wheel.kcl": https://github.com/KittyCAD/kcl-samples/blob/main/car-wheel/car-wheel.kcl
|
||||
* "gear-rack.kcl": https://github.com/KittyCAD/kcl-samples/blob/main/gear-rack/gear-rack.kcl
|
||||
* "a-parametric-bearing-pillow-block": https://github.com/KittyCAD/kcl-samples/blob/main/a-parametric-bearing-pillow-block/main.kcl
|
||||
* "gear-rack": https://github.com/KittyCAD/kcl-samples/blob/main/gear-rack/main.kcl
|
||||
*/
|
||||
test(
|
||||
'Desktop: should create new file by default, optionally overwrite',
|
||||
@ -93,8 +93,8 @@ test.describe('Testing in-app sample loading', () => {
|
||||
|
||||
// Locators and constants
|
||||
const sampleOne = {
|
||||
file: 'car-wheel' + FILE_EXT,
|
||||
title: 'Car Wheel',
|
||||
file: 'a-parametric-bearing-pillow-block' + FILE_EXT,
|
||||
title: 'A Parametric Bearing Pillow Block',
|
||||
}
|
||||
const sampleTwo = {
|
||||
file: 'gear-rack' + FILE_EXT,
|
||||
|
@ -389,25 +389,25 @@ test.describe('Testing selections', () => {
|
||||
await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({
|
||||
plane = {
|
||||
origin = { x = 0, y = -50, z = 0 },
|
||||
x_axis = { x = 1, y = 0, z = 0 },
|
||||
y_axis = { x = 0, y = 0, z = 1 },
|
||||
z_axis = { x = 0, y = -1, z = 0 }
|
||||
xAxis = { x = 1, y = 0, z = 0 },
|
||||
yAxis = { x = 0, y = 0, z = 1 },
|
||||
zAxis = { x = 0, y = -1, z = 0 }
|
||||
}
|
||||
})`)
|
||||
await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({
|
||||
plane = {
|
||||
origin = { x = 116.53, y = 0, z = 163.25 },
|
||||
x_axis = { x = -0.81, y = 0, z = 0.58 },
|
||||
y_axis = { x = 0, y = -1, z = 0 },
|
||||
z_axis = { x = 0.58, y = 0, z = 0.81 }
|
||||
xAxis = { x = -0.81, y = 0, z = 0.58 },
|
||||
yAxis = { x = 0, y = -1, z = 0 },
|
||||
zAxis = { x = 0.58, y = 0, z = 0.81 }
|
||||
}
|
||||
})`)
|
||||
await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({
|
||||
plane = {
|
||||
origin = { x = -91.74, y = 0, z = 80.89 },
|
||||
x_axis = { x = -0.66, y = 0, z = -0.75 },
|
||||
y_axis = { x = 0, y = -1, z = 0 },
|
||||
z_axis = { x = -0.75, y = 0, z = 0.66 }
|
||||
xAxis = { x = -0.66, y = 0, z = -0.75 },
|
||||
yAxis = { x = 0, y = -1, z = 0 },
|
||||
zAxis = { x = -0.75, y = 0, z = 0.66 }
|
||||
}
|
||||
})`)
|
||||
|
||||
@ -906,53 +906,6 @@ test.describe('Testing selections', () => {
|
||||
).not.toBeDisabled()
|
||||
})
|
||||
|
||||
test('Fillet button states test', async ({ page, homePage }) => {
|
||||
const u = await getUtils(page)
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([-5, -5], %)
|
||||
|> line([0, 10], %)
|
||||
|> line([10, 0], %)
|
||||
|> line([0, -10], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)`
|
||||
)
|
||||
})
|
||||
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await u.openDebugPanel()
|
||||
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||
await u.closeDebugPanel()
|
||||
|
||||
const selectSegment = () => page.getByText(`line([10, 0], %)`).click()
|
||||
const selectClose = () => page.getByText(`close(%)`).click()
|
||||
const clickEmpty = () => page.mouse.click(950, 100)
|
||||
|
||||
// Now that we don't disable toolbar buttons based on selection,
|
||||
// but rather based on a "selection" step in the command palette,
|
||||
// the fillet button should always be enabled with a good network connection.
|
||||
// I'm not sure if this test is actually useful anymore.
|
||||
await selectSegment()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||
await clickEmpty()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||
|
||||
// test fillet button with the body in the scene
|
||||
const codeToAdd = `${await u.codeLocator.allInnerTexts()}
|
||||
extrude001 = extrude(10, sketch001)`
|
||||
await u.codeLocator.clear()
|
||||
await u.codeLocator.fill(codeToAdd)
|
||||
await selectSegment()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||
await selectClose()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||
await clickEmpty()
|
||||
await expect(page.getByRole('button', { name: 'Fillet' })).toBeEnabled()
|
||||
})
|
||||
|
||||
const removeAfterFirstParenthesis = (inputString: string) => {
|
||||
const index = inputString.indexOf('(')
|
||||
if (index !== -1) {
|
||||
|
18
flake.lock
generated
@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1721933792,
|
||||
"narHash": "sha256-zYVwABlQnxpbaHMfX6Wt9jhyQstFYwN2XjleOJV3VVg=",
|
||||
"lastModified": 1736320768,
|
||||
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2122a9b35b35719ad9a395fe783eabb092df01b1",
|
||||
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -18,11 +18,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1718428119,
|
||||
"narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=",
|
||||
"lastModified": 1728538411,
|
||||
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5",
|
||||
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -43,11 +43,11 @@
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1721960387,
|
||||
"narHash": "sha256-o21ax+745ETGXrcgc/yUuLw1SI77ymp3xEpJt+w/kks=",
|
||||
"lastModified": 1736476219,
|
||||
"narHash": "sha256-+qyv3QqdZCdZ3cSO/cbpEY6tntyYjfe1bB12mdpNFaY=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "9cbf831c5b20a53354fc12758abd05966f9f1699",
|
||||
"rev": "de30cc5963da22e9742bbbbb9a3344570ed237b9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
1
interface.d.ts
vendored
@ -93,5 +93,6 @@ export interface IElectronAPI {
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: IElectronAPI
|
||||
openExternalLink: (e: React.MouseEvent<HTMLAnchorElement>) => void
|
||||
}
|
||||
}
|
||||
|
43
package.json
@ -15,7 +15,7 @@
|
||||
"@codemirror/autocomplete": "^6.17.0",
|
||||
"@codemirror/commands": "^6.6.0",
|
||||
"@codemirror/language": "^6.10.3",
|
||||
"@codemirror/lint": "^6.8.1",
|
||||
"@codemirror/lint": "^6.8.4",
|
||||
"@codemirror/search": "^6.5.6",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
@ -26,7 +26,7 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@kittycad/lib": "2.0.12",
|
||||
"@kittycad/lib": "2.0.13",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.1",
|
||||
"@react-hook/resize-observer": "^2.0.1",
|
||||
@ -52,20 +52,20 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-hotkeys-hook": "^4.5.1",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-modal": "^3.16.1",
|
||||
"react-modal": "^3.16.3",
|
||||
"react-modal-promise": "^1.0.2",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"sketch-helpers": "^0.0.4",
|
||||
"three": "^0.166.1",
|
||||
"three": "^0.172.0",
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"uuid": "^11.0.2",
|
||||
"vscode-jsonrpc": "^8.2.1",
|
||||
"vscode-languageserver-protocol": "^3.17.5",
|
||||
"vscode-uri": "^3.0.8",
|
||||
"web-vitals": "^3.5.2",
|
||||
"xstate": "^5.17.4",
|
||||
"xstate": "^5.19.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
@ -91,8 +91,8 @@
|
||||
"build:wasm": "yarn wasm-prep && cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||
"wasm-prep": "rimraf src/wasm-lib/pkg && mkdirp src/wasm-lib/pkg && rimraf src/wasm-lib/kcl/bindings",
|
||||
"lint-fix": "eslint --fix src e2e packages/codemirror-lsp-client",
|
||||
"lint": "eslint --max-warnings 0 src e2e packages/codemirror-lsp-client",
|
||||
"lint-fix": "eslint --fix --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src",
|
||||
"lint": "eslint --max-warnings 0 --ext .ts --ext .tsx src e2e packages/codemirror-lsp-client/src",
|
||||
"files:set-version": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json",
|
||||
"files:set-notes": "./scripts/set-files-notes.sh",
|
||||
"files:flip-to-nightly": "./scripts/flip-files-to-nightly.sh",
|
||||
@ -113,9 +113,9 @@
|
||||
"test:unit": "vitest run --mode development --exclude **/kclSamples.test.ts",
|
||||
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts",
|
||||
"test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot'",
|
||||
"test:playwright:electron:windows": "playwright test --config=playwright.electron.config.ts --grep-invert=\"@skipWin|@snapshot\"",
|
||||
"test:playwright:electron:macos": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipMacos|@snapshot'",
|
||||
"test:playwright:electron:ubuntu": "playwright test --config=playwright.electron.config.ts --grep-invert='@skipLinux|@snapshot'",
|
||||
"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'",
|
||||
@ -149,12 +149,11 @@
|
||||
"@electron-forge/plugin-vite": "7.4.0",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@lezer/generator": "^1.7.1",
|
||||
"@lezer/generator": "^1.7.2",
|
||||
"@nabla/vite-plugin-eslint": "^2.0.5",
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/electron": "^1.6.10",
|
||||
"@types/isomorphic-fetch": "^0.0.39",
|
||||
@ -166,27 +165,28 @@
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-modal": "^3.16.3",
|
||||
"@types/three": "^0.163.0",
|
||||
"@types/three": "^0.172.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"@vitest/web-worker": "^1.5.0",
|
||||
"@xstate/cli": "^0.5.17",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"d3-force": "^3.0.0",
|
||||
"electron": "32.1.2",
|
||||
"electron-builder": "24.13.3",
|
||||
"electron-notarize": "1.2.2",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-jest": "^28.10.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.3",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-suggest-no-throw": "^1.0.0",
|
||||
"happy-dom": "^15.11.7",
|
||||
"eslint-plugin-testing-library": "^7.1.1",
|
||||
"happy-dom": "^16.3.0",
|
||||
"http-server": "^14.1.1",
|
||||
"husky": "^9.1.5",
|
||||
"kill-port": "^2.0.1",
|
||||
@ -199,8 +199,9 @@
|
||||
"setimmediate": "^1.0.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.6",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.19.1",
|
||||
"vite": "^5.4.12",
|
||||
"vite-plugin-package-version": "^1.1.0",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.6.0",
|
||||
|
1
packages/codemirror-lang-kcl/.gitignore
vendored
@ -4,4 +4,5 @@ dist
|
||||
tsconfig.tsbuildinfo
|
||||
*.d.ts
|
||||
*.js
|
||||
!postcss.config.js
|
||||
!rollup.config.js
|
||||
|
@ -28,6 +28,7 @@
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"rollup": "^4.29.1",
|
||||
"rollup-plugin-dts": "^6.1.1",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^2.1.8"
|
||||
},
|
||||
"files": [
|
||||
|
1
packages/codemirror-lang-kcl/postcss.config.js
Normal file
@ -0,0 +1 @@
|
||||
// This is here to prevent using the one in the root of the project.
|
@ -85,7 +85,7 @@ commaSep1NoTrailingComma<term> { term ("," term)* }
|
||||
@tokens {
|
||||
String[isolate] { "'" ("\\" _ | !['\\])* "'" | '"' ("\\" _ | !["\\])* '"' }
|
||||
|
||||
Number { "." @digit+ | @digit+ ("." @digit*)? }
|
||||
Number { "." @digit+ | @digit+ ("." @digit+)? }
|
||||
@precedence { Number, "." }
|
||||
|
||||
AddOp { "+" | "-" }
|
||||
|
43
packages/codemirror-lang-kcl/test/range.txt
Normal file
@ -0,0 +1,43 @@
|
||||
# spaced
|
||||
|
||||
a = [0 .. 1]
|
||||
|
||||
==>
|
||||
Program(VariableDeclaration(VariableDefinition,
|
||||
Equals,
|
||||
ArrayExpression(IntegerRange(Number,
|
||||
Number))))
|
||||
|
||||
# compact
|
||||
|
||||
a = [0..1]
|
||||
|
||||
==>
|
||||
Program(VariableDeclaration(VariableDefinition,
|
||||
Equals,
|
||||
ArrayExpression(IntegerRange(Number,
|
||||
Number))))
|
||||
|
||||
# expr spaced
|
||||
|
||||
a = [start .. start + 10]
|
||||
|
||||
==>
|
||||
Program(VariableDeclaration(VariableDefinition,
|
||||
Equals,
|
||||
ArrayExpression(IntegerRange(VariableName,
|
||||
BinaryExpression(VariableName,
|
||||
AddOp,
|
||||
Number)))))
|
||||
|
||||
# expr compact
|
||||
|
||||
a = [start..start + 10]
|
||||
|
||||
==>
|
||||
Program(VariableDeclaration(VariableDefinition,
|
||||
Equals,
|
||||
ArrayExpression(IntegerRange(VariableName,
|
||||
BinaryExpression(VariableName,
|
||||
AddOp,
|
||||
Number)))))
|
@ -398,7 +398,7 @@ check-error@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc"
|
||||
integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==
|
||||
|
||||
debug@^4.3.7:
|
||||
debug@^4.1.1, debug@^4.3.7:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
|
||||
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
|
||||
@ -471,6 +471,11 @@ function-bind@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
globrex@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
|
||||
integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
|
||||
|
||||
hasown@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
|
||||
@ -647,6 +652,11 @@ tinyspy@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a"
|
||||
integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==
|
||||
|
||||
tsconfck@^3.0.3:
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.4.tgz#de01a15334962e2feb526824339b51be26712229"
|
||||
integrity sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==
|
||||
|
||||
typescript@^5.7.2:
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
|
||||
@ -663,10 +673,19 @@ vite-node@2.1.8:
|
||||
pathe "^1.1.2"
|
||||
vite "^5.0.0"
|
||||
|
||||
vite-tsconfig-paths@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9"
|
||||
integrity sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
globrex "^0.1.2"
|
||||
tsconfck "^3.0.3"
|
||||
|
||||
vite@^5.0.0:
|
||||
version "5.4.11"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5"
|
||||
integrity sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==
|
||||
version "5.4.14"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.14.tgz#ff8255edb02134df180dcfca1916c37a6abe8408"
|
||||
integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==
|
||||
dependencies:
|
||||
esbuild "^0.21.3"
|
||||
postcss "^8.4.43"
|
||||
|
@ -29,7 +29,7 @@
|
||||
"vscode-uri": "^3.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.9",
|
||||
"@types/node": "^22.10.6",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ export default class StreamDemuxer extends Queue<Uint8Array> {
|
||||
// try to parse the content-length from the headers
|
||||
const length = parseInt(match[1])
|
||||
|
||||
if (isNaN(length))
|
||||
if (Number.isNaN(length))
|
||||
return Promise.reject(new Error('invalid content length'))
|
||||
|
||||
// slice the headers since we now have the content length
|
||||
|
@ -368,13 +368,20 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
sortText,
|
||||
filterText,
|
||||
}) => {
|
||||
const detailText = [
|
||||
deprecated ? 'Deprecated' : undefined,
|
||||
labelDetails ? labelDetails.detail : detail,
|
||||
]
|
||||
// Don't let undefined appear.
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
const completion: Completion & {
|
||||
filterText: string
|
||||
sortText?: string
|
||||
apply: string
|
||||
} = {
|
||||
label,
|
||||
detail: labelDetails ? labelDetails.detail : detail,
|
||||
detail: detailText,
|
||||
apply: label,
|
||||
type: kind && CompletionItemKindMap[kind].toLowerCase(),
|
||||
sortText: sortText ?? label,
|
||||
@ -382,7 +389,11 @@ export class LanguageServerPlugin implements PluginValue {
|
||||
}
|
||||
if (documentation) {
|
||||
completion.info = () => {
|
||||
const htmlString = formatMarkdownContents(documentation)
|
||||
const deprecatedHtml = deprecated
|
||||
? '<p><strong>Deprecated</strong></p>'
|
||||
: ''
|
||||
const htmlString =
|
||||
deprecatedHtml + formatMarkdownContents(documentation)
|
||||
const htmlNode = document.createElement('div')
|
||||
htmlNode.style.display = 'contents'
|
||||
htmlNode.innerHTML = htmlString
|
||||
|
@ -109,12 +109,12 @@
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
|
||||
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
|
||||
|
||||
"@types/node@^20.14.9":
|
||||
version "20.14.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.9.tgz#12e8e765ab27f8c421a1820c99f5f313a933b420"
|
||||
integrity sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==
|
||||
"@types/node@^22.10.6":
|
||||
version "22.10.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.6.tgz#5c6795e71635876039f853cbccd59f523d9e4239"
|
||||
integrity sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
undici-types "~6.20.0"
|
||||
|
||||
acorn-walk@^8.1.1:
|
||||
version "8.3.3"
|
||||
@ -187,10 +187,10 @@ typescript@^5.7.2:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.2.tgz#3169cf8c4c8a828cde53ba9ecb3d2b1d5dd67be6"
|
||||
integrity sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
undici-types@~6.20.0:
|
||||
version "6.20.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
|
||||
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
|
||||
|
||||
v8-compile-cache-lib@^3.0.1:
|
||||
version "3.0.1"
|
||||
|
@ -32,10 +32,9 @@ export default defineConfig({
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'Google Chrome',
|
||||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
channel: 'chrome',
|
||||
contextOptions: {
|
||||
/* Chromium is the only one with these permission types */
|
||||
permissions: ['clipboard-write', 'clipboard-read'],
|
||||
|
@ -1,172 +1,212 @@
|
||||
[
|
||||
{
|
||||
"file": "80-20-rail.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "80-20-rail/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "80/20 Rail",
|
||||
"description": "An 80/20 extruded aluminum linear rail. T-slot profile adjustable by profile height, rail length, and origin position"
|
||||
},
|
||||
{
|
||||
"file": "a-parametric-bearing-pillow-block.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "a-parametric-bearing-pillow-block/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "A Parametric Bearing Pillow Block",
|
||||
"description": "A bearing pillow block, also known as a plummer block or pillow block bearing, is a pedestal used to provide support for a rotating shaft with the help of compatible bearings and various accessories. Housing a bearing, the pillow block provides a secure and stable foundation that allows the shaft to rotate smoothly within its machinery setup. These components are essential in a wide range of mechanical systems and machinery, playing a key role in reducing friction and supporting radial and axial loads."
|
||||
},
|
||||
{
|
||||
"file": "ball-bearing.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "ball-bearing/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Ball Bearing",
|
||||
"description": "A ball bearing is a type of rolling-element bearing that uses balls to maintain the separation between the bearing races. The primary purpose of a ball bearing is to reduce rotational friction and support radial and axial loads."
|
||||
},
|
||||
{
|
||||
"file": "bracket.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "bracket/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Shelf Bracket",
|
||||
"description": "This is a bracket that holds a shelf. It is made of aluminum and is designed to hold a force of 300 lbs. The bracket is 6 inches wide and the force is applied at the end of the shelf, 12 inches from the wall. The bracket has a factor of safety of 1.2. The legs of the bracket are 5 inches and 2 inches long. The thickness of the bracket is calculated from the constraints provided."
|
||||
},
|
||||
{
|
||||
"file": "brake-caliper.kcl",
|
||||
"title": "Brake Caliper",
|
||||
"description": "Brake calipers are used to squeeze the brake pads against the rotor, causing larger and larger amounts of friction depending on how hard the brakes are pressed."
|
||||
},
|
||||
{
|
||||
"file": "car-wheel.kcl",
|
||||
"title": "Car Wheel",
|
||||
"description": "A sports car wheel with a circular lug pattern and spokes."
|
||||
},
|
||||
{
|
||||
"file": "car-wheel-assembly.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "car-wheel-assembly/main.kcl",
|
||||
"multipleFiles": true,
|
||||
"title": "Car Wheel Assembly",
|
||||
"description": "A car wheel assembly with a rotor, tire, and lug nuts."
|
||||
},
|
||||
{
|
||||
"file": "dodecahedron.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "dodecahedron/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Hollow Dodecahedron",
|
||||
"description": "A regular dodecahedron or pentagonal dodecahedron is a dodecahedron composed of regular pentagonal faces, three meeting at each vertex. This example shows constructing the individual faces of the dodecahedron and extruding inwards."
|
||||
},
|
||||
{
|
||||
"file": "enclosure.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "enclosure/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Enclosure",
|
||||
"description": "An enclosure body and sealing lid for storing items"
|
||||
},
|
||||
{
|
||||
"file": "flange-with-patterns.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "flange-with-patterns/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Flange",
|
||||
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
|
||||
},
|
||||
{
|
||||
"file": "flange-xy.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "flange-xy/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Flange with XY coordinates",
|
||||
"description": "A flange is a flat rim, collar, or rib, typically forged or cast, that is used to strengthen an object, guide it, or attach it to another object. Flanges are known for their use in various applications, including piping, plumbing, and mechanical engineering, among others."
|
||||
},
|
||||
{
|
||||
"file": "focusrite-scarlett-mounting-bracket.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "focusrite-scarlett-mounting-bracket/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "A mounting bracket for the Focusrite Scarlett Solo audio interface",
|
||||
"description": "This is a bracket that holds an audio device underneath a desk or shelf. The audio device has dimensions of 144mm wide, 80mm length and 45mm depth with fillets of 6mm. This mounting bracket is designed to be 3D printed with PLA material"
|
||||
},
|
||||
{
|
||||
"file": "food-service-spatula.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "food-service-spatula/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Food Service Spatula",
|
||||
"description": "Use these spatulas for mixing, flipping, and scraping."
|
||||
},
|
||||
{
|
||||
"file": "french-press.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "french-press/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "French Press",
|
||||
"description": "A french press immersion coffee maker"
|
||||
},
|
||||
{
|
||||
"file": "gear.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "gear/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Spur Gear",
|
||||
"description": "A rotating machine part having cut teeth or, in the case of a cogwheel, inserted teeth (called cogs), which mesh with another toothed part to transmit torque. Geared devices can change the speed, torque, and direction of a power source. The two elements that define a gear are its circular shape and the teeth that are integrated into its outer edge, which are designed to fit into the teeth of another gear."
|
||||
},
|
||||
{
|
||||
"file": "gear-rack.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "gear-rack/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "100mm Gear Rack",
|
||||
"description": "A flat bar or rail that is engraved with teeth along its length. These teeth are designed to mesh with the teeth of a gear, known as a pinion. When the pinion, a small cylindrical gear, rotates, its teeth engage with the teeth on the rack, causing the rack to move linearly. Conversely, linear motion applied to the rack will cause the pinion to rotate."
|
||||
},
|
||||
{
|
||||
"file": "hex-nut.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "hex-nut/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Hex nut",
|
||||
"description": "A hex nut is a type of fastener with a threaded hole and a hexagonal outer shape, used in a wide variety of applications to secure parts together. The hexagonal shape allows for a greater torque to be applied with wrenches or tools, making it one of the most common nut types in hardware."
|
||||
},
|
||||
{
|
||||
"file": "i-beam.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "i-beam/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "I-beam",
|
||||
"description": "A structural metal beam with an I shaped cross section. Often used in construction"
|
||||
},
|
||||
{
|
||||
"file": "kitt.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "kitt/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Kitt",
|
||||
"description": "The beloved KittyCAD mascot in a voxelized style."
|
||||
},
|
||||
{
|
||||
"file": "lego.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "lego/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Lego Brick",
|
||||
"description": "A standard Lego brick. This is a small, plastic construction block toy that can be interlocked with other blocks to build various structures, models, and figures. There are a lot of hacks used in this code."
|
||||
},
|
||||
{
|
||||
"file": "lug-nut.kcl",
|
||||
"title": "Lug Nut",
|
||||
"description": "lug Nuts are essential components used to create secure connections, whether for electrical purposes, like terminating wires or grounding, or for mechanical purposes, such as providing mounting points or reinforcing structural joints."
|
||||
},
|
||||
{
|
||||
"file": "mounting-plate.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "mounting-plate/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Mounting Plate",
|
||||
"description": "A flat piece of material, often metal or plastic, that serves as a support or base for attaching, securing, or mounting various types of equipment, devices, or components."
|
||||
},
|
||||
{
|
||||
"file": "multi-axis-robot.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "multi-axis-robot/main.kcl",
|
||||
"multipleFiles": true,
|
||||
"title": "Robot Arm",
|
||||
"description": "A 4 axis robotic arm for industrial use. These machines can be used for assembly, packaging, organization of goods, and quality inspection processes"
|
||||
},
|
||||
{
|
||||
"file": "pipe.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "pipe/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Pipe",
|
||||
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
|
||||
},
|
||||
{
|
||||
"file": "pipe-flange-assembly.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "pipe-flange-assembly/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Pipe and Flange Assembly",
|
||||
"description": "A crucial component in various piping systems, designed to facilitate the connection, disconnection, and access to piping for inspection, cleaning, and modifications. This assembly combines pipes (long cylindrical conduits) with flanges (plate-like fittings) to create a secure yet detachable joint."
|
||||
},
|
||||
{
|
||||
"file": "pipe-with-bend.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "pipe-with-bend/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Pipe with bend",
|
||||
"description": "A tubular section or hollow cylinder, usually but not necessarily of circular cross-section, used mainly to convey substances that can flow."
|
||||
},
|
||||
{
|
||||
"file": "poopy-shoe.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "poopy-shoe/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Poopy Shoe",
|
||||
"description": "poop shute for bambu labs printer - optimized for printing."
|
||||
},
|
||||
{
|
||||
"file": "router-template-cross-bar.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "router-template-cross-bar/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Router template for a cross bar",
|
||||
"description": "A guide for routing a notch into a cross bar."
|
||||
},
|
||||
{
|
||||
"file": "router-template-slate.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "router-template-slate/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Router template for a slate",
|
||||
"description": "A guide for routing a slate for a cross bar."
|
||||
},
|
||||
{
|
||||
"file": "sheet-metal-bracket.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "sheet-metal-bracket/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Sheet Metal Bracket",
|
||||
"description": "A component typically made from flat sheet metal through various manufacturing processes such as bending, punching, cutting, and forming. These brackets are used to support, attach, or mount other hardware components, often providing a structural or functional base for assembly."
|
||||
},
|
||||
{
|
||||
"file": "socket-head-cap-screw.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "socket-head-cap-screw/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Socket Head Cap Screw",
|
||||
"description": "This is for a #10-24 screw that is 1.00 inches long. A socket head cap screw is a type of fastener that is widely used in a variety of applications requiring a high strength fastening solution. It is characterized by its cylindrical head and internal hexagonal drive, which allows for tightening with an Allen wrench or hex key."
|
||||
},
|
||||
{
|
||||
"file": "tire.kcl",
|
||||
"title": "Tire",
|
||||
"description": "A tire is a critical component of a vehicle that provides the necessary traction and grip between the car and the road. It supports the vehicle's weight and absorbs shocks from road irregularities."
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "walkie-talkie/main.kcl",
|
||||
"multipleFiles": true,
|
||||
"title": "Walkie Talkie",
|
||||
"description": "A portable, handheld two-way radio device that allows users to communicate wirelessly over short to medium distances. It operates on specific radio frequencies and features a push-to-talk button for transmitting messages, making it ideal for quick and reliable communication in outdoor, work, or emergency settings."
|
||||
},
|
||||
{
|
||||
"file": "washer.kcl",
|
||||
"file": "main.kcl",
|
||||
"pathFromProjectDirectoryToFirstFile": "washer/main.kcl",
|
||||
"multipleFiles": false,
|
||||
"title": "Washer",
|
||||
"description": "A small, typically disk-shaped component with a hole in the middle, used in a wide range of applications, primarily in conjunction with fasteners like bolts and screws. Washers distribute the load of a fastener across a broader area. This is especially important when the fastening surface is soft or uneven, as it helps to prevent damage to the surface and ensures the load is evenly distributed, reducing the risk of the fastener becoming loose over time."
|
||||
},
|
||||
{
|
||||
"file": "wheel-rotor.kcl",
|
||||
"title": "Wheel rotor",
|
||||
"description": "A component of a disc brake system. It provides a surface for brake pads to press against, generating the friction needed to slow or stop the vehicle."
|
||||
}
|
||||
]
|
15
src/App.tsx
@ -22,13 +22,28 @@ import Gizmo from 'components/Gizmo'
|
||||
import { CoreDumpManager } from 'lib/coredump'
|
||||
import { UnitsMenu } from 'components/UnitsMenu'
|
||||
import { CameraProjectionToggle } from 'components/CameraProjectionToggle'
|
||||
import { useCreateFileLinkQuery } from 'hooks/useCreateFileLinkQueryWatcher'
|
||||
import { maybeWriteToDisk } from 'lib/telemetry'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
maybeWriteToDisk()
|
||||
.then(() => {})
|
||||
.catch(() => {})
|
||||
|
||||
export function App() {
|
||||
const { project, file } = useLoaderData() as IndexLoaderData
|
||||
|
||||
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
|
||||
useCreateFileLinkQuery((argDefaultValues) => {
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
groupId: 'projects',
|
||||
name: 'Import file from URL',
|
||||
argDefaultValues,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
useRefreshSettings(PATHS.FILE + 'SETTINGS')
|
||||
const navigate = useNavigate()
|
||||
const filePath = useAbsoluteFilePath()
|
||||
|
@ -31,11 +31,10 @@ import {
|
||||
settingsLoader,
|
||||
telemetryLoader,
|
||||
} from 'lib/routeLoaders'
|
||||
import { CommandBarProvider } from 'components/CommandBar/CommandBarProvider'
|
||||
import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
||||
import LspProvider from 'components/LspProvider'
|
||||
import { KclContextProvider } from 'lang/KclProvider'
|
||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||
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'
|
||||
@ -47,6 +46,7 @@ import { AppStateProvider } from 'AppState'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { RouteProvider } from 'components/RouteProvider'
|
||||
import { ProjectsContextProvider } from 'components/ProjectsContextProvider'
|
||||
import { OpenInDesktopAppHandler } from 'components/OpenInDesktopAppHandler'
|
||||
|
||||
const createRouter = isDesktop() ? createHashRouter : createBrowserRouter
|
||||
|
||||
@ -58,7 +58,7 @@ const router = createRouter([
|
||||
/* Make sure auth is the outermost provider or else we will have
|
||||
* inefficient re-renders, use the react profiler to see. */
|
||||
element: (
|
||||
<CommandBarProvider>
|
||||
<OpenInDesktopAppHandler>
|
||||
<RouteProvider>
|
||||
<SettingsAuthProvider>
|
||||
<LspProvider>
|
||||
@ -74,17 +74,26 @@ const router = createRouter([
|
||||
</LspProvider>
|
||||
</SettingsAuthProvider>
|
||||
</RouteProvider>
|
||||
</CommandBarProvider>
|
||||
</OpenInDesktopAppHandler>
|
||||
),
|
||||
errorElement: <ErrorPage />,
|
||||
children: [
|
||||
{
|
||||
path: PATHS.INDEX,
|
||||
loader: async () => {
|
||||
loader: async ({ request }) => {
|
||||
const onDesktop = isDesktop()
|
||||
return onDesktop
|
||||
? redirect(PATHS.HOME)
|
||||
: redirect(PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||
const url = new URL(request.url)
|
||||
if (onDesktop) {
|
||||
return redirect(PATHS.HOME + (url.search || ''))
|
||||
} else {
|
||||
const searchParams = new URLSearchParams(url.search)
|
||||
if (!searchParams.has(ASK_TO_OPEN_QUERY_PARAM)) {
|
||||
return redirect(
|
||||
PATHS.FILE + '/%2F' + BROWSER_PROJECT_NAME + (url.search || '')
|
||||
)
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
{
|
||||
|
221
src/Toolbar.tsx
@ -1,8 +1,7 @@
|
||||
import { useRef, useMemo, memo } from 'react'
|
||||
import { useRef, useMemo, memo, useCallback, useState } from 'react'
|
||||
import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
@ -22,20 +21,19 @@ import {
|
||||
} from 'lib/toolbar'
|
||||
import { isDesktop } from 'lib/isDesktop'
|
||||
import { openExternalBrowserIfDesktop } from 'lib/openWindow'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
export function Toolbar({
|
||||
className = '',
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLElement>) {
|
||||
const { state, send, context } = useModelingContext()
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const iconClassName =
|
||||
'group-disabled:text-chalkboard-50 !text-inherit dark:group-enabled:group-hover:!text-inherit'
|
||||
const bgClassName = '!bg-transparent'
|
||||
const buttonBgClassName =
|
||||
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10'
|
||||
const buttonBorderClassName =
|
||||
'!border-transparent hover:!border-chalkboard-20 dark:enabled:hover:!border-primary pressed:!border-primary ui-open:!border-primary'
|
||||
const buttonBorderClassName = '!border-transparent'
|
||||
|
||||
const sketchPathId = useMemo(() => {
|
||||
if (!isSingleCursorInPipe(context.selectionRanges, kclManager.ast))
|
||||
@ -50,6 +48,7 @@ export function Toolbar({
|
||||
const { overallState } = useNetworkContext()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { isStreamReady } = useAppState()
|
||||
const [showRichContent, setShowRichContent] = useState(false)
|
||||
|
||||
const disableAllButtons =
|
||||
(overallState !== NetworkHealthState.Ok &&
|
||||
@ -71,12 +70,45 @@ export function Toolbar({
|
||||
() => ({
|
||||
modelingState: state,
|
||||
modelingSend: send,
|
||||
commandBarSend,
|
||||
sketchPathId,
|
||||
}),
|
||||
[state, send, commandBarSend, sketchPathId]
|
||||
[state, send, commandBarActor.send, sketchPathId]
|
||||
)
|
||||
|
||||
const tooltipContentClassName = !showRichContent
|
||||
? ''
|
||||
: '!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch'
|
||||
const richContentTimeout = useRef<number | null>(null)
|
||||
const richContentClearTimeout = useRef<number | null>(null)
|
||||
// On mouse enter, show rich content after a 1s delay
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
// Cancel the clear timeout if it's already set
|
||||
if (richContentClearTimeout.current) {
|
||||
clearTimeout(richContentClearTimeout.current)
|
||||
}
|
||||
// Start our own timeout to show the rich content
|
||||
richContentTimeout.current = window.setTimeout(() => {
|
||||
setShowRichContent(true)
|
||||
if (richContentClearTimeout.current) {
|
||||
clearTimeout(richContentClearTimeout.current)
|
||||
}
|
||||
}, 1000)
|
||||
}, [setShowRichContent])
|
||||
// On mouse leave, clear the timeout and hide rich content
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
// Clear the timeout to show rich content
|
||||
if (richContentTimeout.current) {
|
||||
clearTimeout(richContentTimeout.current)
|
||||
}
|
||||
// Start a timeout to hide the rich content
|
||||
richContentClearTimeout.current = window.setTimeout(() => {
|
||||
setShowRichContent(false)
|
||||
if (richContentClearTimeout.current) {
|
||||
clearTimeout(richContentClearTimeout.current)
|
||||
}
|
||||
}, 500)
|
||||
}, [setShowRichContent])
|
||||
|
||||
/**
|
||||
* Resolve all the callbacks and values for the current mode,
|
||||
* so we don't need to worry about the other modes
|
||||
@ -174,43 +206,64 @@ export function Toolbar({
|
||||
status: itemConfig.status,
|
||||
}))}
|
||||
>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
id={maybeIconConfig[0].id}
|
||||
data-testid={maybeIconConfig[0].id}
|
||||
iconStart={{
|
||||
icon: maybeIconConfig[0].icon,
|
||||
className: iconClassName,
|
||||
bgClassName: bgClassName,
|
||||
}}
|
||||
className={
|
||||
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
||||
buttonBgClassName
|
||||
}
|
||||
aria-pressed={maybeIconConfig[0].isActive}
|
||||
disabled={
|
||||
disableAllButtons ||
|
||||
maybeIconConfig[0].status !== 'available' ||
|
||||
maybeIconConfig[0].disabled
|
||||
}
|
||||
name={maybeIconConfig[0].title}
|
||||
// aria-description is still in ARIA 1.3 draft.
|
||||
// eslint-disable-next-line jsx-a11y/aria-props
|
||||
aria-description={maybeIconConfig[0].description}
|
||||
onClick={() =>
|
||||
maybeIconConfig[0].onClick(configCallbackProps)
|
||||
}
|
||||
<div
|
||||
className="contents"
|
||||
// Mouse events do not fire on disabled buttons
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<span
|
||||
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
|
||||
<ActionButton
|
||||
Element="button"
|
||||
id={maybeIconConfig[0].id}
|
||||
data-testid={maybeIconConfig[0].id}
|
||||
iconStart={{
|
||||
icon: maybeIconConfig[0].icon,
|
||||
className: iconClassName,
|
||||
bgClassName: bgClassName,
|
||||
}}
|
||||
className={
|
||||
'!border-transparent !px-0 pressed:!text-chalkboard-10 pressed:enabled:hovered:!text-chalkboard-10 ' +
|
||||
buttonBgClassName
|
||||
}
|
||||
aria-pressed={maybeIconConfig[0].isActive}
|
||||
disabled={
|
||||
disableAllButtons ||
|
||||
maybeIconConfig[0].status !== 'available' ||
|
||||
maybeIconConfig[0].disabled
|
||||
}
|
||||
name={maybeIconConfig[0].title}
|
||||
// aria-description is still in ARIA 1.3 draft.
|
||||
// eslint-disable-next-line jsx-a11y/aria-props
|
||||
aria-description={maybeIconConfig[0].description}
|
||||
onClick={() =>
|
||||
maybeIconConfig[0].onClick(configCallbackProps)
|
||||
}
|
||||
>
|
||||
{maybeIconConfig[0].title}
|
||||
</span>
|
||||
</ActionButton>
|
||||
<ToolbarItemTooltip
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
configCallbackProps={configCallbackProps}
|
||||
/>
|
||||
<span
|
||||
className={!maybeIconConfig[0].showTitle ? 'sr-only' : ''}
|
||||
>
|
||||
{maybeIconConfig[0].title}
|
||||
</span>
|
||||
<ToolbarItemTooltip
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
configCallbackProps={configCallbackProps}
|
||||
wrapperClassName="ui-open:!hidden"
|
||||
contentClassName={tooltipContentClassName}
|
||||
>
|
||||
{showRichContent ? (
|
||||
<ToolbarItemTooltipRichContent
|
||||
itemConfig={maybeIconConfig[0]}
|
||||
/>
|
||||
) : (
|
||||
<ToolbarItemTooltipShortContent
|
||||
status={maybeIconConfig[0].status}
|
||||
title={maybeIconConfig[0].title}
|
||||
hotkey={maybeIconConfig[0].hotkey}
|
||||
/>
|
||||
)}
|
||||
</ToolbarItemTooltip>
|
||||
</ActionButton>
|
||||
</div>
|
||||
</ActionButtonDropdown>
|
||||
)
|
||||
}
|
||||
@ -218,7 +271,13 @@ export function Toolbar({
|
||||
|
||||
// A single button
|
||||
return (
|
||||
<div className="relative" key={itemConfig.id}>
|
||||
<div
|
||||
className="relative"
|
||||
key={itemConfig.id}
|
||||
// Mouse events do not fire on disabled buttons
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
key={itemConfig.id}
|
||||
@ -255,7 +314,18 @@ export function Toolbar({
|
||||
<ToolbarItemTooltip
|
||||
itemConfig={itemConfig}
|
||||
configCallbackProps={configCallbackProps}
|
||||
/>
|
||||
contentClassName={tooltipContentClassName}
|
||||
>
|
||||
{showRichContent ? (
|
||||
<ToolbarItemTooltipRichContent itemConfig={itemConfig} />
|
||||
) : (
|
||||
<ToolbarItemTooltipShortContent
|
||||
status={itemConfig.status}
|
||||
title={itemConfig.title}
|
||||
hotkey={itemConfig.hotkey}
|
||||
/>
|
||||
)}
|
||||
</ToolbarItemTooltip>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@ -269,6 +339,12 @@ export function Toolbar({
|
||||
)
|
||||
}
|
||||
|
||||
interface ToolbarItemContentsProps extends React.PropsWithChildren {
|
||||
itemConfig: ToolbarItemResolved
|
||||
configCallbackProps: ToolbarItemCallbackProps
|
||||
wrapperClassName?: string
|
||||
contentClassName?: string
|
||||
}
|
||||
/**
|
||||
* The single button and dropdown button share content, so we extract it here
|
||||
* It contains a tooltip with the title, description, and links
|
||||
@ -277,12 +353,10 @@ export function Toolbar({
|
||||
const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
itemConfig,
|
||||
configCallbackProps,
|
||||
}: {
|
||||
itemConfig: ToolbarItemResolved
|
||||
configCallbackProps: ToolbarItemCallbackProps
|
||||
}) {
|
||||
const { state } = useModelingContext()
|
||||
|
||||
wrapperClassName = '',
|
||||
contentClassName = '',
|
||||
children,
|
||||
}: ToolbarItemContentsProps) {
|
||||
useHotkeys(
|
||||
itemConfig.hotkey || '',
|
||||
() => {
|
||||
@ -305,11 +379,50 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties)
|
||||
: {}
|
||||
}
|
||||
hoverOnly
|
||||
position="bottom"
|
||||
wrapperClassName="!p-4 !pointer-events-auto"
|
||||
contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch"
|
||||
wrapperClassName={'!p-4 !pointer-events-auto ' + wrapperClassName}
|
||||
contentClassName={contentClassName}
|
||||
delay={0}
|
||||
>
|
||||
{children}
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
|
||||
const ToolbarItemTooltipShortContent = ({
|
||||
status,
|
||||
title,
|
||||
hotkey,
|
||||
}: {
|
||||
status: string
|
||||
title: string
|
||||
hotkey?: string | string[]
|
||||
}) => (
|
||||
<span
|
||||
className={`text-sm ${
|
||||
status !== 'available' ? 'text-chalkboard-70 dark:text-chalkboard-40' : ''
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
{hotkey && (
|
||||
<kbd className="inline-block ml-2 flex-none hotkey">{hotkey}</kbd>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
|
||||
const ToolbarItemTooltipRichContent = ({
|
||||
itemConfig,
|
||||
}: {
|
||||
itemConfig: ToolbarItemResolved
|
||||
}) => {
|
||||
const { state } = useModelingContext()
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-top flex items-center gap-2 pt-3 pb-2 px-2 bg-chalkboard-20/50 dark:bg-chalkboard-80/50">
|
||||
{itemConfig.icon && (
|
||||
<CustomIcon className="w-5 h-5" name={itemConfig.icon} />
|
||||
)}
|
||||
<span
|
||||
className={`text-sm flex-1 ${
|
||||
itemConfig.status !== 'available'
|
||||
@ -378,6 +491,6 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -108,6 +108,8 @@ export class CameraControls {
|
||||
interactionGuards: MouseGuard = cameraMouseDragGuards.Zoo
|
||||
isFovAnimationInProgress = false
|
||||
perspectiveFovBeforeOrtho = 45
|
||||
// NOTE: Duplicated state across Provider and singleton. Mapped from settingsMachine
|
||||
_setting_allowOrbitInSketchMode = false
|
||||
get isPerspective() {
|
||||
return this.camera instanceof PerspectiveCamera
|
||||
}
|
||||
|
@ -25,13 +25,13 @@ import {
|
||||
CallExpression,
|
||||
PathToNode,
|
||||
Program,
|
||||
SourceRange,
|
||||
Expr,
|
||||
parse,
|
||||
recast,
|
||||
defaultSourceRange,
|
||||
resultIsOk,
|
||||
ProgramMemory,
|
||||
topLevelRange,
|
||||
} from 'lang/wasm'
|
||||
import { CustomIcon, CustomIconName } from 'components/CustomIcon'
|
||||
import { ConstrainInfo } from 'lang/std/stdTypes'
|
||||
@ -46,8 +46,8 @@ import {
|
||||
} from 'lang/modifyAst'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { err, reportRejection, trap } from 'lib/trap'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
|
||||
const [isCamMoving, setIsCamMoving] = useState(false)
|
||||
@ -124,6 +124,14 @@ export const ClientSideScene = ({
|
||||
'mouseup',
|
||||
toSync(sceneInfra.onMouseUp, reportRejection)
|
||||
)
|
||||
sceneEntitiesManager
|
||||
.tearDownSketch()
|
||||
.then(() => {
|
||||
// no op
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -510,7 +518,6 @@ const ConstraintSymbol = ({
|
||||
constrainInfo: ConstrainInfo
|
||||
verticalPosition: 'top' | 'bottom'
|
||||
}) => {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { context } = useModelingContext()
|
||||
const varNameMap: {
|
||||
[key in ConstrainInfo['type']]: {
|
||||
@ -600,8 +607,8 @@ const ConstraintSymbol = ({
|
||||
if (err(_node)) return
|
||||
const node = _node.node
|
||||
|
||||
const range: SourceRange = node
|
||||
? [node.start, node.end, true]
|
||||
const range = node
|
||||
? topLevelRange(node.start, node.end)
|
||||
: defaultSourceRange()
|
||||
|
||||
if (_type === 'intersectionTag') return null
|
||||
@ -630,7 +637,7 @@ const ConstraintSymbol = ({
|
||||
// disabled={implicitDesc} TODO why does this change styles that are hard to override?
|
||||
onClick={toSync(async () => {
|
||||
if (!isConstrained) {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
name: 'Constrain with named value',
|
||||
@ -756,7 +763,6 @@ export const CamDebugSettings = () => {
|
||||
sceneInfra.camControls.reactCameraProperties
|
||||
)
|
||||
const [fov, setFov] = useState(12)
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
|
||||
useEffect(() => {
|
||||
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
|
||||
@ -775,7 +781,7 @@ export const CamDebugSettings = () => {
|
||||
type="checkbox"
|
||||
checked={camSettings.type === 'perspective'}
|
||||
onChange={() =>
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
groupId: 'settings',
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {
|
||||
BoxGeometry,
|
||||
Color,
|
||||
DoubleSide,
|
||||
Group,
|
||||
Intersection,
|
||||
@ -58,7 +59,9 @@ import {
|
||||
sourceRangeFromRust,
|
||||
resultIsOk,
|
||||
SourceRange,
|
||||
topLevelRange,
|
||||
} from 'lang/wasm'
|
||||
import { calculate_circle_from_3_points } from '../wasm-lib/pkg/wasm_lib'
|
||||
import {
|
||||
engineCommandManager,
|
||||
kclManager,
|
||||
@ -66,11 +69,12 @@ import {
|
||||
codeManager,
|
||||
editorManager,
|
||||
} from 'lib/singletons'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { executeAst, ToolTip } from 'lang/langHelpers'
|
||||
import {
|
||||
createProfileStartHandle,
|
||||
createArcGeometry,
|
||||
createCircleGeometry,
|
||||
SegmentUtils,
|
||||
segmentUtils,
|
||||
} from './segments'
|
||||
@ -109,6 +113,8 @@ import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
|
||||
import { Point3d } from 'wasm-lib/kcl/bindings/Point3d'
|
||||
import { SegmentInputs } from 'lang/std/stdTypes'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { LabeledArg } from 'wasm-lib/kcl/bindings/LabeledArg'
|
||||
import { Literal } from 'wasm-lib/kcl/bindings/Literal'
|
||||
import { radToDeg } from 'three/src/math/MathUtils'
|
||||
import { getArtifactFromRange, codeRefFromRange } from 'lang/std/artifactGraph'
|
||||
|
||||
@ -624,7 +630,7 @@ export class SceneEntities {
|
||||
|
||||
const startRange = _node1.node.start
|
||||
const endRange = _node1.node.end
|
||||
const sourceRange: SourceRange = [startRange, endRange, true]
|
||||
const sourceRange = topLevelRange(startRange, endRange)
|
||||
const selection: Selections = computeSelectionFromSourceRangeAndAST(
|
||||
sourceRange,
|
||||
maybeModdedAst
|
||||
@ -1261,110 +1267,98 @@ export class SceneEntities {
|
||||
const groupOfDrafts = new Group()
|
||||
groupOfDrafts.name = 'circle-3-point-group'
|
||||
groupOfDrafts.position.copy(sketchOrigin)
|
||||
|
||||
// lee: I'm keeping this here as a developer gotchya:
|
||||
// Do not reorient your surfaces to the intersection plane. Your points are
|
||||
// already in 3D space, not 2D. If you intersect say XZ, you want the points
|
||||
// to continue to live at the 3D intersection point, not be rotated to end
|
||||
// up elsewhere!
|
||||
// groupOfDrafts.setRotationFromQuaternion(orientation)
|
||||
// If you use 3D points, do not rotate anything.
|
||||
// If you use 2D points (easier to deal with, generally do this!), then
|
||||
// rotate the group just like this! Remember to rotate other groups too!
|
||||
groupOfDrafts.setRotationFromQuaternion(orientation)
|
||||
this.scene.add(groupOfDrafts)
|
||||
|
||||
const DRAFT_POINT_RADIUS = 6
|
||||
// How large the points on the circle will render as
|
||||
const DRAFT_POINT_RADIUS = 10 // px
|
||||
|
||||
const createPoint = (center: Vector3): number => {
|
||||
// The target of our dragging
|
||||
let target: Object3D | undefined = undefined
|
||||
|
||||
// The KCL this will generate.
|
||||
const kclCircle3Point = parse(`circleThreePoint(
|
||||
p1 = [0.0, 0.0],
|
||||
p2 = [0.0, 0.0],
|
||||
p3 = [0.0, 0.0],
|
||||
)`)
|
||||
|
||||
const createPoint = (
|
||||
center: Vector3,
|
||||
opts?: { noInteraction?: boolean }
|
||||
): Mesh => {
|
||||
const geometry = new SphereGeometry(DRAFT_POINT_RADIUS)
|
||||
const color = getThemeColorForThreeJs(sceneInfra._theme)
|
||||
const material = new MeshBasicMaterial({ color })
|
||||
|
||||
const material = new MeshBasicMaterial({
|
||||
color: opts?.noInteraction
|
||||
? sceneInfra._theme === 'light'
|
||||
? new Color(color).multiplyScalar(0.15)
|
||||
: new Color(0x010101).multiplyScalar(2000)
|
||||
: color,
|
||||
})
|
||||
|
||||
const mesh = new Mesh(geometry, material)
|
||||
mesh.userData = { type: CIRCLE_3_POINT_DRAFT_POINT }
|
||||
mesh.userData = {
|
||||
type: opts?.noInteraction ? 'ghost' : CIRCLE_3_POINT_DRAFT_POINT,
|
||||
}
|
||||
mesh.renderOrder = 1000
|
||||
mesh.layers.set(SKETCH_LAYER)
|
||||
mesh.position.copy(center)
|
||||
mesh.scale.set(scale, scale, scale)
|
||||
mesh.renderOrder = 100
|
||||
|
||||
groupOfDrafts.add(mesh)
|
||||
|
||||
return mesh.id
|
||||
return mesh
|
||||
}
|
||||
|
||||
const circle3Point = (
|
||||
points: Vector2[]
|
||||
): undefined | { center: Vector3; radius: number } => {
|
||||
// A 3-point circle is undefined if it doesn't have 3 points :)
|
||||
if (points.length !== 3) return undefined
|
||||
|
||||
// y = (i/j)(x-h) + b
|
||||
// i and j variables for the slopes
|
||||
const i = [points[1].x - points[0].x, points[2].x - points[1].x]
|
||||
const j = [points[1].y - points[0].y, points[2].y - points[1].y]
|
||||
|
||||
// Our / threejs coordinate system affects this a lot. If you take this
|
||||
// code into a different code base, you may have to adjust a/b to being
|
||||
// -1/a/b, b/a, etc! In this case, a/-b did the trick.
|
||||
const m = [i[0] / -j[0], i[1] / -j[1]]
|
||||
|
||||
const h = [
|
||||
(points[0].x + points[1].x) / 2,
|
||||
(points[1].x + points[2].x) / 2,
|
||||
]
|
||||
const b = [
|
||||
(points[0].y + points[1].y) / 2,
|
||||
(points[1].y + points[2].y) / 2,
|
||||
]
|
||||
|
||||
// Algebraically derived
|
||||
const x = (-m[0] * h[0] + b[0] - b[1] + m[1] * h[1]) / (m[1] - m[0])
|
||||
const y = m[0] * (x - h[0]) + b[0]
|
||||
|
||||
const center = new Vector3(x, y, 0)
|
||||
const radius = Math.sqrt((points[1].x - x) ** 2 + (points[1].y - y) ** 2)
|
||||
|
||||
return {
|
||||
center,
|
||||
radius,
|
||||
}
|
||||
}
|
||||
|
||||
// TO BE SHORT LIVED: unused function to draw the circle and lines.
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
const createCircle3Point = (points: Vector2[]) => {
|
||||
const circleParams = circle3Point(points)
|
||||
|
||||
// A circle cannot be created for these points.
|
||||
if (!circleParams) return
|
||||
const createCircle3PointGraphic = async (
|
||||
points: Vector2[],
|
||||
center: Vector2,
|
||||
radius: number
|
||||
) => {
|
||||
if (
|
||||
Number.isNaN(radius) ||
|
||||
Number.isNaN(center.x) ||
|
||||
Number.isNaN(center.y)
|
||||
)
|
||||
return
|
||||
|
||||
const color = getThemeColorForThreeJs(sceneInfra._theme)
|
||||
const geometryCircle = createArcGeometry({
|
||||
center: [circleParams.center.x, circleParams.center.y],
|
||||
radius: circleParams.radius,
|
||||
startAngle: 0,
|
||||
endAngle: Math.PI * 2,
|
||||
ccw: true,
|
||||
isDashed: true,
|
||||
scale,
|
||||
const lineCircle = createCircleGeometry({
|
||||
center: [center.x, center.y],
|
||||
radius,
|
||||
color,
|
||||
isDashed: false,
|
||||
scale: 1,
|
||||
})
|
||||
const materialCircle = new MeshBasicMaterial({ color })
|
||||
lineCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE }
|
||||
lineCircle.layers.set(SKETCH_LAYER)
|
||||
// devnote: it's a mistake to use these with EllipseCurve :)
|
||||
// lineCircle.position.set(center.x, center.y, 0)
|
||||
// lineCircle.scale.set(scale, scale, scale)
|
||||
|
||||
if (groupCircle) groupOfDrafts.remove(groupCircle)
|
||||
groupCircle = new Group()
|
||||
groupCircle.renderOrder = 1
|
||||
groupCircle.add(lineCircle)
|
||||
|
||||
const meshCircle = new Mesh(geometryCircle, materialCircle)
|
||||
meshCircle.userData = { type: CIRCLE_3_POINT_DRAFT_CIRCLE }
|
||||
meshCircle.layers.set(SKETCH_LAYER)
|
||||
meshCircle.position.set(circleParams.center.x, circleParams.center.y, 0)
|
||||
meshCircle.scale.set(scale, scale, scale)
|
||||
groupCircle.add(meshCircle)
|
||||
const pointMesh = createPoint(new Vector3(center.x, center.y, 0), {
|
||||
noInteraction: true,
|
||||
})
|
||||
groupCircle.add(pointMesh)
|
||||
|
||||
const geometryPolyLine = new BufferGeometry().setFromPoints([
|
||||
...points,
|
||||
points[0],
|
||||
...points.map((p) => new Vector3(p.x, p.y, 0)),
|
||||
new Vector3(points[0].x, points[0].y, 0),
|
||||
])
|
||||
const materialPolyLine = new LineDashedMaterial({
|
||||
color,
|
||||
scale,
|
||||
scale: 1 / scale,
|
||||
dashSize: 6,
|
||||
gapSize: 6,
|
||||
})
|
||||
@ -1375,13 +1369,146 @@ export class SceneEntities {
|
||||
groupOfDrafts.add(groupCircle)
|
||||
}
|
||||
|
||||
// The target of our dragging
|
||||
let target: Object3D | undefined = undefined
|
||||
const insertCircle3PointKclIntoAstSnapshot = (
|
||||
points: Vector2[]
|
||||
): Program => {
|
||||
if (err(kclCircle3Point) || kclCircle3Point.program === null)
|
||||
return kclManager.ast
|
||||
if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement')
|
||||
return kclManager.ast
|
||||
if (
|
||||
kclCircle3Point.program.body[0].expression.type !== 'CallExpressionKw'
|
||||
)
|
||||
return kclManager.ast
|
||||
|
||||
const arg = (x: LabeledArg): Literal[] | undefined => {
|
||||
if (
|
||||
'arg' in x &&
|
||||
'elements' in x.arg &&
|
||||
x.arg.type === 'ArrayExpression'
|
||||
) {
|
||||
if (x.arg.elements.every((x) => x.type === 'Literal')) {
|
||||
return x.arg.elements
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const kclCircle3PointArgs =
|
||||
kclCircle3Point.program.body[0].expression.arguments
|
||||
|
||||
const arg0 = arg(kclCircle3PointArgs[0])
|
||||
if (!arg0) return kclManager.ast
|
||||
arg0[0].value = { value: points[0].x, suffix: 'None' }
|
||||
arg0[0].raw = points[0].x.toString()
|
||||
arg0[1].value = { value: points[0].y, suffix: 'None' }
|
||||
arg0[1].raw = points[0].y.toString()
|
||||
|
||||
const arg1 = arg(kclCircle3PointArgs[1])
|
||||
if (!arg1) return kclManager.ast
|
||||
arg1[0].value = { value: points[1].x, suffix: 'None' }
|
||||
arg1[0].raw = points[1].x.toString()
|
||||
arg1[1].value = { value: points[1].y, suffix: 'None' }
|
||||
arg1[1].raw = points[1].y.toString()
|
||||
|
||||
const arg2 = arg(kclCircle3PointArgs[2])
|
||||
if (!arg2) return kclManager.ast
|
||||
arg2[0].value = { value: points[2].x, suffix: 'None' }
|
||||
arg2[0].raw = points[2].x.toString()
|
||||
arg2[1].value = { value: points[2].y, suffix: 'None' }
|
||||
arg2[1].raw = points[2].y.toString()
|
||||
|
||||
const astSnapshot = structuredClone(kclManager.ast)
|
||||
const startSketchOnASTNode = getNodeFromPath<VariableDeclaration>(
|
||||
astSnapshot,
|
||||
startSketchOnASTNodePath,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(startSketchOnASTNode)) return astSnapshot
|
||||
|
||||
// It's possible we're already dealing with a PipeExpression.
|
||||
// Modify the current one.
|
||||
if (
|
||||
startSketchOnASTNode.node.declaration.init.type === 'PipeExpression' &&
|
||||
startSketchOnASTNode.node.declaration.init.body[1].type ===
|
||||
'CallExpressionKw' &&
|
||||
startSketchOnASTNode.node.declaration.init.body.length >= 2
|
||||
) {
|
||||
startSketchOnASTNode.node.declaration.init.body[1].arguments =
|
||||
kclCircle3Point.program.body[0].expression.arguments
|
||||
} else {
|
||||
// Clone a new node based on the old, and replace the old with the new.
|
||||
const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode)
|
||||
startSketchOnASTNode.node.declaration.init = createPipeExpression([
|
||||
clonedStartSketchOnASTNode.node.declaration.init,
|
||||
kclCircle3Point.program.body[0].expression,
|
||||
])
|
||||
}
|
||||
|
||||
// Return the `Program`
|
||||
return astSnapshot
|
||||
}
|
||||
|
||||
const updateCircle3Point = async (opts?: { execute?: true }) => {
|
||||
const points_ = Array.from(points.values())
|
||||
const circleParams = calculate_circle_from_3_points(
|
||||
points_[0].x,
|
||||
points_[0].y,
|
||||
points_[1].x,
|
||||
points_[1].y,
|
||||
points_[2].x,
|
||||
points_[2].y
|
||||
)
|
||||
|
||||
if (Number.isNaN(circleParams.radius)) return
|
||||
|
||||
await createCircle3PointGraphic(
|
||||
points_,
|
||||
new Vector2(circleParams.center_x, circleParams.center_y),
|
||||
circleParams.radius
|
||||
)
|
||||
const astWithNewCode = insertCircle3PointKclIntoAstSnapshot(points_)
|
||||
const codeAsString = recast(astWithNewCode)
|
||||
if (err(codeAsString)) return
|
||||
codeManager.updateCodeStateEditor(codeAsString)
|
||||
}
|
||||
|
||||
const cleanupFn = () => {
|
||||
this.scene.remove(groupOfDrafts)
|
||||
}
|
||||
|
||||
// The AST node we extracted earlier may already have a circleThreePoint!
|
||||
// Use the points in the AST as starting points.
|
||||
const astSnapshot = structuredClone(kclManager.ast)
|
||||
const maybeVariableDeclaration = getNodeFromPath<VariableDeclaration>(
|
||||
astSnapshot,
|
||||
startSketchOnASTNodePath,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(maybeVariableDeclaration))
|
||||
return () => {
|
||||
done()
|
||||
}
|
||||
|
||||
const maybeCallExpressionKw = maybeVariableDeclaration.node.declaration.init
|
||||
if (
|
||||
maybeCallExpressionKw.type === 'PipeExpression' &&
|
||||
maybeCallExpressionKw.body[1].type === 'CallExpressionKw' &&
|
||||
maybeCallExpressionKw.body[1]?.callee.name === 'circleThreePoint'
|
||||
) {
|
||||
maybeCallExpressionKw?.body[1].arguments
|
||||
.map(
|
||||
({ arg }: any) =>
|
||||
new Vector2(arg.elements[0].value, arg.elements[1].value)
|
||||
)
|
||||
.forEach((point: Vector2) => {
|
||||
const pointMesh = createPoint(new Vector3(point.x, point.y, 0))
|
||||
groupOfDrafts.add(pointMesh)
|
||||
points.set(pointMesh.id, point)
|
||||
})
|
||||
void updateCircle3Point()
|
||||
}
|
||||
|
||||
sceneInfra.setCallbacks({
|
||||
async onDrag(args) {
|
||||
const draftPointsIntersected = args.intersects.filter(
|
||||
@ -1397,8 +1524,18 @@ export class SceneEntities {
|
||||
// The user was off their mark! Missed the object to select.
|
||||
if (!target) return
|
||||
|
||||
target.position.copy(args.intersectionPoint.threeD)
|
||||
target.position.copy(
|
||||
new Vector3(
|
||||
args.intersectionPoint.twoD.x,
|
||||
args.intersectionPoint.twoD.y,
|
||||
0
|
||||
)
|
||||
)
|
||||
points.set(target.id, args.intersectionPoint.twoD)
|
||||
|
||||
if (points.size <= 2) return
|
||||
|
||||
await updateCircle3Point()
|
||||
},
|
||||
async onDragEnd(_args) {
|
||||
target = undefined
|
||||
@ -1407,45 +1544,19 @@ export class SceneEntities {
|
||||
if (points.size >= 3) return
|
||||
if (!args.intersectionPoint) return
|
||||
|
||||
const id = createPoint(args.intersectionPoint.threeD)
|
||||
points.set(id, args.intersectionPoint.twoD)
|
||||
|
||||
if (points.size < 2) return
|
||||
|
||||
// We've now got 3 points, let's create our circle!
|
||||
const astSnapshot = structuredClone(kclManager.ast)
|
||||
let nodeQueryResult
|
||||
nodeQueryResult = getNodeFromPath<VariableDeclaration>(
|
||||
astSnapshot,
|
||||
startSketchOnASTNodePath,
|
||||
'VariableDeclaration'
|
||||
const pointMesh = createPoint(
|
||||
new Vector3(
|
||||
args.intersectionPoint.twoD.x,
|
||||
args.intersectionPoint.twoD.y,
|
||||
0
|
||||
)
|
||||
)
|
||||
if (err(nodeQueryResult)) return Promise.reject(nodeQueryResult)
|
||||
const startSketchOnASTNode = nodeQueryResult
|
||||
groupOfDrafts.add(pointMesh)
|
||||
points.set(pointMesh.id, args.intersectionPoint.twoD)
|
||||
|
||||
const circleParams = circle3Point(Array.from(points.values()))
|
||||
if (points.size <= 2) return
|
||||
|
||||
if (!circleParams) return
|
||||
|
||||
const kclCircle3Point = parse(`circle({
|
||||
center = [${circleParams.center.x}, ${circleParams.center.y}],
|
||||
radius = ${circleParams.radius},
|
||||
}, %)`)
|
||||
|
||||
if (err(kclCircle3Point) || kclCircle3Point.program === null) return
|
||||
if (kclCircle3Point.program.body[0].type !== 'ExpressionStatement')
|
||||
return
|
||||
|
||||
const clonedStartSketchOnASTNode = structuredClone(startSketchOnASTNode)
|
||||
startSketchOnASTNode.node.declaration.init = createPipeExpression([
|
||||
clonedStartSketchOnASTNode.node.declaration.init,
|
||||
kclCircle3Point.program.body[0].expression,
|
||||
])
|
||||
|
||||
await kclManager.executeAstMock(astSnapshot)
|
||||
await codeManager.updateEditorWithAstAndWriteToFile(astSnapshot)
|
||||
|
||||
done()
|
||||
await updateCircle3Point()
|
||||
},
|
||||
})
|
||||
|
||||
@ -1903,7 +2014,7 @@ export class SceneEntities {
|
||||
kclManager.programMemory,
|
||||
{
|
||||
type: 'sourceRange',
|
||||
sourceRange: [node.start, node.end, true],
|
||||
sourceRange: topLevelRange(node.start, node.end),
|
||||
},
|
||||
getChangeSketchInput()
|
||||
)
|
||||
@ -1941,8 +2052,8 @@ export class SceneEntities {
|
||||
)
|
||||
if (!(sk instanceof Reason)) {
|
||||
sketch = sk
|
||||
} else if ((maybeSketch as Solid).sketch) {
|
||||
sketch = (maybeSketch as Solid).sketch
|
||||
} else if (maybeSketch && (maybeSketch.value as Solid)?.sketch) {
|
||||
sketch = (maybeSketch.value as Solid).sketch
|
||||
}
|
||||
if (!sketch) return
|
||||
|
||||
@ -2154,7 +2265,7 @@ export class SceneEntities {
|
||||
)
|
||||
if (trap(_node, { suppress: true })) return
|
||||
const node = _node.node
|
||||
editorManager.setHighlightRange([[node.start, node.end, true]])
|
||||
editorManager.setHighlightRange([topLevelRange(node.start, node.end)])
|
||||
const yellow = 0xffff00
|
||||
colorSegment(selected, yellow)
|
||||
const extraSegmentGroup = parent.getObjectByName(EXTRA_SEGMENT_HANDLE)
|
||||
@ -2431,7 +2542,7 @@ export function sketchFromPathToNode({
|
||||
const varDec = _varDec.node
|
||||
const result = programMemory.get(varDec?.id?.name || '')
|
||||
if (result?.type === 'Solid') {
|
||||
return result.sketch
|
||||
return result.value.sketch
|
||||
}
|
||||
const sg = sketchFromKclValue(result, varDec?.id?.name)
|
||||
if (err(sg)) {
|
||||
|
@ -9,6 +9,9 @@ import {
|
||||
ExtrudeGeometry,
|
||||
Group,
|
||||
LineCurve3,
|
||||
LineBasicMaterial,
|
||||
LineDashedMaterial,
|
||||
Line,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
NormalBufferAttributes,
|
||||
@ -58,6 +61,7 @@ import { SegmentInputs } from 'lang/std/stdTypes'
|
||||
import { err } from 'lib/trap'
|
||||
import { editorManager, sceneInfra } from 'lib/singletons'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { commandBarActor } from 'machines/commandBarMachine'
|
||||
|
||||
interface CreateSegmentArgs {
|
||||
input: SegmentInputs
|
||||
@ -844,7 +848,7 @@ function createLengthIndicator({
|
||||
})
|
||||
|
||||
// Command Bar
|
||||
editorManager.commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
name: 'Constrain length',
|
||||
@ -1003,6 +1007,49 @@ export function createArcGeometry({
|
||||
return geo
|
||||
}
|
||||
|
||||
// (lee) The above is much more complex than necessary.
|
||||
// I've derived the new code from:
|
||||
// https://threejs.org/docs/#api/en/extras/curves/EllipseCurve
|
||||
// I'm not sure why it wasn't done like this in the first place?
|
||||
// I don't touch the code above because it may break something else.
|
||||
export function createCircleGeometry({
|
||||
center,
|
||||
radius,
|
||||
color,
|
||||
isDashed = false,
|
||||
scale = 1,
|
||||
}: {
|
||||
center: Coords2d
|
||||
radius: number
|
||||
color: number
|
||||
isDashed?: boolean
|
||||
scale?: number
|
||||
}): Line {
|
||||
const circle = new EllipseCurve(
|
||||
center[0],
|
||||
center[1],
|
||||
radius,
|
||||
radius,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
true,
|
||||
scale
|
||||
)
|
||||
const points = circle.getPoints(75) // just enough points to not see edges.
|
||||
const geometry = new BufferGeometry().setFromPoints(points)
|
||||
const material = !isDashed
|
||||
? new LineBasicMaterial({ color })
|
||||
: new LineDashedMaterial({
|
||||
color,
|
||||
scale,
|
||||
dashSize: 6,
|
||||
gapSize: 6,
|
||||
})
|
||||
const line = new Line(geometry, material)
|
||||
line.computeLineDistances()
|
||||
return line
|
||||
}
|
||||
|
||||
export function dashedStraight(
|
||||
from: Coords2d,
|
||||
to: Coords2d,
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { ActionButtonProps } from './ActionButton'
|
||||
import { CustomIcon } from './CustomIcon'
|
||||
import Tooltip from './Tooltip'
|
||||
|
||||
type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
|
||||
name?: string
|
||||
dropdownTooltipText?: string
|
||||
splitMenuItems: {
|
||||
id: string
|
||||
label: string
|
||||
@ -17,6 +19,7 @@ type ActionButtonSplitProps = ActionButtonProps & { Element: 'button' } & {
|
||||
export function ActionButtonDropdown({
|
||||
splitMenuItems,
|
||||
className,
|
||||
dropdownTooltipText = 'More tools',
|
||||
children,
|
||||
...props
|
||||
}: ActionButtonSplitProps) {
|
||||
@ -26,7 +29,14 @@ export function ActionButtonDropdown({
|
||||
{({ close }) => (
|
||||
<>
|
||||
{children}
|
||||
<Popover.Button className="border-transparent dark:border-transparent p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary">
|
||||
<Popover.Button
|
||||
className={
|
||||
'!border-transparent dark:!border-transparent ' +
|
||||
'bg-chalkboard-transparent dark:bg-transparent disabled:bg-transparent dark:disabled:bg-transparent ' +
|
||||
'enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 ' +
|
||||
'pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10 p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary'
|
||||
}
|
||||
>
|
||||
<CustomIcon
|
||||
name="caretDown"
|
||||
className={
|
||||
@ -37,6 +47,14 @@ export function ActionButtonDropdown({
|
||||
<span className="sr-only">
|
||||
{props.name ? props.name + ': ' : ''}open menu
|
||||
</span>
|
||||
<Tooltip
|
||||
delay={0}
|
||||
position="bottom"
|
||||
hoverOnly
|
||||
wrapperClassName="ui-open:!hidden"
|
||||
>
|
||||
{dropdownTooltipText}
|
||||
</Tooltip>
|
||||
</Popover.Button>
|
||||
<Popover.Panel
|
||||
as="ul"
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { editorManager, engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { trap } from 'lib/trap'
|
||||
import { codeToIdSelections } from 'lib/selections'
|
||||
import { codeRefFromRange } from 'lang/std/artifactGraph'
|
||||
import { defaultSourceRange } from 'lang/wasm'
|
||||
import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm'
|
||||
|
||||
export function AstExplorer() {
|
||||
const { context } = useModelingContext()
|
||||
@ -118,19 +119,19 @@ function DisplayObj({
|
||||
hasCursor ? 'bg-violet-100/80 dark:bg-violet-100/25' : ''
|
||||
}`}
|
||||
onMouseEnter={(e) => {
|
||||
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
|
||||
editorManager.setHighlightRange([
|
||||
topLevelRange(obj?.start || 0, obj.end),
|
||||
])
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onMouseMove={(e) => {
|
||||
e.stopPropagation()
|
||||
editorManager.setHighlightRange([[obj?.start || 0, obj.end, true]])
|
||||
editorManager.setHighlightRange([
|
||||
topLevelRange(obj?.start || 0, obj.end),
|
||||
])
|
||||
}}
|
||||
onClick={(e) => {
|
||||
const range: [number, number, boolean] = [
|
||||
obj?.start || 0,
|
||||
obj.end || 0,
|
||||
true,
|
||||
]
|
||||
const range = topLevelRange(obj?.start || 0, obj.end || 0)
|
||||
const idInfo = codeToIdSelections([
|
||||
{ codeRef: codeRefFromRange(range, kclManager.ast) },
|
||||
])[0]
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Combobox } from '@headlessui/react'
|
||||
import { useSelector } from '@xstate/react'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { AnyStateMachine, StateFrom } from 'xstate'
|
||||
|
||||
@ -23,7 +23,7 @@ function CommandArgOptionInput({
|
||||
placeholder?: string
|
||||
}) {
|
||||
const actorContext = useSelector(arg.machineActor, contextSelector)
|
||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||
const commandBarState = useCommandBarState()
|
||||
const resolvedOptions = useMemo(
|
||||
() =>
|
||||
typeof arg.options === 'function'
|
||||
@ -129,11 +129,13 @@ function CommandArgOptionInput({
|
||||
<label
|
||||
htmlFor="option-input"
|
||||
className="capitalize px-2 py-1 rounded-l bg-chalkboard-100 dark:bg-chalkboard-80 text-chalkboard-10 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80"
|
||||
data-testid="cmd-bar-arg-name"
|
||||
>
|
||||
{argName}
|
||||
</label>
|
||||
<Combobox.Input
|
||||
id="option-input"
|
||||
data-testid="cmd-bar-arg-value"
|
||||
ref={inputRef}
|
||||
onChange={(event) =>
|
||||
!event.target.disabled && setQuery(event.target.value)
|
||||
@ -141,7 +143,7 @@ function CommandArgOptionInput({
|
||||
className="flex-grow px-2 py-1 border-b border-b-chalkboard-100 dark:border-b-chalkboard-80 !bg-transparent focus:outline-none"
|
||||
onKeyDown={(event) => {
|
||||
if (event.metaKey && event.key === 'k')
|
||||
commandBarSend({ type: 'Close' })
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
if (event.key === 'Backspace' && !event.currentTarget.value) {
|
||||
stepBack()
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Dialog, Popover, Transition } from '@headlessui/react'
|
||||
import { Fragment, useEffect } from 'react'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import CommandBarArgument from './CommandBarArgument'
|
||||
import CommandComboBox from '../CommandComboBox'
|
||||
import CommandBarReview from './CommandBarReview'
|
||||
@ -8,12 +7,13 @@ import { useLocation } from 'react-router-dom'
|
||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||
import { CustomIcon } from 'components/CustomIcon'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine'
|
||||
|
||||
export const COMMAND_PALETTE_HOTKEY = 'mod+k'
|
||||
|
||||
export const CommandBar = () => {
|
||||
const { pathname } = useLocation()
|
||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||
const commandBarState = useCommandBarState()
|
||||
const {
|
||||
context: { selectedCommand, currentArgument, commands },
|
||||
} = commandBarState
|
||||
@ -22,16 +22,17 @@ export const CommandBar = () => {
|
||||
|
||||
// Close the command bar when navigating
|
||||
useEffect(() => {
|
||||
commandBarSend({ type: 'Close' })
|
||||
if (commandBarState.matches('Closed')) return
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
}, [pathname])
|
||||
|
||||
// Hook up keyboard shortcuts
|
||||
useHotkeyWrapper([COMMAND_PALETTE_HOTKEY], () => {
|
||||
if (commandBarState.context.commands.length === 0) return
|
||||
if (commandBarState.matches('Closed')) {
|
||||
commandBarSend({ type: 'Open' })
|
||||
commandBarActor.send({ type: 'Open' })
|
||||
} else {
|
||||
commandBarSend({ type: 'Close' })
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
}
|
||||
})
|
||||
|
||||
@ -51,14 +52,14 @@ export const CommandBar = () => {
|
||||
...entries[entries.length - 1][1],
|
||||
}
|
||||
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Edit argument',
|
||||
data: {
|
||||
arg: currentArg,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
commandBarSend({ type: 'Deselect command' })
|
||||
commandBarActor.send({ type: 'Deselect command' })
|
||||
}
|
||||
} else {
|
||||
const entries = Object.entries(selectedCommand?.args || {})
|
||||
@ -67,9 +68,9 @@ export const CommandBar = () => {
|
||||
)
|
||||
|
||||
if (index === 0) {
|
||||
commandBarSend({ type: 'Deselect command' })
|
||||
commandBarActor.send({ type: 'Deselect command' })
|
||||
} else {
|
||||
commandBarSend({
|
||||
commandBarActor.send({
|
||||
type: 'Change current argument',
|
||||
data: {
|
||||
arg: { name: entries[index - 1][0], ...entries[index - 1][1] },
|
||||
@ -84,19 +85,20 @@ export const CommandBar = () => {
|
||||
show={!commandBarState.matches('Closed') || false}
|
||||
afterLeave={() => {
|
||||
if (selectedCommand?.onCancel) selectedCommand.onCancel()
|
||||
commandBarSend({ type: 'Clear' })
|
||||
commandBarActor.send({ type: 'Clear' })
|
||||
}}
|
||||
as={Fragment}
|
||||
>
|
||||
<WrapperComponent
|
||||
open={!commandBarState.matches('Closed') || isSelectionArgument}
|
||||
onClose={() => {
|
||||
commandBarSend({ type: 'Close' })
|
||||
commandBarActor.send({ type: 'Close' })
|
||||
}}
|
||||
className={
|
||||
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
|
||||
(isSelectionArgument ? 'pointer-events-none' : '')
|
||||
}
|
||||
data-testid="command-bar-wrapper"
|
||||
>
|
||||
<Transition.Child
|
||||
enter="duration-100 ease-out"
|
||||
@ -121,7 +123,7 @@ export const CommandBar = () => {
|
||||
)
|
||||
)}
|
||||
<button
|
||||
onClick={() => commandBarSend({ type: 'Close' })}
|
||||
onClick={() => commandBarActor.send({ type: 'Close' })}
|
||||
className="group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent"
|
||||
>
|
||||
<CustomIcon
|
||||
|