Compare commits
	
		
			222 Commits
		
	
	
		
			nightly-v2
			...
			pierremtb/
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6c78dbd4c8 | |||
| 059593372a | |||
| 1ba8c5af00 | |||
| 410b4e81eb | |||
| 30275d86cc | |||
| 39c40b2cde | |||
| 10789d9c3c | |||
| 67cc4f5835 | |||
| 2692f2b73a | |||
| 965cb18059 | |||
| a022b8ef6c | |||
| 907102a8fa | |||
| 4d24bf7c94 | |||
| 9a537da183 | |||
| 353eca110e | |||
| df81b76b8b | |||
| ac3f7ab712 | |||
| dac91d3b79 | |||
| 0698432abf | |||
| fb56820811 | |||
| fb37bb83a8 | |||
| f90811695d | |||
| 0592d3b5da | |||
| 31e4d60045 | |||
| c0817b00e4 | |||
| 4ea1d16fb6 | |||
| d049bf33e8 | |||
| 7b11047d07 | |||
| 5c1dfe0c8e | |||
| 412e9568f2 | |||
| 9be208e5e1 | |||
| 842ef5ede9 | |||
| 3f855d7bad | |||
| 0a1a6e50cf | |||
| f06873a0e2 | |||
| d4e955289c | |||
| c147a219f4 | |||
| 09025179f9 | |||
| 38513a1e25 | |||
| 521a593451 | |||
| 87c4e6c74e | |||
| 82cd106898 | |||
| e14cc4ace3 | |||
| 2a2a31d0ef | |||
| f2669223c5 | |||
| c3bc1fad6d | |||
| 96ff1dd55b | |||
| 82bd04631a | |||
| abec2d6d66 | |||
| 6089b1932a | |||
| 074fd2b5c7 | |||
| b2485b804c | |||
| e753082653 | |||
| 634745bb81 | |||
| e3660c75fc | |||
| ef61d10615 | |||
| c208e16c76 | |||
| 585ca7e80f | |||
| f7bae1d221 | |||
| 339de00e68 | |||
| 4f02e45da3 | |||
| 1908383f0e | |||
| 68204bb23d | |||
| 5438a987ab | |||
| fa3f934948 | |||
| 08e714080e | |||
| c0c5c790ca | |||
| 8b60f75220 | |||
| f91ad4331f | |||
| 59103a2118 | |||
| 9737c2550a | |||
| bf9d01a8dd | |||
| 702e322f90 | |||
| e82830754d | |||
| 7806377a5a | |||
| 859afa2fd8 | |||
| 0a5f3093fc | |||
| b65f7939f6 | |||
| c35dea5e07 | |||
| fc66d4745f | |||
| b313d26c2a | |||
| 00b94ead62 | |||
| df01c233e4 | |||
| b30a37a0b3 | |||
| 0531ea1ce9 | |||
| 5f9a4887c1 | |||
| da7dfa16d8 | |||
| 363ae10658 | |||
| ac4a6c84cf | |||
| c6fad2e2dc | |||
| 013cb10961 | |||
| 6261083cb1 | |||
| 82aefec34d | |||
| 679b65f643 | |||
| d64270d494 | |||
| c06b2b4029 | |||
| 8b8a2bc4e2 | |||
| af702ae1b2 | |||
| 83e72dafa3 | |||
| e417e60053 | |||
| 2b0ba37ed0 | |||
| 96174f3cf6 | |||
| aed62ff912 | |||
| ebc6b6460d | |||
| 91f0cfe467 | |||
| 9334d64608 | |||
| a2ff0aeceb | |||
| f05acf92cc | |||
| 4fa7d2d8c8 | |||
| 3e615dfdbc | |||
| c9860af29f | |||
| 670faac1e8 | |||
| 23a42f0195 | |||
| a77fa639f3 | |||
| 0a5ad7c95b | |||
| 4a654523d2 | |||
| 73a7e2bfd6 | |||
| ca09224c92 | |||
| 5cbd11cec8 | |||
| 28eb99f655 | |||
| eb0850fea9 | |||
| 029f76f273 | |||
| 28b5f7080c | |||
| 5b1dcfecd6 | |||
| f89d191425 | |||
| 2f4e4b62a8 | |||
| 5ebd5c8dbb | |||
| a9ceaf2678 | |||
| c8afd3399b | |||
| 5dda4828c6 | |||
| 72acab752c | |||
| 81df38ad1c | |||
| 0576a2bef1 | |||
| 4b2f6b4647 | |||
| 69edaa4183 | |||
| 2eb7c382bf | |||
| 38913ecb98 | |||
| debd06129f | |||
| d38bd342a0 | |||
| f026f10335 | |||
| 895d7ebc6d | |||
| 65edf17a44 | |||
| 0c2a0a8c07 | |||
| 97cef4d16c | |||
| 9358278f7b | |||
| a174e084d4 | |||
| df7246897a | |||
| 0c9f64dd7c | |||
| d2b9d3a058 | |||
| 7e54f08778 | |||
| d9c2dd376e | |||
| 275a2150e7 | |||
| 8b8feb8d68 | |||
| e21ef3f122 | |||
| 66834931aa | |||
| 06c1bcaf2e | |||
| fc7df7ecbe | |||
| 67cb7b33bb | |||
| ea74b94fac | |||
| 529833c63f | |||
| 92da86391a | |||
| e7cb390db4 | |||
| 8a66bbbdbd | |||
| 3c53babb50 | |||
| 474acb1c68 | |||
| 1c941112d7 | |||
| 6f1d718097 | |||
| 36957237c0 | |||
| c29be6e341 | |||
| 2193d563c5 | |||
| 570d159c29 | |||
| 713886b274 | |||
| 2aa4a01cb7 | |||
| 2048c26b9f | |||
| cbb8df5904 | |||
| bb67a9e9cf | |||
| b84d5951b7 | |||
| 1e5954e5ed | |||
| d58a147b7d | |||
| 96b06247a4 | |||
| 36d49b1bcb | |||
| 4748c2d1e0 | |||
| 698ce671df | |||
| a2330a0dbc | |||
| c882e34ea9 | |||
| 1ce3d8ccd0 | |||
| 15bedd56f4 | |||
| 746ebf80d1 | |||
| 02b249bd31 | |||
| 524fcb03ad | |||
| 3a9e0c72a8 | |||
| 5dc983ad7b | |||
| 81411033d7 | |||
| 30a24c8ae6 | |||
| 403cee5f16 | |||
| 14eeafb70a | |||
| f4ecd16ffa | |||
| 48380be480 | |||
| 80e32b337f | |||
| 9378d9862b | |||
| 1f515b712b | |||
| 372f2eebcc | |||
| e22a9edde8 | |||
| 75e3f843eb | |||
| f0136a5939 | |||
| 3d2e48732c | |||
| 7545b61b49 | |||
| d1be6d7b64 | |||
| 8ab24ceee7 | |||
| f163870b86 | |||
| 3fc707a2a4 | |||
| 238163d7db | |||
| bfccb79c1c | |||
| fe6d1f8119 | |||
| f496d94258 | |||
| 5d8f3f988a | |||
| 4f06524776 | |||
| d7fe827a9e | |||
| 049e487ac4 | |||
| 5bd89047b2 | |||
| 5822321f35 | |||
| 401dcf8152 | 
| @ -1,3 +1,3 @@ | |||||||
| [codespell] | [codespell] | ||||||
| ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,absolutey,atleast,ue,afterall | ignore-words-list: crate,everytime,inout,co-ordinate,ot,nwo,atleast,ue,afterall,ser | ||||||
| skip: **/target,node_modules,build,**/Cargo.lock,./docs/kcl/*.md,.yarn.lock,**/yarn.lock,./openapi/*.json,./src/lib/machine-api.d.ts | 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": [ |     "plugins": [ | ||||||
|       "css-modules", |       "css-modules", | ||||||
|  |       "jest", | ||||||
|  |       "jsx-a11y", | ||||||
|  |       "react", | ||||||
|  |       "react-hooks", | ||||||
|       "suggest-no-throw", |       "suggest-no-throw", | ||||||
|  |       "testing-library", | ||||||
|  |       "@typescript-eslint" | ||||||
|     ], |     ], | ||||||
|     "extends": [ |     "extends": [ | ||||||
|       "react-app", |       "plugin:css-modules/recommended", | ||||||
|       "react-app/jest", |       "plugin:jsx-a11y/recommended", | ||||||
|       "plugin:css-modules/recommended" |       "plugin:react-hooks/recommended" | ||||||
|     ], |     ], | ||||||
|     "rules": { |     "rules": { | ||||||
|       "@typescript-eslint/no-floating-promises": "error", |       "@typescript-eslint/no-floating-promises": "error", | ||||||
|       "@typescript-eslint/no-misused-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": [ |       "semi": [ | ||||||
|         "error", |         "error", | ||||||
|         "never" |         "never" | ||||||
| @ -25,6 +41,9 @@ | |||||||
|     "overrides": [ |     "overrides": [ | ||||||
|       { |       { | ||||||
|         "files": ["e2e/**/*.ts"], // Update the pattern based on your file structure |         "files": ["e2e/**/*.ts"], // Update the pattern based on your file structure | ||||||
|  |         "extends": [ | ||||||
|  |           "plugin:testing-library/react" | ||||||
|  |         ], | ||||||
|         "rules": { |         "rules": { | ||||||
|           "suggest-no-throw/suggest-no-throw": "off", |           "suggest-no-throw/suggest-no-throw": "off", | ||||||
|           "testing-library/prefer-screen-queries": "off", |           "testing-library/prefer-screen-queries": "off", | ||||||
| @ -33,6 +52,9 @@ | |||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         "files": ["src/**/*.test.ts"], |         "files": ["src/**/*.test.ts"], | ||||||
|  |         "extends": [ | ||||||
|  |           "plugin:testing-library/react" | ||||||
|  |         ], | ||||||
|         "rules": { |         "rules": { | ||||||
|           "suggest-no-throw/suggest-no-throw": "off", |           "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 | fi | ||||||
|  |  | ||||||
| retry=1 | retry=1 | ||||||
| max_retrys=4 | max_retrys=5 | ||||||
|  |  | ||||||
| # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues | # retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues | ||||||
| while [[ $retry -le $max_retrys ]]; do | while [[ $retry -le $max_retrys ]]; do | ||||||
|  | |||||||
							
								
								
									
										55
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -5,24 +5,37 @@ | |||||||
|  |  | ||||||
| version: 2 | version: 2 | ||||||
| updates: | updates: | ||||||
|     - package-ecosystem: 'npm' # See documentation for possible values |   - package-ecosystem: 'npm' # See documentation for possible values | ||||||
|       directory: '/' # Location of package manifests |     directories: | ||||||
|       schedule: |       - '/' | ||||||
|           interval: 'weekly' |       - '/packages/codemirror-lang-kcl/' | ||||||
|       reviewers: |       - '/packages/codemirror-lsp-client/' | ||||||
|           - franknoirot |     schedule: | ||||||
|           - irev-dev |       interval: weekly | ||||||
|     - package-ecosystem: 'github-actions' # See documentation for possible values |       day: monday | ||||||
|       directory: '/' # Location of package manifests |     reviewers: | ||||||
|       schedule: |       - franknoirot | ||||||
|           interval: 'weekly' |       - irev-dev | ||||||
|       reviewers: |   - package-ecosystem: 'github-actions' # See documentation for possible values | ||||||
|           - adamchalmers |     directory: '/' # Location of package manifests | ||||||
|           - jessfraz |     schedule: | ||||||
|     - package-ecosystem: 'cargo' # See documentation for possible values |       interval: weekly | ||||||
|       directory: '/src/wasm-lib/' # Location of package manifests |       day: monday | ||||||
|       schedule: |     reviewers: | ||||||
|           interval: 'weekly' |       - adamchalmers | ||||||
|       reviewers: |       - jessfraz | ||||||
|           - adamchalmers |   - package-ecosystem: 'cargo' # See documentation for possible values | ||||||
|           - jessfraz |     directory: '/src/wasm-lib/' # Location of package manifests | ||||||
|  |     schedule: | ||||||
|  |       interval: weekly | ||||||
|  |       day: monday | ||||||
|  |     reviewers: | ||||||
|  |       - adamchalmers | ||||||
|  |       - jessfraz | ||||||
|  |     groups: | ||||||
|  |       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 |       # Upload the WASM bundle as an artifact | ||||||
|       - uses: actions/upload-artifact@v3 |       - uses: actions/upload-artifact@v4 | ||||||
|         with: |         with: | ||||||
|           name: wasm-bundle |           name: wasm-bundle | ||||||
|           path: src/wasm-lib/pkg |           path: src/wasm-lib/pkg | ||||||
|  | |||||||
							
								
								
									
										9
									
								
								.github/workflows/build-apps.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -5,6 +5,7 @@ on: | |||||||
|   push: |   push: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |       - pierremtb/4088/create-file-url | ||||||
|     tags: |     tags: | ||||||
|       - 'v[0-9]+.[0-9]+.[0-9]+' |       - 'v[0-9]+.[0-9]+.[0-9]+' | ||||||
|   schedule: |   schedule: | ||||||
| @ -126,7 +127,13 @@ jobs: | |||||||
|           node-version-file: '.nvmrc' |           node-version-file: '.nvmrc' | ||||||
|           cache: 'yarn' # Set this to npm, yarn or pnpm. |           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 |       - 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 | ||||||
							
								
								
									
										8
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -18,7 +18,6 @@ permissions: | |||||||
| jobs: | jobs: | ||||||
|  |  | ||||||
|   check-rust-changes: |   check-rust-changes: | ||||||
|     if: github.event.pull_request.draft == false |  | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     outputs: |     outputs: | ||||||
|       rust-changed: ${{ steps.filter.outputs.rust }} |       rust-changed: ${{ steps.filter.outputs.rust }} | ||||||
| @ -35,7 +34,6 @@ jobs: | |||||||
|               - 'src/wasm-lib/**' |               - 'src/wasm-lib/**' | ||||||
|  |  | ||||||
|   electron: |   electron: | ||||||
|     if: github.event.pull_request.draft == false |  | ||||||
|     timeout-minutes: 60 |     timeout-minutes: 60 | ||||||
|     name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }} |     name: playwright:electron:${{ matrix.os }} ${{ matrix.shardIndex }} ${{ matrix.shardTotal }} | ||||||
|     strategy: |     strategy: | ||||||
| @ -129,9 +127,12 @@ jobs: | |||||||
|       shell: bash |       shell: bash | ||||||
|       run: yarn tron:package |       run: yarn tron:package | ||||||
|     - name: Run ubuntu/chrome snapshots |     - name: Run ubuntu/chrome snapshots | ||||||
|  |       if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }} | ||||||
|       shell: bash |       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: | |       run: | | ||||||
|         PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot  --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} |         PLATFORM=web yarn playwright test --config=playwright.config.ts --retries="3" --update-snapshots --grep=@snapshot  --shard=1/1 | ||||||
|       env: |       env: | ||||||
|         CI: true |         CI: true | ||||||
|         NODE_ENV: development |         NODE_ENV: development | ||||||
| @ -152,6 +153,7 @@ jobs: | |||||||
|       continue-on-error: true |       continue-on-error: true | ||||||
|       run: rm -r test-results |       run: rm -r test-results | ||||||
|     - name: check for changes |     - name: check for changes | ||||||
|  |       if: ${{ matrix.os == 'namespace-profile-ubuntu-8-cores' && matrix.shardIndex == 1 }} | ||||||
|       shell: bash |       shell: bash | ||||||
|       id: git-check |       id: git-check | ||||||
|       run: | |       run: | | ||||||
|  | |||||||
							
								
								
									
										40
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @ -337,13 +337,47 @@ For individual testing: | |||||||
| yarn test abstractSyntaxTree -t "unexpected closed curly brace" --silent=false | yarn test abstractSyntaxTree -t "unexpected closed curly brace" --silent=false | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro/) tests, in interactive mode by default. | Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testing Library E2E](https://testing-library.com/docs/react-testing-library/intro) tests, in interactive mode by default. | ||||||
|  |  | ||||||
| ### Rust tests | ### Rust tests | ||||||
|  |  | ||||||
| ```bash | **Dependencies** | ||||||
|  |  | ||||||
|  | - `KITTYCAD_API_TOKEN` | ||||||
|  | - `cargo-nextest` | ||||||
|  | - `just` | ||||||
|  |  | ||||||
|  | #### Setting KITTYCAD_API_TOKEN | ||||||
|  | Use the production zoo.dev token, set this environment variable before running the tests | ||||||
|  |  | ||||||
|  | #### Installing cargonextest | ||||||
|  |  | ||||||
|  | ``` | ||||||
| cd src/wasm-lib | cd src/wasm-lib | ||||||
| KITTYCAD_API_TOKEN=XXX cargo test -- --test-threads=1 | cargo search cargo-nextest | ||||||
|  | cargo install cargo-nextest | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | #### just | ||||||
|  | install [`just`](https://github.com/casey/just?tab=readme-ov-file#pre-built-binaries) | ||||||
|  |  | ||||||
|  | #### Running the tests | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # With just | ||||||
|  | # Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set | ||||||
|  | # Make sure you installed cargo-nextest | ||||||
|  | # Make sure you installed just | ||||||
|  | cd src/wasm-lib | ||||||
|  | just test | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Without just | ||||||
|  | # Make sure KITTYCAD_API_TOKEN=<prod zoo.dev token> is set | ||||||
|  | # Make sure you installed cargo-nextest | ||||||
|  | cd src/wasm-lib | ||||||
|  | export RUST_BRACKTRACE="full" && cargo nextest run --workspace --test-threads=1 | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Where `XXX` is an API token from the production engine (NOT the dev environment). | Where `XXX` is an API token from the production engine (NOT the dev environment). | ||||||
|  | |||||||
							
								
								
									
										42
									
								
								docs/kcl/circleThreePoint.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										43
									
								
								docs/kcl/helixRevolutions.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -35,6 +35,7 @@ layout: manual | |||||||
| * [`ceil`](kcl/ceil) | * [`ceil`](kcl/ceil) | ||||||
| * [`chamfer`](kcl/chamfer) | * [`chamfer`](kcl/chamfer) | ||||||
| * [`circle`](kcl/circle) | * [`circle`](kcl/circle) | ||||||
|  | * [`circleThreePoint`](kcl/circleThreePoint) | ||||||
| * [`close`](kcl/close) | * [`close`](kcl/close) | ||||||
| * [`cm`](kcl/cm) | * [`cm`](kcl/cm) | ||||||
| * [`cos`](kcl/cos) | * [`cos`](kcl/cos) | ||||||
| @ -47,11 +48,11 @@ layout: manual | |||||||
| * [`getOppositeEdge`](kcl/getOppositeEdge) | * [`getOppositeEdge`](kcl/getOppositeEdge) | ||||||
| * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) | * [`getPreviousAdjacentEdge`](kcl/getPreviousAdjacentEdge) | ||||||
| * [`helix`](kcl/helix) | * [`helix`](kcl/helix) | ||||||
|  | * [`helixRevolutions`](kcl/helixRevolutions) | ||||||
| * [`hole`](kcl/hole) | * [`hole`](kcl/hole) | ||||||
| * [`hollow`](kcl/hollow) | * [`hollow`](kcl/hollow) | ||||||
| * [`import`](kcl/import) | * [`import`](kcl/import) | ||||||
| * [`inch`](kcl/inch) | * [`inch`](kcl/inch) | ||||||
| * [`int`](kcl/int) |  | ||||||
| * [`lastSegX`](kcl/lastSegX) | * [`lastSegX`](kcl/lastSegX) | ||||||
| * [`lastSegY`](kcl/lastSegY) | * [`lastSegY`](kcl/lastSegY) | ||||||
| * [`legAngX`](kcl/legAngX) | * [`legAngX`](kcl/legAngX) | ||||||
| @ -80,6 +81,7 @@ layout: manual | |||||||
| * [`pi`](kcl/pi) | * [`pi`](kcl/pi) | ||||||
| * [`polar`](kcl/polar) | * [`polar`](kcl/polar) | ||||||
| * [`polygon`](kcl/polygon) | * [`polygon`](kcl/polygon) | ||||||
|  | * [`pop`](kcl/pop) | ||||||
| * [`pow`](kcl/pow) | * [`pow`](kcl/pow) | ||||||
| * [`profileStart`](kcl/profileStart) | * [`profileStart`](kcl/profileStart) | ||||||
| * [`profileStartX`](kcl/profileStartX) | * [`profileStartX`](kcl/profileStartX) | ||||||
|  | |||||||
| @ -4,6 +4,8 @@ excerpt: "Convert a number to an integer." | |||||||
| layout: manual | layout: manual | ||||||
| --- | --- | ||||||
|  |  | ||||||
|  | **WARNING:** This function is deprecated. | ||||||
|  |  | ||||||
| Convert a number to an integer. | Convert a number to an integer. | ||||||
|  |  | ||||||
| DEPRECATED use floor(), ceil(), or round(). | DEPRECATED use floor(), ceil(), or round(). | ||||||
|  | |||||||
							
								
								
									
										39
									
								
								docs/kcl/pop.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										29545
									
								
								docs/kcl/std.json
									
									
									
									
									
								
							
							
						
						| @ -1,19 +1,19 @@ | |||||||
| --- | --- | ||||||
| title: "AxisOrEdgeReference" | title: "Axis2dOrEdgeReference" | ||||||
| excerpt: "Axis or tagged edge." | excerpt: "A 2D axis or tagged edge." | ||||||
| layout: manual | layout: manual | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
| Axis or tagged edge. | A 2D axis or tagged edge. | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| **This schema accepts any of the following:** | **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" | title: "AxisAndOrigin2d" | ||||||
| excerpt: "Axis and origin." | excerpt: "A 2D axis and origin." | ||||||
| layout: manual | 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 | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ---- | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										23
									
								
								docs/kcl/types/CircleThreePointData.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,23 @@ | |||||||
|  | --- | ||||||
|  | title: "CircleThreePointData" | ||||||
|  | excerpt: "Data for drawing a 3-point circle" | ||||||
|  | layout: manual | ||||||
|  | --- | ||||||
|  |  | ||||||
|  | Data for drawing a 3-point circle | ||||||
|  |  | ||||||
|  | **Type:** `object` | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ## Properties | ||||||
|  |  | ||||||
|  | | Property | Type | Description | Required | | ||||||
|  | |----------|------|-------------|----------| | ||||||
|  | | `p1` |`[number, number]`| Point one for circle derivation. | No | | ||||||
|  | | `p2` |`[number, number]`| Point two for circle derivation. | No | | ||||||
|  | | `p3` |`[number, number]`| Point three for circle derivation. | 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" | title: "HelixData" | ||||||
| excerpt: "Data for helices." | excerpt: "Data for a helix." | ||||||
| layout: manual | layout: manual | ||||||
| --- | --- | ||||||
|  |  | ||||||
| Data for helices. | Data for a helix. | ||||||
|  |  | ||||||
| **Type:** `object` | **Type:** `object` | ||||||
|  |  | ||||||
| @ -19,6 +19,8 @@ Data for helices. | |||||||
| | `revolutions` |`number`| Number of revolutions. | No | | | `revolutions` |`number`| Number of revolutions. | No | | ||||||
| | `angleStart` |`number`| Start angle (in degrees). | No | | | `angleStart` |`number`| Start angle (in degrees). | No | | ||||||
| | `ccw` |`boolean`| Is the helix rotation counter clockwise? The default is `false`. | 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` | **Type:** `object` | ||||||
|  |  | ||||||
| @ -181,17 +180,10 @@ A plane. | |||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `type` |enum: [`Plane`](/docs/kcl/types/Plane)|  | No | | | `type` |enum: [`Plane`](/docs/kcl/types/Plane)|  | No | | ||||||
| | `id` |`string`| The id of the plane. | No | | | `value` |[`Plane`](/docs/kcl/types/Plane)| Any KCL value. | 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 | |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| A face. |  | ||||||
|  |  | ||||||
| **Type:** `object` | **Type:** `object` | ||||||
|  |  | ||||||
| @ -203,14 +195,8 @@ A face. | |||||||
|  |  | ||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `type` |enum: `Face`|  | No | | | `type` |enum: [`Face`](/docs/kcl/types/Face)|  | No | | ||||||
| | `id` |`string`| The id of the face. | No | | | `value` |[`Face`](/docs/kcl/types/Face)| Any KCL value. | 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 | |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| @ -246,7 +232,6 @@ A face. | |||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| An solid is a collection of extrude surfaces. |  | ||||||
|  |  | ||||||
| **Type:** `object` | **Type:** `object` | ||||||
|  |  | ||||||
| @ -259,14 +244,7 @@ An solid is a collection of extrude surfaces. | |||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `type` |enum: [`Solid`](/docs/kcl/types/Solid)|  | No | | | `type` |enum: [`Solid`](/docs/kcl/types/Solid)|  | No | | ||||||
| | `id` |`string`| The id of the solid. | No | | | `value` |[`Solid`](/docs/kcl/types/Solid)| Any KCL value. | 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 | |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ---- | ---- | ||||||
| @ -285,6 +263,22 @@ An solid is a collection of extrude surfaces. | |||||||
| | `value` |`[` [`Solid`](/docs/kcl/types/Solid) `]`|  | No | | | `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. | Data for an imported geometry. | ||||||
|  |  | ||||||
|  | |||||||
| @ -16,6 +16,6 @@ Data for a mirror. | |||||||
|  |  | ||||||
| | Property | Type | Description | Required | | | 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 | | | `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 | | | `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 | | | `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 | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`|  | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ Data for revolution surfaces. | |||||||
| | Property | Type | Description | Required | | | Property | Type | Description | Required | | ||||||
| |----------|------|-------------|----------| | |----------|------|-------------|----------| | ||||||
| | `angle` |`number` (**maximum:** 360.0) (**minimum:** -360.0)| Angle to revolve (in degrees). Default is 360. | No | | | `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 | | | `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 | | | `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 | | | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | ||||||
| | `tags` |`object`| Tag identifiers that have been declared in this sketch. | 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 | | | `__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 | | | `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 | | | `start` |[`BasePath`](/docs/kcl/types/BasePath)| The starting path. | No | | ||||||
| | `tags` |`object`| Tag identifiers that have been declared in this sketch. | 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 | | | `__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 | | | `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 | | | `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 | | | `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 | | | `__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 | | | `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 | | | `zAxis` |[`Point3d`](/docs/kcl/types/Point3d)| The z-axis (normal). | No | | ||||||
| | `solid` |[`Solid`](/docs/kcl/types/Solid)| The solid the face is on. | 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 | | | `__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 | | | `startCapId` |`string`| The id of the extrusion start cap | No | | ||||||
| | `endCapId` |`string`| The id of the extrusion end 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 | | | `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 | | | `__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 | | | `startCapId` |`string`| The id of the extrusion start cap | No | | ||||||
| | `endCapId` |`string`| The id of the extrusion end 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 | | | `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 | | | `__meta` |`[` [`Metadata`](/docs/kcl/types/Metadata) `]`| Metadata. | No | | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ Data for a sweep. | |||||||
|  |  | ||||||
| | Property | Type | Description | Required | | | 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 | | | `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 | | | `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.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||||
|       await expect(page.getByText('router-template-slate')).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 () => { |     await test.step('Opening the router-template project should load', async () => { | ||||||
|  | |||||||
| @ -1,7 +1,8 @@ | |||||||
| import { test, expect } from './zoo-test' | import { test, expect } from './zoo-test' | ||||||
|  | import * as fsp from 'fs/promises' | ||||||
| import { getUtils } from './test-utils' | import { executorInputPath, getUtils } from './test-utils' | ||||||
| import { KCL_DEFAULT_LENGTH } from 'lib/constants' | import { KCL_DEFAULT_LENGTH } from 'lib/constants' | ||||||
|  | import path from 'path' | ||||||
|  |  | ||||||
| test.describe('Command bar tests', () => { | test.describe('Command bar tests', () => { | ||||||
|   test('Extrude from command bar selects extrude line after', async ({ |   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 ({ |   test('Command bar can change a setting, and switch back and forth between arguments', async ({ | ||||||
|     page, |     page, | ||||||
|     homePage, |     homePage, | ||||||
| @ -345,4 +306,132 @@ test.describe('Command bar tests', () => { | |||||||
|     await arcToolCommand.click() |     await arcToolCommand.click() | ||||||
|     await expect(arcToolButton).toHaveAttribute('aria-pressed', 'true') |     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']) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
| }) | }) | ||||||
|  | |||||||
| @ -135,4 +135,27 @@ export class CmdBarFixture { | |||||||
|       await promptEditCommand.first().click() |       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 = { | export const fixtures = { | ||||||
|   cmdBar: async ({ page }: { page: Page }, use: any) => { |   cmdBar: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new CmdBarFixture(page)) |     await use(new CmdBarFixture(page)) | ||||||
|   }, |   }, | ||||||
|   editor: async ({ page }: { page: Page }, use: any) => { |   editor: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new EditorFixture(page)) |     await use(new EditorFixture(page)) | ||||||
|   }, |   }, | ||||||
|   toolbar: async ({ page }: { page: Page }, use: any) => { |   toolbar: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new ToolbarFixture(page)) |     await use(new ToolbarFixture(page)) | ||||||
|   }, |   }, | ||||||
|   scene: async ({ page }: { page: Page }, use: any) => { |   scene: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new SceneFixture(page)) |     await use(new SceneFixture(page)) | ||||||
|   }, |   }, | ||||||
|   homePage: async ({ page }: { page: Page }, use: any) => { |   homePage: async ({ page }: { page: Page }, use: any) => { | ||||||
|  |     // eslint-disable-next-line react-hooks/rules-of-hooks | ||||||
|     await use(new HomePageFixture(page)) |     await use(new HomePageFixture(page)) | ||||||
|   }, |   }, | ||||||
| } | } | ||||||
|  | |||||||
| @ -103,7 +103,7 @@ export class HomePageFixture { | |||||||
|       .toEqual(expectedState) |       .toEqual(expectedState) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   createAndGoToProject = async (projectTitle: string) => { |   createAndGoToProject = async (projectTitle = 'project-$nnn') => { | ||||||
|     await expect(this.projectSection).not.toHaveText('Loading your Projects...') |     await expect(this.projectSection).not.toHaveText('Loading your Projects...') | ||||||
|     await this.projectButtonNew.click() |     await this.projectButtonNew.click() | ||||||
|     await this.projectTextName.click() |     await this.projectTextName.click() | ||||||
|  | |||||||
| @ -36,7 +36,8 @@ type DragFromHandler = ( | |||||||
|  |  | ||||||
| export class SceneFixture { | export class SceneFixture { | ||||||
|   public page: Page |   public page: Page | ||||||
|  |   public streamWrapper!: Locator | ||||||
|  |   public loadingIndicator!: Locator | ||||||
|   private exeIndicator!: Locator |   private exeIndicator!: Locator | ||||||
|  |  | ||||||
|   constructor(page: Page) { |   constructor(page: Page) { | ||||||
| @ -64,6 +65,8 @@ export class SceneFixture { | |||||||
|     this.page = page |     this.page = page | ||||||
|  |  | ||||||
|     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') |     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') | ||||||
|  |     this.streamWrapper = page.getByTestId('stream') | ||||||
|  |     this.loadingIndicator = this.streamWrapper.getByTestId('loading') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   makeMouseHelpers = ( |   makeMouseHelpers = ( | ||||||
|  | |||||||
| @ -14,6 +14,9 @@ export class ToolbarFixture { | |||||||
|  |  | ||||||
|   extrudeButton!: Locator |   extrudeButton!: Locator | ||||||
|   loftButton!: Locator |   loftButton!: Locator | ||||||
|  |   sweepButton!: Locator | ||||||
|  |   filletButton!: Locator | ||||||
|  |   chamferButton!: Locator | ||||||
|   shellButton!: Locator |   shellButton!: Locator | ||||||
|   offsetPlaneButton!: Locator |   offsetPlaneButton!: Locator | ||||||
|   startSketchBtn!: Locator |   startSketchBtn!: Locator | ||||||
| @ -40,6 +43,9 @@ export class ToolbarFixture { | |||||||
|     this.page = page |     this.page = page | ||||||
|     this.extrudeButton = page.getByTestId('extrude') |     this.extrudeButton = page.getByTestId('extrude') | ||||||
|     this.loftButton = page.getByTestId('loft') |     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.shellButton = page.getByTestId('shell') | ||||||
|     this.offsetPlaneButton = page.getByTestId('plane-offset') |     this.offsetPlaneButton = page.getByTestId('plane-offset') | ||||||
|     this.startSketchBtn = page.getByTestId('sketch') |     this.startSketchBtn = page.getByTestId('sketch') | ||||||
| @ -57,6 +63,10 @@ export class ToolbarFixture { | |||||||
|     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') |     this.exeIndicator = page.getByTestId('model-state-indicator-execution-done') | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get logoLink() { | ||||||
|  |     return this.page.getByTestId('app-logo') | ||||||
|  |   } | ||||||
|  |  | ||||||
|   startSketchPlaneSelection = async () => |   startSketchPlaneSelection = async () => | ||||||
|     doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500) |     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 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 = [ | const loftPointAndClickCases = [ | ||||||
| @ -818,12 +829,6 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => { | |||||||
|         }) |         }) | ||||||
|         await selectSketches() |         await selectSketches() | ||||||
|         await cmdBar.progressCmdBar() |         await cmdBar.progressCmdBar() | ||||||
|         await cmdBar.expectState({ |  | ||||||
|           stage: 'review', |  | ||||||
|           headerArguments: { Selection: '2 faces' }, |  | ||||||
|           commandName: 'Loft', |  | ||||||
|         }) |  | ||||||
|         await cmdBar.progressCmdBar() |  | ||||||
|       }) |       }) | ||||||
|     } else { |     } else { | ||||||
|       await test.step(`Preselect the two sketches`, async () => { |       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 test.step(`Go through the command bar flow with preselected sketches`, async () => { | ||||||
|         await toolbar.loftButton.click() |         await toolbar.loftButton.click() | ||||||
|         await cmdBar.progressCmdBar() |         await cmdBar.progressCmdBar() | ||||||
|         await cmdBar.expectState({ |  | ||||||
|           stage: 'review', |  | ||||||
|           headerArguments: { Selection: '2 faces' }, |  | ||||||
|           commandName: 'Loft', |  | ||||||
|         }) |  | ||||||
|         await cmdBar.progressCmdBar() |  | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -851,6 +850,603 @@ loftPointAndClickCases.forEach(({ shouldPreselect }) => { | |||||||
|       }) |       }) | ||||||
|       await scene.expectPixelColor([89, 89, 89], testPoint, 15) |       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: 'profile', | ||||||
|  |       currentArgValue: '', | ||||||
|  |       headerArguments: { | ||||||
|  |         Path: '', | ||||||
|  |         Profile: '', | ||||||
|  |       }, | ||||||
|  |       highlightedHeaderArg: 'profile', | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await clickOnSketch1() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Sweep', | ||||||
|  |       currentArgKey: 'path', | ||||||
|  |       currentArgValue: '', | ||||||
|  |       headerArguments: { | ||||||
|  |         Path: '', | ||||||
|  |         Profile: '1 face', | ||||||
|  |       }, | ||||||
|  |       highlightedHeaderArg: 'path', | ||||||
|  |       stage: 'arguments', | ||||||
|  |     }) | ||||||
|  |     await clickOnSketch2() | ||||||
|  |     await cmdBar.expectState({ | ||||||
|  |       commandName: 'Sweep', | ||||||
|  |       headerArguments: { | ||||||
|  |         Path: '1 face', | ||||||
|  |         Profile: '1 face', | ||||||
|  |       }, | ||||||
|  |       stage: 'review', | ||||||
|  |     }) | ||||||
|  |     await cmdBar.progressCmdBar() | ||||||
|  |   }) | ||||||
|  |  | ||||||
|  |   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(`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 +1503,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { | |||||||
|         await clickOnCap() |         await clickOnCap() | ||||||
|         await page.waitForTimeout(500) |         await page.waitForTimeout(500) | ||||||
|         await cmdBar.progressCmdBar() |         await cmdBar.progressCmdBar() | ||||||
|  |         await page.waitForTimeout(500) | ||||||
|         await cmdBar.progressCmdBar() |         await cmdBar.progressCmdBar() | ||||||
|         await cmdBar.expectState({ |         await cmdBar.expectState({ | ||||||
|           stage: 'review', |           stage: 'review', | ||||||
| @ -927,6 +1524,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { | |||||||
|       await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => { |       await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => { | ||||||
|         await toolbar.shellButton.click() |         await toolbar.shellButton.click() | ||||||
|         await cmdBar.progressCmdBar() |         await cmdBar.progressCmdBar() | ||||||
|  |         await page.waitForTimeout(500) | ||||||
|         await cmdBar.progressCmdBar() |         await cmdBar.progressCmdBar() | ||||||
|         await cmdBar.expectState({ |         await cmdBar.expectState({ | ||||||
|           stage: 'review', |           stage: 'review', | ||||||
| @ -1008,6 +1606,7 @@ extrude001 = extrude(40, sketch001) | |||||||
|     await page.waitForTimeout(500) |     await page.waitForTimeout(500) | ||||||
|     await page.keyboard.up('Shift') |     await page.keyboard.up('Shift') | ||||||
|     await cmdBar.progressCmdBar() |     await cmdBar.progressCmdBar() | ||||||
|  |     await page.waitForTimeout(500) | ||||||
|     await cmdBar.progressCmdBar() |     await cmdBar.progressCmdBar() | ||||||
|     await cmdBar.expectState({ |     await cmdBar.expectState({ | ||||||
|       stage: 'review', |       stage: 'review', | ||||||
| @ -1030,4 +1629,162 @@ extrude001 = extrude(40, sketch001) | |||||||
|     }) |     }) | ||||||
|     await scene.expectPixelColor([49, 49, 49], testPoint, 15) |     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( | 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' }, |   { tag: '@electron' }, | ||||||
|   async ({ context, page }, testInfo) => { |   async ({ context, page }, testInfo) => { | ||||||
|     await context.folderSetupFn(async (dir) => { |     await context.folderSetupFn(async (dir) => { | ||||||
| @ -172,7 +172,7 @@ test( | |||||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() |       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||||
|       await expect(page.getByText('broken-code')).toBeVisible() |       await expect(page.getByText('broken-code')).toBeVisible() | ||||||
|       await expect(page.getByText('bracket')).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 () => { |     await test.step('opening broken code project should clear the scene and show the error', async () => { | ||||||
|       // Go back home. |       // Go back home. | ||||||
| @ -199,7 +199,7 @@ test( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| 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' }, |   { tag: '@electron' }, | ||||||
|   async ({ context, page }, testInfo) => { |   async ({ context, page }, testInfo) => { | ||||||
|     await context.folderSetupFn(async (dir) => { |     await context.folderSetupFn(async (dir) => { | ||||||
| @ -253,7 +253,7 @@ test( | |||||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() |       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||||
|       await expect(page.getByText('empty')).toBeVisible() |       await expect(page.getByText('empty')).toBeVisible() | ||||||
|       await expect(page.getByText('bracket')).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 () => { |     await test.step('opening empty code project should clear the scene', async () => { | ||||||
|       // Go back home. |       // Go back home. | ||||||
| @ -276,7 +276,7 @@ test( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| 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' }, |   { tag: '@electron' }, | ||||||
|   async ({ context, page }, testInfo) => { |   async ({ context, page }, testInfo) => { | ||||||
|     await context.folderSetupFn(async (dir) => { |     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( | test( | ||||||
| @ -1391,7 +1511,7 @@ extrude001 = extrude(200, sketch001)`) | |||||||
|     await page.getByTestId('app-logo').click() |     await page.getByTestId('app-logo').click() | ||||||
|  |  | ||||||
|     await expect( |     await expect( | ||||||
|       page.getByRole('button', { name: 'New project' }) |       page.getByRole('button', { name: 'Create project' }) | ||||||
|     ).toBeVisible() |     ).toBeVisible() | ||||||
|  |  | ||||||
|     for (let i = 1; i <= 10; i++) { |     for (let i = 1; i <= 10; i++) { | ||||||
| @ -1465,7 +1585,7 @@ test( | |||||||
|  |  | ||||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() |       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||||
|       await expect(page.getByText('router-template-slate')).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 should load the stream', async () => { | ||||||
| @ -1494,7 +1614,7 @@ test( | |||||||
|  |  | ||||||
|       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() |       await expect(page.getByRole('link', { name: 'bracket' })).toBeVisible() | ||||||
|       await expect(page.getByText('router-template-slate')).toBeVisible() |       await expect(page.getByText('router-template-slate')).toBeVisible() | ||||||
|       await expect(page.getByText('New Project')).toBeVisible() |       await expect(page.getByText('Create project')).toBeVisible() | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| ) | ) | ||||||
| @ -1885,3 +2005,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() |       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) { | async function clickExportButton(page: Page) { | ||||||
|  | |||||||
| @ -39,8 +39,8 @@ test.describe('Sketch tests', () => { | |||||||
|   ${startProfileAt1} |   ${startProfileAt1} | ||||||
|   |> arc({ |   |> arc({ | ||||||
|         radius = screwRadius, |         radius = screwRadius, | ||||||
|         angle_start = 0, |         angleStart = 0, | ||||||
|         angle_end = 360 |         angleEnd = 360 | ||||||
|       }, %) |       }, %) | ||||||
|    |    | ||||||
|     part001 = startSketchOn('XY') |     part001 = startSketchOn('XY') | ||||||
| @ -60,8 +60,8 @@ test.describe('Sketch tests', () => { | |||||||
|   |> yLine(wireOffset, %) |   |> yLine(wireOffset, %) | ||||||
|   |> arc({ |   |> arc({ | ||||||
|         radius = wireRadius, |         radius = wireRadius, | ||||||
|         angle_start = 0, |         angleStart = 0, | ||||||
|         angle_end = 180 |         angleEnd = 180 | ||||||
|       }, %) |       }, %) | ||||||
|   |> yLine(-wireOffset, %) |   |> yLine(-wireOffset, %) | ||||||
|   |> xLine(-width / 4, %) |   |> xLine(-width / 4, %) | ||||||
| @ -1323,3 +1323,85 @@ test.describe(`Sketching with offset planes`, () => { | |||||||
|     }) |     }) | ||||||
|   }) |   }) | ||||||
| }) | }) | ||||||
|  |  | ||||||
|  | // Regression test for https://github.com/KittyCAD/modeling-app/issues/4891 | ||||||
|  | test.describe(`Click based selection don't brick the app when clicked out of range after format using cache`, () => { | ||||||
|  |   test(`Can select a line that reformmed after entering sketch mode`, 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([0, 0], %) | ||||||
|  |   |> line([3.14, 3.14], %) | ||||||
|  |   |> arcTo({ | ||||||
|  |   end = [4, 2], | ||||||
|  |   interior = [1, 2] | ||||||
|  |   }, %) | ||||||
|  | ` | ||||||
|  |       ) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     await homePage.goToModelingScene() | ||||||
|  |     await scene.waitForExecutionDone() | ||||||
|  |  | ||||||
|  |     await test.step(`format the code`, async () => { | ||||||
|  |       // doesn't contain condensed version | ||||||
|  |       await editor.expectEditor.not.toContain( | ||||||
|  |         `arcTo({ end = [4, 2], interior = [1, 2] }, %)` | ||||||
|  |       ) | ||||||
|  |       // click the code to enter sketch mode | ||||||
|  |       await page.getByText(`arcTo`).click() | ||||||
|  |       // Format the code. | ||||||
|  |       await page.locator('#code-pane button:first-child').click() | ||||||
|  |       await page.locator('button:has-text("Format code")').click() | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     await test.step(`Ensure the code reformatted`, async () => { | ||||||
|  |       await editor.expectEditor.toContain( | ||||||
|  |         `arcTo({ end = [4, 2], interior = [1, 2] }, %)` | ||||||
|  |       ) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     const [arcClick, arcHover] = scene.makeMouseHelpers(699, 337) | ||||||
|  |     await test.step('Ensure we can hover the arc', async () => { | ||||||
|  |       await arcHover() | ||||||
|  |  | ||||||
|  |       // Check that the code is highlighted | ||||||
|  |       await editor.expectState({ | ||||||
|  |         activeLines: ["sketch001=startSketchOn('XZ')"], | ||||||
|  |         diagnostics: [], | ||||||
|  |         highlightedCode: 'arcTo({end = [4, 2], interior = [1, 2]}, %)', | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     await test.step('reset the selection', async () => { | ||||||
|  |       // Move the mouse out of the way | ||||||
|  |       await page.mouse.move(655, 337) | ||||||
|  |  | ||||||
|  |       await editor.expectState({ | ||||||
|  |         activeLines: ["sketch001=startSketchOn('XZ')"], | ||||||
|  |         diagnostics: [], | ||||||
|  |         highlightedCode: '', | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     await test.step('Ensure we can click the arc', async () => { | ||||||
|  |       await arcClick() | ||||||
|  |  | ||||||
|  |       // Check that the code is highlighted | ||||||
|  |       await editor.expectState({ | ||||||
|  |         activeLines: [], | ||||||
|  |         diagnostics: [], | ||||||
|  |         highlightedCode: 'arcTo({end = [4, 2], interior = [1, 2]}, %)', | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | }) | ||||||
|  | |||||||
| Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 49 KiB | 
| Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB | 
| Before Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB | 
| Before Width: | Height: | Size: 55 KiB | 
| Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 43 KiB | 
| Before Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 41 KiB | 
| Before Width: | Height: | Size: 39 KiB | 
| Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 35 KiB | 
| Before Width: | Height: | Size: 34 KiB | 
| Before Width: | Height: | Size: 57 KiB | 
| Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 42 KiB | 
| Before Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB | 
| Before Width: | Height: | Size: 52 KiB | 
| Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB | 
| Before Width: | Height: | Size: 65 KiB | 
| Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB | 
| Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 66 KiB | 
| Before Width: | Height: | Size: 62 KiB | 
| Before Width: | Height: | Size: 152 KiB | 
| Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 145 KiB | 
| Before Width: | Height: | Size: 130 KiB | 
| Before Width: | Height: | Size: 136 KiB | 
| Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 129 KiB | 
| Before Width: | Height: | Size: 112 KiB | 
| Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 38 KiB | 
| Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 54 KiB | 
| Before Width: | Height: | Size: 41 KiB | 
| Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 48 KiB | 
| Before Width: | Height: | Size: 40 KiB | 
| Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 53 KiB | 
| Before Width: | Height: | Size: 40 KiB | 
| Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 53 KiB | 
| Before Width: | Height: | Size: 44 KiB | 
| Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 47 KiB | 
| Before Width: | Height: | Size: 38 KiB | 
| @ -1078,7 +1078,7 @@ export async function createProject({ | |||||||
|   returnHome?: boolean |   returnHome?: boolean | ||||||
| }) { | }) { | ||||||
|   await test.step(`Create project and navigate to it`, async () => { |   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('textbox', { name: 'Name' }).fill(name) | ||||||
|     await page.getByRole('button', { name: 'Continue' }).click() |     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', () => { | test.describe('Testing in-app sample loading', () => { | ||||||
|   /** |   /** | ||||||
|    * Note this test implicitly depends on the KCL sample "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/car-wheel/car-wheel.kcl |    * 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 ({ |   test('Web: should overwrite current code, cannot create new file', async ({ | ||||||
|     editor, |     editor, | ||||||
| @ -29,8 +29,8 @@ test.describe('Testing in-app sample loading', () => { | |||||||
|  |  | ||||||
|     // Locators and constants |     // Locators and constants | ||||||
|     const newSample = { |     const newSample = { | ||||||
|       file: 'car-wheel' + FILE_EXT, |       file: 'a-parametric-bearing-pillow-block' + FILE_EXT, | ||||||
|       title: 'Car Wheel', |       title: 'A Parametric Bearing Pillow Block', | ||||||
|     } |     } | ||||||
|     const commandBarButton = page.getByRole('button', { name: 'Commands' }) |     const commandBarButton = page.getByRole('button', { name: 'Commands' }) | ||||||
|     const samplesCommandOption = page.getByRole('option', { |     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: |    * 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 |    * "a-parametric-bearing-pillow-block": https://github.com/KittyCAD/kcl-samples/blob/main/a-parametric-bearing-pillow-block/main.kcl | ||||||
|    * "gear-rack.kcl": https://github.com/KittyCAD/kcl-samples/blob/main/gear-rack/gear-rack.kcl |    * "gear-rack": https://github.com/KittyCAD/kcl-samples/blob/main/gear-rack/main.kcl | ||||||
|    */ |    */ | ||||||
|   test( |   test( | ||||||
|     'Desktop: should create new file by default, optionally overwrite', |     'Desktop: should create new file by default, optionally overwrite', | ||||||
| @ -93,8 +93,8 @@ test.describe('Testing in-app sample loading', () => { | |||||||
|  |  | ||||||
|       // Locators and constants |       // Locators and constants | ||||||
|       const sampleOne = { |       const sampleOne = { | ||||||
|         file: 'car-wheel' + FILE_EXT, |         file: 'a-parametric-bearing-pillow-block' + FILE_EXT, | ||||||
|         title: 'Car Wheel', |         title: 'A Parametric Bearing Pillow Block', | ||||||
|       } |       } | ||||||
|       const sampleTwo = { |       const sampleTwo = { | ||||||
|         file: 'gear-rack' + FILE_EXT, |         file: 'gear-rack' + FILE_EXT, | ||||||
|  | |||||||
| @ -389,25 +389,25 @@ test.describe('Testing selections', () => { | |||||||
|     await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({ |     await expect(u.codeLocator).toContainText(`sketch005 = startSketchOn({ | ||||||
|      plane = { |      plane = { | ||||||
|        origin = { x = 0, y = -50, z = 0 }, |        origin = { x = 0, y = -50, z = 0 }, | ||||||
|        x_axis = { x = 1, y = 0, z = 0 }, |        xAxis = { x = 1, y = 0, z = 0 }, | ||||||
|        y_axis = { x = 0, y = 0, z = 1 }, |        yAxis = { x = 0, y = 0, z = 1 }, | ||||||
|        z_axis = { x = 0, y = -1, z = 0 } |        zAxis = { x = 0, y = -1, z = 0 } | ||||||
|      } |      } | ||||||
|    })`) |    })`) | ||||||
|     await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({ |     await expect(u.codeLocator).toContainText(`sketch003 = startSketchOn({ | ||||||
|      plane = { |      plane = { | ||||||
|        origin = { x = 116.53, y = 0, z = 163.25 }, |        origin = { x = 116.53, y = 0, z = 163.25 }, | ||||||
|        x_axis = { x = -0.81, y = 0, z = 0.58 }, |        xAxis = { x = -0.81, y = 0, z = 0.58 }, | ||||||
|        y_axis = { x = 0, y = -1, z = 0 }, |        yAxis = { x = 0, y = -1, z = 0 }, | ||||||
|        z_axis = { x = 0.58, y = 0, z = 0.81 } |        zAxis = { x = 0.58, y = 0, z = 0.81 } | ||||||
|      } |      } | ||||||
|    })`) |    })`) | ||||||
|     await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({ |     await expect(u.codeLocator).toContainText(`sketch002 = startSketchOn({ | ||||||
|      plane = { |      plane = { | ||||||
|        origin = { x = -91.74, y = 0, z = 80.89 }, |        origin = { x = -91.74, y = 0, z = 80.89 }, | ||||||
|        x_axis = { x = -0.66, y = 0, z = -0.75 }, |        xAxis = { x = -0.66, y = 0, z = -0.75 }, | ||||||
|        y_axis = { x = 0, y = -1, z = 0 }, |        yAxis = { x = 0, y = -1, z = 0 }, | ||||||
|        z_axis = { x = -0.75, y = 0, z = 0.66 } |        zAxis = { x = -0.75, y = 0, z = 0.66 } | ||||||
|      } |      } | ||||||
|    })`) |    })`) | ||||||
|  |  | ||||||
| @ -906,53 +906,6 @@ test.describe('Testing selections', () => { | |||||||
|     ).not.toBeDisabled() |     ).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 removeAfterFirstParenthesis = (inputString: string) => { | ||||||
|     const index = inputString.indexOf('(') |     const index = inputString.indexOf('(') | ||||||
|     if (index !== -1) { |     if (index !== -1) { | ||||||
|  | |||||||
