Compare commits
	
		
			21 Commits
		
	
	
		
			kurt-model
			...
			cut-releas
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 04a77efae3 | |||
| 379cd1e067 | |||
| 8c3d438f6d | |||
| ac15049e2c | |||
| 9538ffb8ec | |||
| 466da6be55 | |||
| 55d1da226f | |||
| 2bfde64bf1 | |||
| 7cb9a2efd9 | |||
| 57e85d7fd0 | |||
| 38d5be001b | |||
| ca4a442cce | |||
| 46eef39d53 | |||
| dbc5f7b11f | |||
| 6797331c9d | |||
| cc80a2da3d | |||
| 54fb9c903a | |||
| e63597458a | |||
| e15c38fa23 | |||
| 906ca65611 | |||
| 805b9f48e5 | 
| @ -2,7 +2,9 @@ NODE_ENV=development | ||||
| DEV=true | ||||
| VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands | ||||
| VITE_KC_API_BASE_URL=https://api.dev.zoo.dev | ||||
| BASE_URL=https://api.dev.zoo.dev | ||||
| VITE_KC_SITE_BASE_URL=https://dev.zoo.dev | ||||
| VITE_KC_SKIP_AUTH=false | ||||
| VITE_KC_CONNECTION_TIMEOUT_MS=5000 | ||||
| VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local" | ||||
| # ONLY add your token in .env.development.local if you want to skip auth, otherwise this token takes precedence! | ||||
| #VITE_KC_DEV_TOKEN="your token from dev.zoo.dev should go in .env.development.local" | ||||
|  | ||||
							
								
								
									
										116
									
								
								.github/workflows/build-test-publish-apps.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -5,6 +5,7 @@ on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|       - cut-release-v0.25.1-updater-test-build-1 | ||||
|   release: | ||||
|     types: [published] | ||||
|   schedule: | ||||
| @ -13,8 +14,8 @@ on: | ||||
|   # Will checkout the last commit from the default branch (main as of 2023-10-04) | ||||
|  | ||||
| env: | ||||
|   CUT_RELEASE_PR: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }} | ||||
|   BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }} | ||||
|   CUT_RELEASE_PR: true | ||||
|   BUILD_RELEASE: true | ||||
|  | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | ||||
| @ -44,7 +45,7 @@ jobs: | ||||
|  | ||||
|       # TODO: see if we can fetch from main instead if no diff at src/wasm-lib | ||||
|       - name: Run build:wasm | ||||
|         run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}" | ||||
|         run: "yarn build:wasm" | ||||
|  | ||||
|       - name: Set nightly version | ||||
|         if: github.event_name == 'schedule' | ||||
| @ -81,8 +82,6 @@ jobs: | ||||
|       CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | ||||
|       CSC_KEYCHAIN: ${{ secrets.APPLE_SIGNING_IDENTITY }} | ||||
|       CSC_FOR_PULL_REQUEST: true | ||||
|       TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | ||||
|       TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | ||||
|       VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }} | ||||
|       VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} | ||||
|       WINDOWS_CERTIFICATE_THUMBPRINT: F4C9A52FF7BC26EE5E054946F6B11DEEA94C748D | ||||
| @ -142,37 +141,12 @@ jobs: | ||||
|       - name: List artifacts in out/ | ||||
|         run: ls -R out | ||||
|  | ||||
|       - name: Prepare the tauri update bundles (macOS) | ||||
|         if: ${{ env.BUILD_RELEASE && matrix.os == 'macos-14' }} | ||||
|         run: | | ||||
|           for ARCH in arm64 x64; do | ||||
|             TAURI_DIR=out/tauri/$VERSION/macos | ||||
|             TEMP_DIR=temp/$ARCH | ||||
|             mkdir -p $TAURI_DIR | ||||
|             mkdir -p $TEMP_DIR | ||||
|             unzip out/*-$ARCH-mac.zip -d $TEMP_DIR | ||||
|             tar -czvf "$TAURI_DIR/Zoo Modeling App-$ARCH.app.tar.gz" -C $TEMP_DIR "Zoo Modeling App.app"  | ||||
|             yarn tauri signer sign "$TAURI_DIR/Zoo Modeling App-$ARCH.app.tar.gz" | ||||
|           done | ||||
|           ls -R out | ||||
|  | ||||
|       - name: Prepare the tauri update bundles (Windows) | ||||
|         if: ${{ env.BUILD_RELEASE && matrix.os == 'windows-2022' }} | ||||
|         run: | | ||||
|           $env:TAURI_DIR="out/tauri/${env:VERSION}/nsis" | ||||
|           mkdir -p ${env:TAURI_DIR} | ||||
|           $env:OUT_FILE="${env:TAURI_DIR}/Zoo Modeling App_${env:VERSION_NO_V}_x64-setup.nsis.zip" | ||||
|           7z a -mm=Copy "${env:OUT_FILE}" ./out/*-x64-win.exe | ||||
|           yarn tauri signer sign "${env:OUT_FILE}" | ||||
|           ls -R out | ||||
|  | ||||
|       - uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           name: out-${{ matrix.os }} | ||||
|           path: | | ||||
|             out/Zoo*.* | ||||
|             out/latest*.yml | ||||
|             out/tauri | ||||
|  | ||||
|       # TODO: add the 'Build for Mac TestFlight (nightly)' stage back | ||||
|  | ||||
| @ -183,17 +157,15 @@ jobs: | ||||
|     runs-on: ubuntu-22.04 | ||||
|     permissions: | ||||
|       contents: write | ||||
|     if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }} | ||||
|     # if: ${{ github.event_name == 'release' || github.event_name == 'schedule' }} | ||||
|     needs: [prepare-files, build-apps] | ||||
|     env: | ||||
|       VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} | ||||
|       VERSION: ${{ github.event_name == 'schedule' && needs.prepare-files.outputs.version || format('v{0}', needs.prepare-files.outputs.version) }} | ||||
|       PUB_DATE: ${{ github.event_name == 'release' && github.event.release.created_at || github.event.repository.updated_at }} | ||||
|       NOTES: ${{ github.event_name == 'release' && github.event.release.body || format('Non-release build, commit {0}', github.sha) }} | ||||
|       BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app' }} | ||||
|       WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app' }} | ||||
|       BUCKET_DIR_TAURI: 'dl.kittycad.io/releases/modeling-app/tauri-compat' | ||||
|       WEBSITE_DIR_TAURI: 'dl.zoo.dev/releases/modeling-app/tauri-compat' | ||||
|       BUCKET_DIR: ${{ github.event_name == 'schedule' && 'dl.kittycad.io/releases/modeling-app/nightly' || 'dl.kittycad.io/releases/modeling-app/test/cut-release-v0.25.1-updater-test' }} | ||||
|       WEBSITE_DIR: ${{ github.event_name == 'schedule' && 'dl.zoo.dev/releases/modeling-app/nightly' || 'dl.zoo.dev/releases/modeling-app/test/cut-release-v0.25.1-updater-test' }} | ||||
|       URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
| @ -212,7 +184,7 @@ jobs: | ||||
|         with: | ||||
|           name: out-ubuntu-22.04 | ||||
|           path: out | ||||
|        | ||||
|  | ||||
|       - name: Generate the download static endpoint | ||||
|         run: | | ||||
|           RELEASE_DIR=https://${WEBSITE_DIR} | ||||
| @ -253,44 +225,6 @@ jobs: | ||||
|             }' > last_download.json | ||||
|             cat last_download.json | ||||
|  | ||||
|       - name: Generate the update static endpoint for tauri | ||||
|         run: | | ||||
|           TAURI_DIR=out/tauri/$VERSION | ||||
|           MAC_ARM64_SIG=`cat $TAURI_DIR/macos/*-arm64.app.tar.gz.sig` | ||||
|           MAC_X64_SIG=`cat $TAURI_DIR/macos/*-x64.app.tar.gz.sig` | ||||
|           WINDOWS_SIG=`cat $TAURI_DIR/nsis/*.nsis.zip.sig` | ||||
|           RELEASE_DIR=https://${WEBSITE_DIR_TAURI}/${VERSION} | ||||
|           jq --null-input \ | ||||
|             --arg version "${VERSION}" \ | ||||
|             --arg pub_date "${PUB_DATE}" \ | ||||
|             --arg notes "${NOTES}" \ | ||||
|             --arg mac_arm64_sig "$MAC_ARM64_SIG" \ | ||||
|             --arg mac_arm64_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}-arm64.app.tar.gz" \ | ||||
|             --arg mac_x64_sig "$MAC_X64_SIG" \ | ||||
|             --arg mac_x64_url "$RELEASE_DIR/macos/${{ env.URL_CODED_NAME }}-x64.app.tar.gz" \ | ||||
|             --arg windows_sig "$WINDOWS_SIG" \ | ||||
|             --arg windows_url "$RELEASE_DIR/nsis/${{ env.URL_CODED_NAME }}_${VERSION_NO_V}_x64-setup.nsis.zip" \ | ||||
|             '{ | ||||
|               "version": $version, | ||||
|               "pub_date": $pub_date, | ||||
|               "notes": $notes, | ||||
|               "platforms": { | ||||
|                 "darwin-x86_64": { | ||||
|                   "signature": $mac_x64_sig, | ||||
|                   "url": $mac_x64_url | ||||
|                 }, | ||||
|                 "darwin-aarch64": { | ||||
|                   "signature": $mac_arm64_sig, | ||||
|                   "url": $mac_arm64_url | ||||
|                 }, | ||||
|                 "windows-x86_64": { | ||||
|                   "signature": $windows_sig, | ||||
|                   "url": $windows_url | ||||
|                 } | ||||
|               } | ||||
|             }' > last_update.json | ||||
|             cat last_update.json | ||||
|  | ||||
|       - name: List artifacts | ||||
|         run: "ls -R out" | ||||
|  | ||||
| @ -312,13 +246,31 @@ jobs: | ||||
|           parent: false | ||||
|           destination: ${{ env.BUCKET_DIR }} | ||||
|  | ||||
|       # TODO: remove workaround introduced in https://github.com/KittyCAD/modeling-app/issues/3817 | ||||
|       - name: Upload release files to public bucket (test/electron-builder workaround) | ||||
|         uses: google-github-actions/upload-cloud-storage@v2.2.0 | ||||
|         with: | ||||
|           path: out | ||||
|           glob: 'Zoo*' | ||||
|           parent: false | ||||
|           destination: '${{ env.BUCKET_DIR }}/test/electron-builder' | ||||
|  | ||||
|       - name: Upload update endpoint to public bucket | ||||
|         uses: google-github-actions/upload-cloud-storage@v2.2.0 | ||||
|         with: | ||||
|           path: out | ||||
|           glob: 'latest*' | ||||
|           parent: false | ||||
|           destination: ${{ env.BUCKET_DIR }}  | ||||
|           destination: ${{ env.BUCKET_DIR }} | ||||
|  | ||||
|       # TODO: remove workaround introduced in https://github.com/KittyCAD/modeling-app/issues/3817 | ||||
|       - name: Upload update endpoint to public bucket (test/electron-builder workaround) | ||||
|         uses: google-github-actions/upload-cloud-storage@v2.2.0 | ||||
|         with: | ||||
|           path: out | ||||
|           glob: 'latest*' | ||||
|           parent: false | ||||
|           destination: '${{ env.BUCKET_DIR }}/test/electron-builder' | ||||
|  | ||||
|       - name: Upload download endpoint to public bucket | ||||
|         uses: google-github-actions/upload-cloud-storage@v2.2.0 | ||||
| @ -326,20 +278,6 @@ jobs: | ||||
|           path: last_download.json | ||||
|           destination: ${{ env.BUCKET_DIR }} | ||||
|  | ||||
|       - name: Upload release files to public bucket for tauri | ||||
|         uses: google-github-actions/upload-cloud-storage@v2.2.0 | ||||
|         with: | ||||
|           path: "out/tauri/${{ env.VERSION }}"  | ||||
|           glob: '*/Zoo*' | ||||
|           parent: false | ||||
|           destination: ${{ env.BUCKET_DIR_TAURI }}/${{ env.VERSION }} | ||||
|  | ||||
|       - name: Upload update endpoint to public bucket for tauri | ||||
|         uses: google-github-actions/upload-cloud-storage@v2.2.0 | ||||
|         with: | ||||
|           path: last_update.json | ||||
|           destination: ${{ env.BUCKET_DIR }} | ||||
|  | ||||
|       - name: Upload release files to Github | ||||
|         if: ${{ github.event_name == 'release' }} | ||||
|         uses: softprops/action-gh-release@v2 | ||||
|  | ||||
							
								
								
									
										3
									
								
								.github/workflows/cargo-clippy.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -28,6 +28,7 @@ jobs: | ||||
|         dir: ['src/wasm-lib'] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - uses: taiki-e/install-action@just | ||||
|       - name: Install latest rust | ||||
|         uses: actions-rs/toolchain@v1 | ||||
|         with: | ||||
| @ -41,7 +42,7 @@ jobs: | ||||
|       - name: Run clippy | ||||
|         run: | | ||||
|           cd "${{ matrix.dir }}" | ||||
|           cargo clippy --all --tests --benches -- -D warnings | ||||
|           just lint | ||||
|       # If this fails, run "cargo check" to update Cargo.lock, | ||||
|       # then add Cargo.lock to the PR. | ||||
|       - name: Check Cargo.lock doesn't need updating | ||||
|  | ||||
							
								
								
									
										19
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @ -351,25 +351,6 @@ PS: for the debug panel, the following JSON is useful for snapping the camera | ||||
|  | ||||
| </details> | ||||
|  | ||||
| ### Tauri e2e tests | ||||
|  | ||||
| #### Windows (local only until the CI edge version mismatch is fixed) | ||||
|  | ||||
| ``` | ||||
| yarn install | ||||
| yarn build:wasm-dev | ||||
| cp src/wasm-lib/pkg/wasm_lib_bg.wasm public | ||||
| yarn vite build --mode development | ||||
| yarn tauri build --debug -b | ||||
| $env:KITTYCAD_API_TOKEN="<YOUR_KITTYCAD_API_TOKEN>" | ||||
| $env:VITE_KC_API_BASE_URL="https://api.dev.zoo.dev" | ||||
| $env:E2E_TAURI_ENABLED="true" | ||||
| $env:TS_NODE_COMPILER_OPTIONS='{"module": "commonjs"}' | ||||
| $env:E2E_APPLICATION=".\src-tauri\target\debug\Zoo Modeling App.exe" | ||||
| Stop-Process -Name msedgedriver | ||||
| yarn wdio run wdio.conf.ts | ||||
| ``` | ||||
|  | ||||
| ## KCL | ||||
|  | ||||
| For how to contribute to KCL, [see our KCL README](https://github.com/KittyCAD/modeling-app/tree/main/src/wasm-lib/kcl). | ||||
|  | ||||
| @ -147713,7 +147713,8 @@ | ||||
|     "deprecated": false, | ||||
|     "examples": [ | ||||
|       "// Loft a square and a triangle.\nconst squareSketch = startSketchOn('XY')\n  |> startProfileAt([-100, 200], %)\n  |> line([200, 0], %)\n  |> line([0, -200], %)\n  |> line([-200, 0], %)\n  |> lineTo([profileStartX(%), profileStartY(%)], %)\n  |> close(%)\n\nconst triangleSketch = startSketchOn(offsetPlane('XY', 75))\n  |> startProfileAt([0, 125], %)\n  |> line([-15, -30], %)\n  |> line([30, 0], %)\n  |> lineTo([profileStartX(%), profileStartY(%)], %)\n  |> close(%)\n\nloft([squareSketch, triangleSketch])", | ||||
|       "// Loft a square, a circle, and another circle.\nconst squareSketch = startSketchOn('XY')\n  |> startProfileAt([-100, 200], %)\n  |> line([200, 0], %)\n  |> line([0, -200], %)\n  |> line([-200, 0], %)\n  |> lineTo([profileStartX(%), profileStartY(%)], %)\n  |> close(%)\n\nconst circleSketch0 = startSketchOn(offsetPlane('XY', 75))\n  |> circle([0, 100], 50, %)\n\nconst circleSketch1 = startSketchOn(offsetPlane('XY', 150))\n  |> circle([0, 100], 20, %)\n\nloft([\n  squareSketch,\n  circleSketch0,\n  circleSketch1\n])" | ||||
|       "// Loft a square, a circle, and another circle.\nconst squareSketch = startSketchOn('XY')\n  |> startProfileAt([-100, 200], %)\n  |> line([200, 0], %)\n  |> line([0, -200], %)\n  |> line([-200, 0], %)\n  |> lineTo([profileStartX(%), profileStartY(%)], %)\n  |> close(%)\n\nconst circleSketch0 = startSketchOn(offsetPlane('XY', 75))\n  |> circle([0, 100], 50, %)\n\nconst circleSketch1 = startSketchOn(offsetPlane('XY', 150))\n  |> circle([0, 100], 20, %)\n\nloft([\n  squareSketch,\n  circleSketch0,\n  circleSketch1\n])", | ||||
|       "// Loft a square, a circle, and another circle with options.\nconst squareSketch = startSketchOn('XY')\n  |> startProfileAt([-100, 200], %)\n  |> line([200, 0], %)\n  |> line([0, -200], %)\n  |> line([-200, 0], %)\n  |> lineTo([profileStartX(%), profileStartY(%)], %)\n  |> close(%)\n\nconst circleSketch0 = startSketchOn(offsetPlane('XY', 75))\n  |> circle([0, 100], 50, %)\n\nconst circleSketch1 = startSketchOn(offsetPlane('XY', 150))\n  |> circle([0, 100], 20, %)\n\nloft([\n  squareSketch,\n  circleSketch0,\n  circleSketch1\n], {\n  // This can be set to override the automatically determined\n  // topological base curve, which is usually the first section encountered.\n  baseCurveIndex: 0,\n  // Attempt to approximate rational curves (such as arcs) using a bezier.\n  // This will remove banding around interpolations between arcs and non-arcs.\n  // It may produce errors in other scenarios Over time, this field won't be necessary.\n  bezApproximateRational: false,\n  // Tolerance for the loft operation.\n  tolerance: 0.000001,\n  // Degree of the interpolation. Must be greater than zero.\n  // For example, use 2 for quadratic, or 3 for cubic interpolation in\n  // the V direction. This defaults to 2, if not specified.\n  vDegree: 2\n})" | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|  | ||||
| @ -112,7 +112,8 @@ test.describe('when using the file tree to', () => { | ||||
|       }) | ||||
|  | ||||
|       const { | ||||
|         panesOpen, | ||||
|         openKclCodePanel, | ||||
|         openFilePanel, | ||||
|         createAndSelectProject, | ||||
|         pasteCodeInEditor, | ||||
|         createNewFileAndSelect, | ||||
| @ -124,9 +125,9 @@ test.describe('when using the file tree to', () => { | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       page.on('console', console.log) | ||||
|  | ||||
|       await panesOpen(['files', 'code']) | ||||
|  | ||||
|       await createAndSelectProject('project-000') | ||||
|       await openKclCodePanel() | ||||
|       await openFilePanel() | ||||
|       // File the main.kcl with contents | ||||
|       const kclCube = await fsp.readFile( | ||||
|         'src/wasm-lib/tests/executor/inputs/cube.kcl', | ||||
| @ -201,4 +202,78 @@ test.describe('when using the file tree to', () => { | ||||
|       await electronApp.close() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   test( | ||||
|     'loading small file, then large, then back to small', | ||||
|     { | ||||
|       tag: '@electron', | ||||
|     }, | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       const { page } = await setupElectron({ | ||||
|         testInfo, | ||||
|       }) | ||||
|  | ||||
|       const { | ||||
|         panesOpen, | ||||
|         createAndSelectProject, | ||||
|         pasteCodeInEditor, | ||||
|         createNewFile, | ||||
|         openDebugPanel, | ||||
|         closeDebugPanel, | ||||
|         expectCmdLog, | ||||
|       } = await getUtils(page, test) | ||||
|  | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       page.on('console', console.log) | ||||
|  | ||||
|       await panesOpen(['files', 'code']) | ||||
|       await createAndSelectProject('project-000') | ||||
|  | ||||
|       // Create a small file | ||||
|       const kclCube = await fsp.readFile( | ||||
|         'src/wasm-lib/tests/executor/inputs/cube.kcl', | ||||
|         'utf-8' | ||||
|       ) | ||||
|       // pasted into main.kcl | ||||
|       await pasteCodeInEditor(kclCube) | ||||
|  | ||||
|       // Create a large lego file | ||||
|       await createNewFile('lego') | ||||
|       const legoFile = page.getByRole('listitem').filter({ | ||||
|         has: page.getByRole('button', { name: 'lego.kcl' }), | ||||
|       }) | ||||
|       await expect(legoFile).toBeVisible({ timeout: 60_000 }) | ||||
|       await legoFile.click() | ||||
|       const kclLego = await fsp.readFile( | ||||
|         'src/wasm-lib/tests/executor/inputs/lego.kcl', | ||||
|         'utf-8' | ||||
|       ) | ||||
|       await pasteCodeInEditor(kclLego) | ||||
|       const mainFile = page.getByRole('listitem').filter({ | ||||
|         has: page.getByRole('button', { name: 'main.kcl' }), | ||||
|       }) | ||||
|  | ||||
|       // Open settings and enable the debug panel | ||||
|       await page | ||||
|         .getByRole('link', { | ||||
|           name: 'settings Settings', | ||||
|         }) | ||||
|         .click() | ||||
|       await page.locator('#showDebugPanel').getByText('OffOn').click() | ||||
|       await page.getByTestId('settings-close-button').click() | ||||
|  | ||||
|       await test.step('swap between small and large files', async () => { | ||||
|         await openDebugPanel() | ||||
|         // Previously created a file so we need to start back at main.kcl | ||||
|         await mainFile.click() | ||||
|         await expectCmdLog('[data-message-type="execution-done"]', 60_000) | ||||
|         // Click the large file | ||||
|         await legoFile.click() | ||||
|         // Once it is building, click back to the smaller file | ||||
|         await mainFile.click() | ||||
|         await expectCmdLog('[data-message-type="execution-done"]', 60_000) | ||||
|         await closeDebugPanel() | ||||
|       }) | ||||
|     } | ||||
|   ) | ||||
| }) | ||||
|  | ||||
| @ -548,13 +548,16 @@ export async function getUtils(page: Page, test_?: typeof test) { | ||||
|  | ||||
|     createNewFileAndSelect: async (name: string) => { | ||||
|       return test?.step(`Create a file named ${name}, select it`, async () => { | ||||
|         await openFilePanel(page) | ||||
|         await page.getByTestId('create-file-button').click() | ||||
|         await page.getByTestId('file-rename-field').fill(name) | ||||
|         await page.keyboard.press('Enter') | ||||
|         await page | ||||
|         const newFile = page | ||||
|           .locator('[data-testid="file-pane-scroll-container"] button') | ||||
|           .filter({ hasText: name }) | ||||
|           .click() | ||||
|  | ||||
|         await expect(newFile).toBeVisible() | ||||
|         await newFile.click() | ||||
|       }) | ||||
|     }, | ||||
|  | ||||
| @ -585,6 +588,15 @@ export async function getUtils(page: Page, test_?: typeof test) { | ||||
|       }) | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * @deprecated Sorry I don't have time to fix this right now, but runs like | ||||
|      * the one linked below show me that setting the open panes in this manner is not reliable. | ||||
|      * You can either set `openPanes` as a part of the same initScript we run in setupElectron/setup, | ||||
|      * or you can imperatively open the panes with functions like {openKclCodePanel} | ||||
|      * (or we can make a general openPane function that takes a paneId)., | ||||
|      * but having a separate initScript does not seem to work reliably. | ||||
|      * @link https://github.com/KittyCAD/modeling-app/actions/runs/10731890169/job/29762700806?pr=3807#step:20:19553 | ||||
|      */ | ||||
|     panesOpen: async (paneIds: PaneId[]) => { | ||||
|       return test?.step(`Setting ${paneIds} panes to be open`, async () => { | ||||
|         await page.addInitScript( | ||||
|  | ||||
| @ -288,7 +288,7 @@ test.describe('Testing settings', () => { | ||||
|       }) | ||||
|  | ||||
|       await test.step('Refresh the application and see project setting applied', async () => { | ||||
|         await page.reload() | ||||
|         await page.reload({ waitUntil: 'domcontentloaded' }) | ||||
|  | ||||
|         await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor) | ||||
|         await settingsCloseButton.click() | ||||
| @ -364,47 +364,48 @@ test.describe('Testing settings', () => { | ||||
|     async ({ browser: _ }, testInfo) => { | ||||
|       const { electronApp, page } = await setupElectron({ | ||||
|         testInfo, | ||||
|         folderSetupFn: async () => {}, | ||||
|         folderSetupFn: async (dir) => { | ||||
|           const bracketDir = join(dir, 'project-000') | ||||
|           await fsp.mkdir(bracketDir, { recursive: true }) | ||||
|           await fsp.copyFile( | ||||
|             executorInputPath('cube.kcl'), | ||||
|             join(bracketDir, 'main.kcl') | ||||
|           ) | ||||
|           await fsp.copyFile( | ||||
|             executorInputPath('cylinder.kcl'), | ||||
|             join(bracketDir, '2.kcl') | ||||
|           ) | ||||
|         }, | ||||
|       }) | ||||
|       const kclCube = await fsp.readFile(executorInputPath('cube.kcl'), 'utf-8') | ||||
|       const kclCylinder = await fsp.readFile( | ||||
|         executorInputPath('cylinder.kcl'), | ||||
|         'utf8' | ||||
|       ) | ||||
|  | ||||
|       const { | ||||
|         panesOpen, | ||||
|         createAndSelectProject, | ||||
|         pasteCodeInEditor, | ||||
|         clickPane, | ||||
|         createNewFileAndSelect, | ||||
|         openKclCodePanel, | ||||
|         openFilePanel, | ||||
|         waitForPageLoad, | ||||
|         selectFile, | ||||
|         editorTextMatches, | ||||
|       } = await getUtils(page, test) | ||||
|  | ||||
|       await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|       page.on('console', console.log) | ||||
|  | ||||
|       await panesOpen([]) | ||||
|  | ||||
|       await test.step('Precondition: No projects exist', async () => { | ||||
|       await test.step('Precondition: Open to second project file', async () => { | ||||
|         await expect(page.getByTestId('home-section')).toBeVisible() | ||||
|         const projectLinksPre = page.getByTestId('project-link') | ||||
|         await expect(projectLinksPre).toHaveCount(0) | ||||
|         await page.getByText('project-000').click() | ||||
|         await waitForPageLoad() | ||||
|         await openKclCodePanel() | ||||
|         await openFilePanel() | ||||
|         await editorTextMatches(kclCube) | ||||
|  | ||||
|         await selectFile('2.kcl') | ||||
|         await editorTextMatches(kclCylinder) | ||||
|       }) | ||||
|  | ||||
|       await createAndSelectProject('project-000') | ||||
|  | ||||
|       await clickPane('code') | ||||
|       const kclCube = await fsp.readFile( | ||||
|         'src/wasm-lib/tests/executor/inputs/cube.kcl', | ||||
|         'utf-8' | ||||
|       ) | ||||
|       await pasteCodeInEditor(kclCube) | ||||
|  | ||||
|       await clickPane('files') | ||||
|       await createNewFileAndSelect('2.kcl') | ||||
|  | ||||
|       const kclCylinder = await fsp.readFile( | ||||
|         'src/wasm-lib/tests/executor/inputs/cylinder.kcl', | ||||
|         'utf-8' | ||||
|       ) | ||||
|       await pasteCodeInEditor(kclCylinder) | ||||
|  | ||||
|       const settingsOpenButton = page.getByRole('link', { | ||||
|         name: 'settings Settings', | ||||
|       }) | ||||
| @ -412,6 +413,9 @@ test.describe('Testing settings', () => { | ||||
|  | ||||
|       await test.step('Open and close settings', async () => { | ||||
|         await settingsOpenButton.click() | ||||
|         await expect( | ||||
|           page.getByRole('heading', { name: 'Settings', exact: true }) | ||||
|         ).toBeVisible() | ||||
|         await settingsCloseButton.click() | ||||
|       }) | ||||
|  | ||||
|  | ||||
| @ -534,7 +534,7 @@ test.describe('Text-to-CAD tests', () => { | ||||
|  | ||||
|     // Ensure the final toast remains. | ||||
|     await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible() | ||||
|     await expect(page.getByText(`a 2x8 lego`)).not.toBeVisible() | ||||
|     await expect(page.getByText(`Prompt: "a 2x8 lego`)).not.toBeVisible() | ||||
|     await expect(page.getByText(`a 2x4 lego`)).toBeVisible() | ||||
|  | ||||
|     // Ensure you can copy the code for the final model. | ||||
| @ -690,40 +690,53 @@ test( | ||||
|   'Text-to-CAD functionality', | ||||
|   { tag: '@electron' }, | ||||
|   async ({ browserName }, testInfo) => { | ||||
|     const projectName = 'project-000' | ||||
|     const prompt = 'lego 2x4' | ||||
|     const textToCadFileName = 'lego-2x4.kcl' | ||||
|  | ||||
|     const { electronApp, page, dir } = await setupElectron({ testInfo }) | ||||
|     const fileExists = () => | ||||
|       fs.existsSync(join(dir, 'project-000', 'lego-2x4.kcl')) | ||||
|       fs.existsSync(join(dir, projectName, textToCadFileName)) | ||||
|  | ||||
|     const { createAndSelectProject, panesOpen } = await getUtils(page, test) | ||||
|     const { | ||||
|       createAndSelectProject, | ||||
|       openFilePanel, | ||||
|       openKclCodePanel, | ||||
|       waitForPageLoad, | ||||
|     } = await getUtils(page, test) | ||||
|  | ||||
|     await page.setViewportSize({ width: 1200, height: 500 }) | ||||
|  | ||||
|     await panesOpen(['code', 'files']) | ||||
|     // Locators | ||||
|     const projectMenuButton = page.getByRole('button', { name: projectName }) | ||||
|     const textToCadFileButton = page.getByRole('listitem').filter({ | ||||
|       has: page.getByRole('button', { name: textToCadFileName }), | ||||
|     }) | ||||
|     const textToCadComment = page.getByText( | ||||
|       `// Generated by Text-to-CAD: ${prompt}` | ||||
|     ) | ||||
|  | ||||
|     // Create and navigate to the project | ||||
|     await createAndSelectProject('project-000') | ||||
|  | ||||
|     // Wait for Start Sketch otherwise you will not have access Text-to-CAD command | ||||
|     await expect( | ||||
|       page.getByRole('button', { name: 'Start Sketch' }) | ||||
|     ).toBeEnabled({ | ||||
|       timeout: 20_000, | ||||
|     }) | ||||
|     await waitForPageLoad() | ||||
|     await openFilePanel() | ||||
|     await openKclCodePanel() | ||||
|  | ||||
|     await test.step(`Test file creation`, async () => { | ||||
|       await sendPromptFromCommandBar(page, 'lego 2x4') | ||||
|       await sendPromptFromCommandBar(page, prompt) | ||||
|       // File is considered created if it shows up in the Project Files pane | ||||
|       const file = page.getByRole('button', { name: 'lego-2x4.kcl' }) | ||||
|       await expect(file).toBeVisible({ timeout: 20_000 }) | ||||
|       await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 }) | ||||
|       expect(fileExists()).toBeTruthy() | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Test file navigation`, async () => { | ||||
|       const file = page.getByRole('button', { name: 'lego-2x4.kcl' }) | ||||
|       await file.click() | ||||
|       const kclComment = page.getByText('Lego 2x4 Brick') | ||||
|       await expect(projectMenuButton).toContainText('main.kcl') | ||||
|       await textToCadFileButton.click() | ||||
|       // File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane | ||||
|       await expect(kclComment).toBeVisible({ timeout: 20_000 }) | ||||
|       await expect(textToCadComment).toBeVisible({ timeout: 20_000 }) | ||||
|       await expect(projectMenuButton).toContainText(textToCadFileName) | ||||
|     }) | ||||
|  | ||||
|     await test.step(`Test file deletion on rejection`, async () => { | ||||
| @ -737,6 +750,8 @@ test( | ||||
|       ) | ||||
|       await expect(submittingToastMessage).toBeVisible() | ||||
|       expect(fileExists()).toBeFalsy() | ||||
|       // Confirm we've navigated back to the main.kcl file after deletion | ||||
|       await expect(projectMenuButton).toContainText('main.kcl') | ||||
|     }) | ||||
|  | ||||
|     await electronApp.close() | ||||
|  | ||||
| @ -79,5 +79,5 @@ linux: | ||||
|  | ||||
| publish: | ||||
|   - provider: generic | ||||
|     url: https://dl.zoo.dev/releases/modeling-app/test/electron-builder | ||||
|     url: https://dl.zoo.dev/releases/modeling-app/test/cut-release-v0.25.1-updater-test | ||||
|     channel: latest | ||||
|  | ||||
							
								
								
									
										1
									
								
								interface.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -30,7 +30,6 @@ export interface IElectronAPI { | ||||
|   join: typeof path.join | ||||
|   sep: typeof path.sep | ||||
|   rename: (prev: string, next: string) => typeof fs.rename | ||||
|   setBaseUrl: (value: string) => void | ||||
|   packageJson: { | ||||
|     name: string | ||||
|   } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "zoo-modeling-app", | ||||
|   "version": "0.25.0", | ||||
|   "version": "0.25.1", | ||||
|   "private": true, | ||||
|   "productName": "Zoo Modeling App", | ||||
|   "author": { | ||||
| @ -137,7 +137,6 @@ | ||||
|     "@iarna/toml": "^2.2.5", | ||||
|     "@lezer/generator": "^1.7.1", | ||||
|     "@playwright/test": "^1.46.1", | ||||
|     "@tauri-apps/cli": "^2.0.0-rc.9", | ||||
|     "@testing-library/jest-dom": "^5.14.1", | ||||
|     "@testing-library/react": "^15.0.2", | ||||
|     "@types/d3-force": "^3.0.10", | ||||
|  | ||||
							
								
								
									
										10
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						| @ -122,11 +122,11 @@ export function App() { | ||||
|         // Override the electron window draggable region behavior as well | ||||
|         // when the button is down in the stream | ||||
|         style={ | ||||
|           { | ||||
|             '-webkit-app-region': context.store?.buttonDownInStream | ||||
|               ? 'no-drag' | ||||
|               : '', | ||||
|           } as React.CSSProperties | ||||
|           isDesktop() && context.store?.buttonDownInStream | ||||
|             ? ({ | ||||
|                 '-webkit-app-region': 'no-drag', | ||||
|               } as React.CSSProperties) | ||||
|             : {} | ||||
|         } | ||||
|         project={{ project, file }} | ||||
|         enableMenu={true} | ||||
|  | ||||
| @ -20,6 +20,8 @@ import { | ||||
|   ToolbarItemResolved, | ||||
|   ToolbarModeName, | ||||
| } from 'lib/toolbar' | ||||
| import { isDesktop } from 'lib/isDesktop' | ||||
| import { openExternalBrowserIfDesktop } from 'lib/openWindow' | ||||
|  | ||||
| export function Toolbar({ | ||||
|   className = '', | ||||
| @ -288,6 +290,11 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({ | ||||
|   return ( | ||||
|     <Tooltip | ||||
|       inert={false} | ||||
|       wrapperStyle={ | ||||
|         isDesktop() | ||||
|           ? ({ '-webkit-app-region': 'no-drag' } as React.CSSProperties) | ||||
|           : {} | ||||
|       } | ||||
|       position="bottom" | ||||
|       wrapperClassName="!p-4 !pointer-events-auto" | ||||
|       contentClassName="!text-left text-wrap !text-xs !p-0 !pb-2 flex gap-2 !max-w-none !w-72 flex-col items-stretch" | ||||
| @ -337,6 +344,7 @@ const ToolbarItemTooltip = memo(function ToolbarItemContents({ | ||||
|               <li key={link.label} className="contents"> | ||||
|                 <a | ||||
|                   href={link.url} | ||||
|                   onClick={openExternalBrowserIfDesktop(link.url)} | ||||
|                   target="_blank" | ||||
|                   rel="noreferrer" | ||||
|                   className="flex items-center rounded-sm p-1 no-underline text-inherit hover:bg-primary/10 hover:text-primary dark:hover:bg-chalkboard-70 dark:hover:text-inherit" | ||||
|  | ||||
| @ -6,6 +6,9 @@ | ||||
|   grid-template-columns: 1fr auto 1fr; | ||||
|   user-select: none; | ||||
|   -webkit-user-select: none; | ||||
| } | ||||
|  | ||||
| .header.desktopApp { | ||||
|   /* Make the header act as a handle to drag the electron app window, | ||||
|    * per the electron docs: https://www.electronjs.org/docs/latest/tutorial/window-customization#set-custom-draggable-region | ||||
|    * all interactive elements opt-out of this behavior by default in src/index.css | ||||
|  | ||||
| @ -6,6 +6,7 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' | ||||
| import styles from './AppHeader.module.css' | ||||
| import { RefreshButton } from 'components/RefreshButton' | ||||
| import { CommandBarOpenButton } from './CommandBarOpenButton' | ||||
| import { isDesktop } from 'lib/isDesktop' | ||||
|  | ||||
| interface AppHeaderProps extends React.PropsWithChildren { | ||||
|   showToolbar?: boolean | ||||
| @ -32,7 +33,9 @@ export const AppHeader = ({ | ||||
|       className={ | ||||
|         'w-full grid ' + | ||||
|         styles.header + | ||||
|         ' overlaid-panes sticky top-0 z-20 px-2 items-start ' + | ||||
|         ` ${ | ||||
|           isDesktop() ? styles.desktopApp + ' ' : '' | ||||
|         }overlaid-panes sticky top-0 z-20 px-2 items-start ` + | ||||
|         className | ||||
|       } | ||||
|       style={style} | ||||
|  | ||||
| @ -12,6 +12,7 @@ interface TooltipProps extends React.PropsWithChildren { | ||||
|   position?: TooltipPosition | ||||
|   wrapperClassName?: string | ||||
|   contentClassName?: string | ||||
|   wrapperStyle?: React.CSSProperties | ||||
|   delay?: number | ||||
|   hoverOnly?: boolean | ||||
|   inert?: boolean | ||||
| @ -22,6 +23,7 @@ export default function Tooltip({ | ||||
|   position = 'top', | ||||
|   wrapperClassName: className, | ||||
|   contentClassName, | ||||
|   wrapperStyle = {}, | ||||
|   delay = 200, | ||||
|   hoverOnly = false, | ||||
|   inert = true, | ||||
| @ -36,7 +38,10 @@ export default function Tooltip({ | ||||
|       } ${styles.tooltipWrapper} ${hoverOnly ? '' : styles.withFocus} ${ | ||||
|         styles[position] | ||||
|       } ${className}`} | ||||
|       style={{ '--_delay': delay + 'ms' } as React.CSSProperties} | ||||
|       style={Object.assign( | ||||
|         { '--_delay': delay + 'ms' } as React.CSSProperties, | ||||
|         wrapperStyle | ||||
|       )} | ||||
|     > | ||||
|       <div className={`rounded ${styles.tooltip} ${contentClassName || ''}`}> | ||||
|         {children} | ||||
|  | ||||
| @ -8,7 +8,7 @@ import { moveValueIntoNewVariable } from 'lang/modifyAst' | ||||
| import { isNodeSafeToReplace } from 'lang/queryAst' | ||||
| import { useEffect, useState } from 'react' | ||||
| import { useModelingContext } from './useModelingContext' | ||||
| import { PathToNode, SourceRange, parse, recast } from 'lang/wasm' | ||||
| import { PathToNode, SourceRange } from 'lang/wasm' | ||||
| import { useKclContext } from 'lang/KclProvider' | ||||
|  | ||||
| export const getVarNameModal = createSetVarNameModal(SetVarNameModal) | ||||
| @ -23,8 +23,7 @@ export function useConvertToVariable(range?: SourceRange) { | ||||
|   }, [enable]) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const parsed = parse(recast(ast)) | ||||
|     if (trap(parsed)) return | ||||
|     const parsed = ast | ||||
|  | ||||
|     const meta = isNodeSafeToReplace( | ||||
|       parsed, | ||||
|  | ||||
| @ -50,6 +50,14 @@ body.dark { | ||||
|   @apply text-chalkboard-10; | ||||
| } | ||||
|  | ||||
| @media (prefers-color-scheme: dark) { | ||||
|   body, | ||||
|   .body-bg, | ||||
|   .dark .body-bg { | ||||
|     @apply bg-chalkboard-100; | ||||
|   } | ||||
| } | ||||
|  | ||||
| select { | ||||
|   @apply bg-chalkboard-20; | ||||
| } | ||||
| @ -287,32 +295,11 @@ code { | ||||
| } | ||||
|  | ||||
| @layer utilities { | ||||
|   /* Modified from the very helpful https://www.transition.style/#in:circle:hesitate */ | ||||
|   @keyframes circle-in-hesitate { | ||||
|     0% { | ||||
|       clip-path: circle( | ||||
|         var(--circle-size-start, 0%) at var(--circle-x, 50%) | ||||
|           var(--circle-y, 50%) | ||||
|       ); | ||||
|     } | ||||
|     40% { | ||||
|       clip-path: circle( | ||||
|         var(--circle-size-mid, 40%) at var(--circle-x, 50%) var(--circle-y, 50%) | ||||
|       ); | ||||
|     } | ||||
|     100% { | ||||
|       clip-path: circle( | ||||
|         var(--circle-size-end, 125%) at var(--circle-x, 50%) | ||||
|           var(--circle-y, 50%) | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .in-circle-hesitate { | ||||
|     animation: var(--circle-duration, 2.5s) | ||||
|       var(--circle-timing, cubic-bezier(0.25, 1, 0.3, 1)) circle-in-hesitate | ||||
|       both; | ||||
|   } | ||||
|   /*  | ||||
|     This is where your own custom Tailwind utility classes can go, | ||||
|     which lets you use them with @apply in your CSS, and get  | ||||
|     autocomplete in classNames in your JSX. | ||||
|   */ | ||||
| } | ||||
|  | ||||
| #code-mirror-override .cm-scroller, | ||||
|  | ||||
| @ -8,7 +8,6 @@ import { | ||||
|   parseProjectSettings, | ||||
| } from 'lang/wasm' | ||||
| import { | ||||
|   DEFAULT_HOST, | ||||
|   PROJECT_ENTRYPOINT, | ||||
|   PROJECT_FOLDER, | ||||
|   PROJECT_SETTINGS_FILE_NAME, | ||||
| @ -556,28 +555,6 @@ export const getUser = async ( | ||||
|   token: string, | ||||
|   hostname: string | ||||
| ): Promise<Models['User_type']> => { | ||||
|   // Use the host passed in if it's set. | ||||
|   // Otherwise, use the default host. | ||||
|   const host = !hostname ? DEFAULT_HOST : hostname | ||||
|  | ||||
|   // Change the baseURL to the one we want. | ||||
|   let baseurl = host | ||||
|   if (!(host.indexOf('http://') === 0) && !(host.indexOf('https://') === 0)) { | ||||
|     baseurl = `https://${host}` | ||||
|     if (host.indexOf('localhost') === 0) { | ||||
|       baseurl = `http://${host}` | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Use kittycad library to fetch the user info from /user/me | ||||
|   if (baseurl !== DEFAULT_HOST) { | ||||
|     // The TypeScript generated library uses environment variables for this | ||||
|     // because it was intended for NodeJS. | ||||
|     // Needs to stay like this because window.electron.kittycad needs it | ||||
|     // internally. | ||||
|     window.electron.setBaseUrl(baseurl) | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     const user = await window.electron.kittycad('users.get_user_self', { | ||||
|       client: { token }, | ||||
|  | ||||
| @ -5,7 +5,7 @@ import { | ||||
|   kclManager, | ||||
|   sceneEntitiesManager, | ||||
| } from 'lib/singletons' | ||||
| import { CallExpression, SourceRange, Expr, parse, recast } from 'lang/wasm' | ||||
| import { CallExpression, SourceRange, Expr, parse } from 'lang/wasm' | ||||
| import { ModelingMachineEvent } from 'machines/modelingMachine' | ||||
| import { uuidv4 } from 'lib/utils' | ||||
| import { EditorSelection, SelectionRange } from '@codemirror/state' | ||||
| @ -300,8 +300,7 @@ export function processCodeMirrorRanges({ | ||||
| } | ||||
|  | ||||
| function updateSceneObjectColors(codeBasedSelections: Selection[]) { | ||||
|   const updated = parse(recast(kclManager.ast)) | ||||
|   if (err(updated)) return | ||||
|   const updated = kclManager.ast | ||||
|  | ||||
|   Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => { | ||||
|     if ( | ||||
|  | ||||
| @ -129,12 +129,16 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = { | ||||
|         id: 'loft', | ||||
|         onClick: () => console.error('Loft not yet implemented'), | ||||
|         icon: 'loft', | ||||
|         status: 'unavailable', | ||||
|         status: 'kcl-only', | ||||
|         title: 'Loft', | ||||
|         hotkey: 'L', | ||||
|         description: | ||||
|           'Create a 3D body by blending between two or more sketches.', | ||||
|         links: [ | ||||
|           { | ||||
|             label: 'KCL docs', | ||||
|             url: 'https://zoo.dev/docs/kcl/loft', | ||||
|           }, | ||||
|           { | ||||
|             label: 'GitHub discussion', | ||||
|             url: 'https://github.com/KittyCAD/modeling-app/discussions/613', | ||||
|  | ||||
							
								
								
									
										10
									
								
								src/main.ts
									
									
									
									
									
								
							
							
						
						| @ -2,7 +2,14 @@ | ||||
| // template that ElectronJS provides. | ||||
|  | ||||
| import dotenv from 'dotenv' | ||||
| import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron' | ||||
| import { | ||||
|   app, | ||||
|   BrowserWindow, | ||||
|   ipcMain, | ||||
|   dialog, | ||||
|   shell, | ||||
|   nativeTheme, | ||||
| } from 'electron' | ||||
| import path from 'path' | ||||
| import { Issuer } from 'openid-client' | ||||
| import { Bonjour, Service } from 'bonjour-service' | ||||
| @ -75,6 +82,7 @@ const createWindow = (filePath?: string): BrowserWindow => { | ||||
|     icon: path.resolve(process.cwd(), 'assets', 'icon.png'), | ||||
|     frame: os.platform() !== 'darwin', | ||||
|     titleBarStyle: 'hiddenInset', | ||||
|     backgroundColor: nativeTheme.shouldUseDarkColors ? '#1C1C1C' : '#FCFCFC', | ||||
|   }) | ||||
|  | ||||
|   // and load the index.html of the app. | ||||
|  | ||||
| @ -93,9 +93,6 @@ contextBridge.exposeInMainWorld('electron', { | ||||
|     isWindows, | ||||
|     isLinux, | ||||
|   }, | ||||
|   // IMPORTANT NOTE: kittycad.ts reads process.env.BASE_URL. But there is | ||||
|   // no way to set it across the bridge boundary. We need to make it a command. | ||||
|   setBaseUrl: (value: string) => (process.env.BASE_URL = value), | ||||
|   process: { | ||||
|     // Setter/getter has to be created because | ||||
|     // these are read-only over the boundary. | ||||
|  | ||||
| @ -58,19 +58,23 @@ const SignIn = () => { | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <main className="bg-primary h-screen grid place-items-stretch m-0 p-2"> | ||||
|     <main | ||||
|       className="bg-primary h-screen grid place-items-stretch m-0 p-2" | ||||
|       style={ | ||||
|         isDesktop() | ||||
|           ? ({ | ||||
|               '-webkit-app-region': 'drag', | ||||
|             } as CSSProperties) | ||||
|           : {} | ||||
|       } | ||||
|     > | ||||
|       <div | ||||
|         style={ | ||||
|           { | ||||
|             height: 'calc(100vh - 16px)', | ||||
|             '--circle-x': '14%', | ||||
|             '--circle-y': '12%', | ||||
|             '--circle-size-mid': '15%', | ||||
|             '--circle-size-end': '200%', | ||||
|             '--circle-timing': 'cubic-bezier(0.25, 1, 0.4, 0.9)', | ||||
|           } as CSSProperties | ||||
|           isDesktop() | ||||
|             ? ({ '-webkit-app-region': 'no-drag' } as CSSProperties) | ||||
|             : {} | ||||
|         } | ||||
|         className="in-circle-hesitate body-bg py-5 px-12 rounded-lg grid place-items-center overflow-y-auto" | ||||
|         className="body-bg py-5 px-12 rounded-lg grid place-items-center overflow-y-auto" | ||||
|       > | ||||
|         <div className="max-w-7xl grid gap-5 grid-cols-3 xl:grid-cols-4 xl:grid-rows-5"> | ||||
|           <div className="col-span-2 xl:col-span-3 xl:row-span-3 max-w-3xl mr-8 mb-8"> | ||||
| @ -194,7 +198,7 @@ const SignIn = () => { | ||||
|               <div className="flex gap-4 flex-wrap items-center"> | ||||
|                 <ActionButton | ||||
|                   Element="externalLink" | ||||
|                   to="https://zoo.dev/docs/kcl-samples/ball-bearing" | ||||
|                   to="https://zoo.dev/docs/kcl-samples/a-parametric-bearing-pillow-block" | ||||
|                   iconStart={{ icon: 'settings' }} | ||||
|                   className="border-chalkboard-30 dark:border-chalkboard-80" | ||||
|                 > | ||||
|  | ||||
							
								
								
									
										33
									
								
								src/wasm-lib/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						| @ -370,9 +370,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "clap" | ||||
| version = "4.5.16" | ||||
| version = "4.5.17" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" | ||||
| checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" | ||||
| dependencies = [ | ||||
|  "clap_builder", | ||||
|  "clap_derive", | ||||
| @ -380,9 +380,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "clap_builder" | ||||
| version = "4.5.15" | ||||
| version = "4.5.17" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" | ||||
| checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" | ||||
| dependencies = [ | ||||
|  "anstyle", | ||||
|  "clap_lex", | ||||
| @ -620,9 +620,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "dashmap" | ||||
| version = "6.0.1" | ||||
| version = "6.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" | ||||
| checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "crossbeam-utils", | ||||
| @ -1345,7 +1345,7 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "kcl-lib" | ||||
| version = "0.2.13" | ||||
| version = "0.2.14" | ||||
| dependencies = [ | ||||
|  "anyhow", | ||||
|  "approx", | ||||
| @ -1357,7 +1357,7 @@ dependencies = [ | ||||
|  "clap", | ||||
|  "convert_case", | ||||
|  "criterion", | ||||
|  "dashmap 6.0.1", | ||||
|  "dashmap 6.1.0", | ||||
|  "databake", | ||||
|  "derive-docs", | ||||
|  "expectorate", | ||||
| @ -1399,7 +1399,7 @@ dependencies = [ | ||||
|  "wasm-bindgen", | ||||
|  "wasm-bindgen-futures", | ||||
|  "web-sys", | ||||
|  "winnow 0.5.40", | ||||
|  "winnow", | ||||
|  "zip", | ||||
| ] | ||||
|  | ||||
| @ -2581,9 +2581,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "serde_json" | ||||
| version = "1.0.127" | ||||
| version = "1.0.128" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" | ||||
| checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" | ||||
| dependencies = [ | ||||
|  "indexmap 2.2.5", | ||||
|  "itoa", | ||||
| @ -3117,7 +3117,7 @@ dependencies = [ | ||||
|  "serde", | ||||
|  "serde_spanned", | ||||
|  "toml_datetime", | ||||
|  "winnow 0.6.18", | ||||
|  "winnow", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @ -3800,15 +3800,6 @@ version = "0.52.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" | ||||
|  | ||||
| [[package]] | ||||
| name = "winnow" | ||||
| version = "0.5.40" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" | ||||
| dependencies = [ | ||||
|  "memchr", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "winnow" | ||||
| version = "0.6.18" | ||||
|  | ||||
| @ -15,7 +15,7 @@ data-encoding = "2.6.0" | ||||
| gloo-utils = "0.2.0" | ||||
| kcl-lib = { path = "kcl" } | ||||
| kittycad.workspace = true | ||||
| serde_json = "1.0.127" | ||||
| serde_json = "1.0.128" | ||||
| tokio = { version = "1.40.0", features = ["sync"] } | ||||
| toml = "0.8.19" | ||||
| uuid = { version = "1.10.0", features = ["v4", "js", "serde"] } | ||||
|  | ||||
| @ -2,3 +2,6 @@ | ||||
| new-test name: | ||||
|     echo "kcl_test!(\"{{name}}\", {{name}});" >> tests/executor/visuals.rs | ||||
|     TWENTY_TWENTY=overwrite cargo nextest run --test executor -E 'test(=visuals::{{name}})' | ||||
|  | ||||
| lint: | ||||
|     cargo clippy --all --tests --benches -- -D warnings | ||||
|  | ||||
| @ -11,5 +11,5 @@ hyper = { version = "0.14.29", features = ["server"] } | ||||
| kcl-lib = { version = "0.2", path = "../kcl" } | ||||
| pico-args = "0.5.0" | ||||
| serde = { version = "1.0.209", features = ["derive"] } | ||||
| serde_json = "1.0.127" | ||||
| serde_json = "1.0.128" | ||||
| tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| [package] | ||||
| name = "kcl-lib" | ||||
| description = "KittyCAD Language implementation and tools" | ||||
| version = "0.2.13" | ||||
| version = "0.2.14" | ||||
| edition = "2021" | ||||
| license = "MIT" | ||||
| repository = "https://github.com/KittyCAD/modeling-app" | ||||
| @ -16,9 +16,9 @@ async-recursion = "1.1.1" | ||||
| async-trait = "0.1.82" | ||||
| base64 = "0.22.1" | ||||
| chrono = "0.4.38" | ||||
| clap = { version = "4.5.16", default-features = false, optional = true, features = ["std", "derive"] } | ||||
| clap = { version = "4.5.17", default-features = false, optional = true, features = ["std", "derive"] } | ||||
| convert_case = "0.6.0" | ||||
| dashmap = "6.0.1" | ||||
| dashmap = "6.1.0" | ||||
| databake = { version = "0.1.8", features = ["derive"] } | ||||
| derive-docs = { version = "0.1.26", path = "../derive-docs" } | ||||
| form_urlencoded = "1.2.1" | ||||
| @ -37,7 +37,7 @@ reqwest = { version = "0.11.26", default-features = false, features = ["stream", | ||||
| ropey = "1.6.1" | ||||
| schemars = { version = "0.8.17", features = ["impl_json_schema", "url", "uuid1"] } | ||||
| serde = { version = "1.0.209", features = ["derive"] } | ||||
| serde_json = "1.0.127" | ||||
| serde_json = "1.0.128" | ||||
| sha2 = "0.10.8" | ||||
| tabled = { version = "0.15.0", optional = true } | ||||
| thiserror = "1.0.63" | ||||
| @ -47,7 +47,7 @@ url = { version = "2.5.2", features = ["serde"] } | ||||
| urlencoding = "2.1.3" | ||||
| uuid = { version = "1.10.0", features = ["v4", "js", "serde"] } | ||||
| validator = { version = "0.18.1", features = ["derive"] } | ||||
| winnow = "0.5.40" | ||||
| winnow = "0.6.18" | ||||
| zip = { version = "2.0.0", default-features = false } | ||||
|  | ||||
| [target.'cfg(target_arch = "wasm32")'.dependencies] | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; | ||||
| use kcl_lib::test_server; | ||||
| use kcl_lib::{settings::types::UnitLength::Mm, test_server}; | ||||
| use tokio::runtime::Runtime; | ||||
|  | ||||
| pub fn bench_execute(c: &mut Criterion) { | ||||
| @ -13,26 +13,42 @@ pub fn bench_execute(c: &mut Criterion) { | ||||
|         // Configure Criterion.rs to detect smaller differences and increase sample size to improve | ||||
|         // precision and counteract the resulting noise. | ||||
|         group.sample_size(10); | ||||
|         group.bench_with_input(BenchmarkId::new("execute_", name), &code, |b, &s| { | ||||
|         group.bench_with_input(BenchmarkId::new("execute", name), &code, |b, &s| { | ||||
|             let rt = Runtime::new().unwrap(); | ||||
|  | ||||
|             // Spawn a future onto the runtime | ||||
|             b.iter(|| { | ||||
|                 rt.block_on(test_server::execute_and_snapshot( | ||||
|                     s, | ||||
|                     kcl_lib::settings::types::UnitLength::Mm, | ||||
|                 )) | ||||
|                 .unwrap(); | ||||
|                 rt.block_on(test_server::execute_and_snapshot(s, Mm)).unwrap(); | ||||
|             }); | ||||
|         }); | ||||
|         group.finish(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| criterion_group!(benches, bench_execute); | ||||
| pub fn bench_lego(c: &mut Criterion) { | ||||
|     let mut group = c.benchmark_group("executor_lego_pattern"); | ||||
|     // Configure Criterion.rs to detect smaller differences and increase sample size to improve | ||||
|     // precision and counteract the resulting noise. | ||||
|     group.sample_size(10); | ||||
|     // Create lego bricks with N x 10 bumps, where N is each element of `sizes`. | ||||
|     let sizes = vec![1, 2, 4]; | ||||
|     for size in sizes { | ||||
|         group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { | ||||
|             let rt = Runtime::new().unwrap(); | ||||
|             let code = LEGO_PROGRAM.replace("{{N}}", &size.to_string()); | ||||
|             // Spawn a future onto the runtime | ||||
|             b.iter(|| { | ||||
|                 rt.block_on(test_server::execute_and_snapshot(&code, Mm)).unwrap(); | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|     group.finish(); | ||||
| } | ||||
|  | ||||
| criterion_group!(benches, bench_lego, bench_execute); | ||||
| criterion_main!(benches); | ||||
|  | ||||
| const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl"); | ||||
| const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.kcl"); | ||||
| const SERVER_RACK_HEAVY_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-heavy.kcl"); | ||||
| const SERVER_RACK_LITE_PROGRAM: &str = include_str!("../../tests/executor/inputs/server-rack-lite.kcl"); | ||||
| const LEGO_PROGRAM: &str = include_str!("../../tests/executor/inputs/slow_lego.kcl.tmpl"); | ||||
|  | ||||
| @ -995,20 +995,20 @@ impl SketchSurface { | ||||
|     } | ||||
|     pub(crate) fn x_axis(&self) -> Point3d { | ||||
|         match self { | ||||
|             SketchSurface::Plane(plane) => plane.x_axis.clone(), | ||||
|             SketchSurface::Face(face) => face.x_axis.clone(), | ||||
|             SketchSurface::Plane(plane) => plane.x_axis, | ||||
|             SketchSurface::Face(face) => face.x_axis, | ||||
|         } | ||||
|     } | ||||
|     pub(crate) fn y_axis(&self) -> Point3d { | ||||
|         match self { | ||||
|             SketchSurface::Plane(plane) => plane.y_axis.clone(), | ||||
|             SketchSurface::Face(face) => face.y_axis.clone(), | ||||
|             SketchSurface::Plane(plane) => plane.y_axis, | ||||
|             SketchSurface::Face(face) => face.y_axis, | ||||
|         } | ||||
|     } | ||||
|     pub(crate) fn z_axis(&self) -> Point3d { | ||||
|         match self { | ||||
|             SketchSurface::Plane(plane) => plane.z_axis.clone(), | ||||
|             SketchSurface::Face(face) => face.z_axis.clone(), | ||||
|             SketchSurface::Plane(plane) => plane.z_axis, | ||||
|             SketchSurface::Face(face) => face.z_axis, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1304,7 +1304,7 @@ impl Point2d { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize, Serialize, PartialEq, Clone, ts_rs::TS, JsonSchema, Default)] | ||||
| #[derive(Debug, Deserialize, Serialize, PartialEq, Clone, Copy, ts_rs::TS, JsonSchema, Default)] | ||||
| #[ts(export)] | ||||
| pub struct Point3d { | ||||
|     pub x: f64, | ||||
| @ -1313,6 +1313,7 @@ pub struct Point3d { | ||||
| } | ||||
|  | ||||
| impl Point3d { | ||||
|     pub const ZERO: Self = Self { x: 0.0, y: 0.0, z: 0.0 }; | ||||
|     pub fn new(x: f64, y: f64, z: f64) -> Self { | ||||
|         Self { x, y, z } | ||||
|     } | ||||
|  | ||||
| @ -927,7 +927,7 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> { | ||||
|  | ||||
|                 match body_items_within_function.parse_next(i) { | ||||
|                     Err(ErrMode::Backtrack(_)) => { | ||||
|                         i.reset(start); | ||||
|                         i.reset(&start); | ||||
|                         break; | ||||
|                     } | ||||
|                     Err(e) => return Err(e), | ||||
| @ -937,7 +937,7 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> { | ||||
|                 } | ||||
|             } | ||||
|             (Err(ErrMode::Backtrack(_)), _) => { | ||||
|                 i.reset(start); | ||||
|                 i.reset(&start); | ||||
|                 break; | ||||
|             } | ||||
|             (Err(e), _) => return Err(e), | ||||
| @ -1276,7 +1276,7 @@ fn unary_expression(i: TokenSlice) -> PResult<UnaryExpression> { | ||||
|  | ||||
| /// Consume tokens that make up a binary expression, but don't actually return them. | ||||
| /// Why not? | ||||
| /// Because this is designed to be used with .recognize() within the `binary_expression` parser. | ||||
| /// Because this is designed to be used with .take() within the `binary_expression` parser. | ||||
| fn binary_expression_tokens(i: TokenSlice) -> PResult<Vec<BinaryExpressionToken>> { | ||||
|     let first = operand.parse_next(i).map(BinaryExpressionToken::from)?; | ||||
|     let remaining: Vec<_> = repeat( | ||||
| @ -1308,7 +1308,7 @@ fn binary_expression(i: TokenSlice) -> PResult<BinaryExpression> { | ||||
| } | ||||
|  | ||||
| fn binary_expr_in_parens(i: TokenSlice) -> PResult<BinaryExpression> { | ||||
|     let span_with_brackets = bracketed_section.recognize().parse_next(i)?; | ||||
|     let span_with_brackets = bracketed_section.take().parse_next(i)?; | ||||
|     let n = span_with_brackets.len(); | ||||
|     let mut span_no_brackets = &span_with_brackets[1..n - 1]; | ||||
|     let expr = binary_expression.parse_next(&mut span_no_brackets)?; | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| use winnow::{ | ||||
|     error::{ErrorKind, ParseError, StrContext}, | ||||
|     stream::Stream, | ||||
|     Located, | ||||
| }; | ||||
|  | ||||
| @ -102,14 +103,17 @@ impl<C> std::default::Default for ContextError<C> { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<I, C> winnow::error::ParserError<I> for ContextError<C> { | ||||
| impl<I, C> winnow::error::ParserError<I> for ContextError<C> | ||||
| where | ||||
|     I: Stream, | ||||
| { | ||||
|     #[inline] | ||||
|     fn from_error_kind(_input: &I, _kind: ErrorKind) -> Self { | ||||
|         Self::default() | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     fn append(self, _input: &I, _kind: ErrorKind) -> Self { | ||||
|     fn append(self, _input: &I, _input_checkpoint: &<I as Stream>::Checkpoint, _kind: ErrorKind) -> Self { | ||||
|         self | ||||
|     } | ||||
|  | ||||
| @ -119,9 +123,12 @@ impl<I, C> winnow::error::ParserError<I> for ContextError<C> { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<C, I> winnow::error::AddContext<I, C> for ContextError<C> { | ||||
| impl<C, I> winnow::error::AddContext<I, C> for ContextError<C> | ||||
| where | ||||
|     I: Stream, | ||||
| { | ||||
|     #[inline] | ||||
|     fn add_context(mut self, _input: &I, ctx: C) -> Self { | ||||
|     fn add_context(mut self, _input: &I, _input_checkpoint: &<I as Stream>::Checkpoint, ctx: C) -> Self { | ||||
|         self.context.push(ctx); | ||||
|         self | ||||
|     } | ||||
|  | ||||
| @ -1,7 +1,10 @@ | ||||
| //! Functions related to extruding. | ||||
|  | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use anyhow::Result; | ||||
| use derive_docs::stdlib; | ||||
| use kittycad::types::{ExtrusionFaceCapType, ExtrusionFaceInfo}; | ||||
| use schemars::JsonSchema; | ||||
| use uuid::Uuid; | ||||
|  | ||||
| @ -90,7 +93,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args | ||||
|                 adjust_camera: false, | ||||
|                 planar_normal: if let SketchSurface::Plane(plane) = &sketch_group.on { | ||||
|                     // We pass in the normal for the plane here. | ||||
|                     Some(plane.z_axis.clone().into()) | ||||
|                     Some(plane.z_axis.into()) | ||||
|                 } else { | ||||
|                     None | ||||
|                 }, | ||||
| @ -98,7 +101,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args | ||||
|         ) | ||||
|         .await?; | ||||
|  | ||||
|         args.send_modeling_cmd( | ||||
|         args.batch_modeling_cmd( | ||||
|             id, | ||||
|             kittycad::types::ModelingCmd::Extrude { | ||||
|                 target: sketch_group.id, | ||||
| @ -111,7 +114,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args | ||||
|         // Disable the sketch mode. | ||||
|         args.batch_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {}) | ||||
|             .await?; | ||||
|         extrude_groups.push(do_post_extrude(sketch_group.clone(), length, id, args.clone()).await?); | ||||
|         extrude_groups.push(do_post_extrude(sketch_group.clone(), length, args.clone()).await?); | ||||
|     } | ||||
|  | ||||
|     Ok(extrude_groups.into()) | ||||
| @ -120,7 +123,6 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args | ||||
| pub(crate) async fn do_post_extrude( | ||||
|     sketch_group: SketchGroup, | ||||
|     length: f64, | ||||
|     id: Uuid, | ||||
|     args: Args, | ||||
| ) -> Result<Box<ExtrudeGroup>, KclError> { | ||||
|     // Bring the object to the front of the scene. | ||||
| @ -164,7 +166,7 @@ pub(crate) async fn do_post_extrude( | ||||
|  | ||||
|     let solid3d_info = args | ||||
|         .send_modeling_cmd( | ||||
|             id, | ||||
|             uuid::Uuid::new_v4(), | ||||
|             kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo { | ||||
|                 edge_id, | ||||
|                 object_id: sketch_group.id, | ||||
| @ -181,91 +183,95 @@ pub(crate) async fn do_post_extrude( | ||||
|         vec![] | ||||
|     }; | ||||
|  | ||||
|     for face_info in face_infos.iter() { | ||||
|         if face_info.cap == kittycad::types::ExtrusionFaceCapType::None { | ||||
|     for (curve_id, face_id) in face_infos | ||||
|         .iter() | ||||
|         .filter(|face_info| face_info.cap == ExtrusionFaceCapType::None) | ||||
|         .filter_map(|face_info| { | ||||
|             if let (Some(curve_id), Some(face_id)) = (face_info.curve_id, face_info.face_id) { | ||||
|                 args.batch_modeling_cmd( | ||||
|                     uuid::Uuid::new_v4(), | ||||
|                     kittycad::types::ModelingCmd::Solid3DGetOppositeEdge { | ||||
|                         edge_id: curve_id, | ||||
|                         object_id: sketch_group.id, | ||||
|                         face_id, | ||||
|                     }, | ||||
|                 ) | ||||
|                 .await?; | ||||
|  | ||||
|                 args.batch_modeling_cmd( | ||||
|                     uuid::Uuid::new_v4(), | ||||
|                     kittycad::types::ModelingCmd::Solid3DGetPrevAdjacentEdge { | ||||
|                         edge_id: curve_id, | ||||
|                         object_id: sketch_group.id, | ||||
|                         face_id, | ||||
|                     }, | ||||
|                 ) | ||||
|                 .await?; | ||||
|                 Some((curve_id, face_id)) | ||||
|             } else { | ||||
|                 None | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Create a hashmap for quick id lookup | ||||
|     let mut face_id_map = std::collections::HashMap::new(); | ||||
|     // creating fake ids for start and end caps is to make extrudes mock-execute safe | ||||
|     let mut start_cap_id = if args.ctx.is_mock { Some(Uuid::new_v4()) } else { None }; | ||||
|     let mut end_cap_id = if args.ctx.is_mock { Some(Uuid::new_v4()) } else { None }; | ||||
|  | ||||
|     for face_info in face_infos { | ||||
|         match face_info.cap { | ||||
|             kittycad::types::ExtrusionFaceCapType::Bottom => start_cap_id = face_info.face_id, | ||||
|             kittycad::types::ExtrusionFaceCapType::Top => end_cap_id = face_info.face_id, | ||||
|             _ => { | ||||
|                 if let Some(curve_id) = face_info.curve_id { | ||||
|                     face_id_map.insert(curve_id, face_info.face_id); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         }) | ||||
|     { | ||||
|         // Batch these commands, because the Rust code doesn't actually care about the outcome. | ||||
|         // So, there's no need to await them. | ||||
|         // Instead, the Typescript codebases (which handles WebSocket sends when compiled via Wasm) | ||||
|         // uses this to build the artifact graph, which the UI needs. | ||||
|         args.batch_modeling_cmd( | ||||
|             uuid::Uuid::new_v4(), | ||||
|             kittycad::types::ModelingCmd::Solid3DGetOppositeEdge { | ||||
|                 edge_id: curve_id, | ||||
|                 object_id: sketch_group.id, | ||||
|                 face_id, | ||||
|             }, | ||||
|         ) | ||||
|         .await?; | ||||
|  | ||||
|         args.batch_modeling_cmd( | ||||
|             uuid::Uuid::new_v4(), | ||||
|             kittycad::types::ModelingCmd::Solid3DGetPrevAdjacentEdge { | ||||
|                 edge_id: curve_id, | ||||
|                 object_id: sketch_group.id, | ||||
|                 face_id, | ||||
|             }, | ||||
|         ) | ||||
|         .await?; | ||||
|     } | ||||
|  | ||||
|     let Faces { | ||||
|         sides: face_id_map, | ||||
|         start_cap_id, | ||||
|         end_cap_id, | ||||
|     } = analyze_faces(&args, face_infos); | ||||
|     // Iterate over the sketch_group.value array and add face_id to GeoMeta | ||||
|     let mut new_value: Vec<ExtrudeSurface> = Vec::new(); | ||||
|     for path in sketch_group.value.iter() { | ||||
|         if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) { | ||||
|             match path { | ||||
|                 Path::TangentialArc { .. } | Path::TangentialArcTo { .. } => { | ||||
|                     let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::executor::ExtrudeArc { | ||||
|                         face_id: *actual_face_id, | ||||
|                         tag: path.get_base().tag.clone(), | ||||
|                         geo_meta: GeoMeta { | ||||
|                             id: path.get_base().geo_meta.id, | ||||
|                             metadata: path.get_base().geo_meta.metadata.clone(), | ||||
|                         }, | ||||
|                     }); | ||||
|                     new_value.push(extrude_surface); | ||||
|                 } | ||||
|                 Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => { | ||||
|                     let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane { | ||||
|                         face_id: *actual_face_id, | ||||
|                         tag: path.get_base().tag.clone(), | ||||
|                         geo_meta: GeoMeta { | ||||
|                             id: path.get_base().geo_meta.id, | ||||
|                             metadata: path.get_base().geo_meta.metadata.clone(), | ||||
|                         }, | ||||
|                     }); | ||||
|                     new_value.push(extrude_surface); | ||||
|     let new_value = sketch_group | ||||
|         .value | ||||
|         .iter() | ||||
|         .flat_map(|path| { | ||||
|             if let Some(Some(actual_face_id)) = face_id_map.get(&path.get_base().geo_meta.id) { | ||||
|                 match path { | ||||
|                     Path::TangentialArc { .. } | Path::TangentialArcTo { .. } => { | ||||
|                         let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::executor::ExtrudeArc { | ||||
|                             face_id: *actual_face_id, | ||||
|                             tag: path.get_base().tag.clone(), | ||||
|                             geo_meta: GeoMeta { | ||||
|                                 id: path.get_base().geo_meta.id, | ||||
|                                 metadata: path.get_base().geo_meta.metadata.clone(), | ||||
|                             }, | ||||
|                         }); | ||||
|                         Some(extrude_surface) | ||||
|                     } | ||||
|                     Path::Base { .. } | Path::ToPoint { .. } | Path::Horizontal { .. } | Path::AngledLineTo { .. } => { | ||||
|                         let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane { | ||||
|                             face_id: *actual_face_id, | ||||
|                             tag: path.get_base().tag.clone(), | ||||
|                             geo_meta: GeoMeta { | ||||
|                                 id: path.get_base().geo_meta.id, | ||||
|                                 metadata: path.get_base().geo_meta.metadata.clone(), | ||||
|                             }, | ||||
|                         }); | ||||
|                         Some(extrude_surface) | ||||
|                     } | ||||
|                 } | ||||
|             } else if args.ctx.is_mock { | ||||
|                 // Only pre-populate the extrude surface if we are in mock mode. | ||||
|  | ||||
|                 let extrude_surface = ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane { | ||||
|                     // pushing this values with a fake face_id to make extrudes mock-execute safe | ||||
|                     face_id: Uuid::new_v4(), | ||||
|                     tag: path.get_base().tag.clone(), | ||||
|                     geo_meta: GeoMeta { | ||||
|                         id: path.get_base().geo_meta.id, | ||||
|                         metadata: path.get_base().geo_meta.metadata.clone(), | ||||
|                     }, | ||||
|                 }); | ||||
|                 Some(extrude_surface) | ||||
|             } else { | ||||
|                 None | ||||
|             } | ||||
|         } else if args.ctx.is_mock { | ||||
|             // Only pre-populate the extrude surface if we are in mock mode. | ||||
|             new_value.push(ExtrudeSurface::ExtrudePlane(crate::executor::ExtrudePlane { | ||||
|                 // pushing this values with a fake face_id to make extrudes mock-execute safe | ||||
|                 face_id: Uuid::new_v4(), | ||||
|                 tag: path.get_base().tag.clone(), | ||||
|                 geo_meta: GeoMeta { | ||||
|                     id: path.get_base().geo_meta.id, | ||||
|                     metadata: path.get_base().geo_meta.metadata.clone(), | ||||
|                 }, | ||||
|             })); | ||||
|         } | ||||
|     } | ||||
|         }) | ||||
|         .collect(); | ||||
|  | ||||
|     Ok(Box::new(ExtrudeGroup { | ||||
|         // Ok so you would think that the id would be the id of the extrude group, | ||||
| @ -273,11 +279,45 @@ pub(crate) async fn do_post_extrude( | ||||
|         // sketch group. | ||||
|         id: sketch_group.id, | ||||
|         value: new_value, | ||||
|         sketch_group: sketch_group.clone(), | ||||
|         meta: sketch_group.meta.clone(), | ||||
|         sketch_group, | ||||
|         height: length, | ||||
|         start_cap_id, | ||||
|         end_cap_id, | ||||
|         edge_cuts: vec![], | ||||
|         meta: sketch_group.meta, | ||||
|     })) | ||||
| } | ||||
|  | ||||
| #[derive(Default)] | ||||
| struct Faces { | ||||
|     /// Maps curve ID to face ID for each side. | ||||
|     sides: HashMap<Uuid, Option<Uuid>>, | ||||
|     /// Top face ID. | ||||
|     end_cap_id: Option<Uuid>, | ||||
|     /// Bottom face ID. | ||||
|     start_cap_id: Option<Uuid>, | ||||
| } | ||||
|  | ||||
| fn analyze_faces(args: &Args, face_infos: Vec<ExtrusionFaceInfo>) -> Faces { | ||||
|     let mut faces = Faces { | ||||
|         sides: HashMap::with_capacity(face_infos.len()), | ||||
|         ..Default::default() | ||||
|     }; | ||||
|     if args.ctx.is_mock { | ||||
|         // Create fake IDs for start and end caps, to make extrudes mock-execute safe | ||||
|         faces.start_cap_id = Some(Uuid::new_v4()); | ||||
|         faces.end_cap_id = Some(Uuid::new_v4()); | ||||
|     } | ||||
|     for face_info in face_infos { | ||||
|         match face_info.cap { | ||||
|             ExtrusionFaceCapType::Bottom => faces.start_cap_id = face_info.face_id, | ||||
|             ExtrusionFaceCapType::Top => faces.end_cap_id = face_info.face_id, | ||||
|             ExtrusionFaceCapType::None => { | ||||
|                 if let Some(curve_id) = face_info.curve_id { | ||||
|                     faces.sides.insert(curve_id, face_info.face_id); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     faces | ||||
| } | ||||
|  | ||||
| @ -12,7 +12,7 @@ use crate::{ | ||||
|     std::{extrude::do_post_extrude, fillet::default_tolerance, Args}, | ||||
| }; | ||||
|  | ||||
| const DEFAULT_V_DEGREE: u32 = 1; | ||||
| const DEFAULT_V_DEGREE: u32 = 2; | ||||
|  | ||||
| /// Data for a loft. | ||||
| #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)] | ||||
| @ -98,6 +98,39 @@ pub async fn loft(args: Args) -> Result<KclValue, KclError> { | ||||
| /// | ||||
| /// loft([squareSketch, circleSketch0, circleSketch1]) | ||||
| /// ``` | ||||
| /// | ||||
| /// ```no_run | ||||
| /// // Loft a square, a circle, and another circle with options. | ||||
| /// const squareSketch = startSketchOn('XY') | ||||
| ///     |> startProfileAt([-100, 200], %) | ||||
| ///     |> line([200, 0], %) | ||||
| ///     |> line([0, -200], %) | ||||
| ///     |> line([-200, 0], %) | ||||
| ///     |> lineTo([profileStartX(%), profileStartY(%)], %) | ||||
| ///     |> close(%) | ||||
| /// | ||||
| /// const circleSketch0 = startSketchOn(offsetPlane('XY', 75)) | ||||
| ///     |> circle([0, 100], 50, %) | ||||
| /// | ||||
| /// const circleSketch1 = startSketchOn(offsetPlane('XY', 150)) | ||||
| ///     |> circle([0, 100], 20, %) | ||||
| /// | ||||
| /// loft([squareSketch, circleSketch0, circleSketch1], { | ||||
| ///     // This can be set to override the automatically determined | ||||
| ///     // topological base curve, which is usually the first section encountered. | ||||
| ///     baseCurveIndex: 0, | ||||
| ///     // Attempt to approximate rational curves (such as arcs) using a bezier. | ||||
| ///     // This will remove banding around interpolations between arcs and non-arcs. | ||||
| ///     // It may produce errors in other scenarios Over time, this field won't be necessary. | ||||
| ///     bezApproximateRational: false, | ||||
| ///     // Tolerance for the loft operation. | ||||
| ///     tolerance: 0.000001, | ||||
| ///     // Degree of the interpolation. Must be greater than zero. | ||||
| ///     // For example, use 2 for quadratic, or 3 for cubic interpolation in | ||||
| ///     // the V direction. This defaults to 2, if not specified. | ||||
| ///     vDegree: 2, | ||||
| /// }) | ||||
| /// ``` | ||||
| #[stdlib { | ||||
|     name = "loft", | ||||
| }] | ||||
| @ -137,5 +170,5 @@ async fn inner_loft( | ||||
|     .await?; | ||||
|  | ||||
|     // Using the first sketch as the base curve, idk we might want to change this later. | ||||
|     do_post_extrude(sketch_groups[0].clone(), 0.0, id, args).await | ||||
|     do_post_extrude(sketch_groups[0].clone(), 0.0, args).await | ||||
| } | ||||
|  | ||||
| @ -488,7 +488,7 @@ layout: manual | ||||
|             buf.push_str(&fn_docs); | ||||
|  | ||||
|             // Write the file. | ||||
|             expectorate::assert_contents(&format!("../../../docs/kcl/{}.md", internal_fn.name()), &buf); | ||||
|             expectorate::assert_contents(format!("../../../docs/kcl/{}.md", internal_fn.name()), &buf); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -299,7 +299,7 @@ async fn inner_revolve( | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     do_post_extrude(sketch_group, 0.0, id, args).await | ||||
|     do_post_extrude(sketch_group, 0.0, args).await | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
|  | ||||
| @ -1269,7 +1269,7 @@ pub(crate) async fn inner_start_profile_at( | ||||
|             adjust_camera: false, | ||||
|             planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface { | ||||
|                 // We pass in the normal for the plane here. | ||||
|                 Some(plane.z_axis.clone().into()) | ||||
|                 Some(plane.z_axis.into()) | ||||
|             } else { | ||||
|                 None | ||||
|             }, | ||||
|  | ||||
| @ -50,13 +50,13 @@ pub fn token(i: &mut Located<&str>) -> PResult<Token> { | ||||
| } | ||||
|  | ||||
| fn block_comment(i: &mut Located<&str>) -> PResult<Token> { | ||||
|     let inner = ("/*", take_until(0.., "*/"), "*/").recognize(); | ||||
|     let inner = ("/*", take_until(0.., "*/"), "*/").take(); | ||||
|     let (value, range) = inner.with_span().parse_next(i)?; | ||||
|     Ok(Token::from_range(range, TokenType::BlockComment, value.to_string())) | ||||
| } | ||||
|  | ||||
| fn line_comment(i: &mut Located<&str>) -> PResult<Token> { | ||||
|     let inner = (r#"//"#, take_till(0.., ['\n', '\r'])).recognize(); | ||||
|     let inner = (r#"//"#, take_till(0.., ['\n', '\r'])).take(); | ||||
|     let (value, range) = inner.with_span().parse_next(i)?; | ||||
|     Ok(Token::from_range(range, TokenType::LineComment, value.to_string())) | ||||
| } | ||||
| @ -68,7 +68,7 @@ fn number(i: &mut Located<&str>) -> PResult<Token> { | ||||
|         // No digits before the decimal point. | ||||
|         ('.', digit1).map(|_| ()), | ||||
|     )); | ||||
|     let (value, range) = number_parser.recognize().with_span().parse_next(i)?; | ||||
|     let (value, range) = number_parser.take().with_span().parse_next(i)?; | ||||
|     Ok(Token::from_range(range, TokenType::Number, value.to_string())) | ||||
| } | ||||
|  | ||||
| @ -79,12 +79,12 @@ fn whitespace(i: &mut Located<&str>) -> PResult<Token> { | ||||
|  | ||||
| fn inner_word(i: &mut Located<&str>) -> PResult<()> { | ||||
|     one_of(('a'..='z', 'A'..='Z', '_')).parse_next(i)?; | ||||
|     repeat(0.., one_of(('a'..='z', 'A'..='Z', '0'..='9', '_'))).parse_next(i)?; | ||||
|     repeat::<_, _, (), _, _>(0.., one_of(('a'..='z', 'A'..='Z', '0'..='9', '_'))).parse_next(i)?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn word(i: &mut Located<&str>) -> PResult<Token> { | ||||
|     let (value, range) = inner_word.recognize().with_span().parse_next(i)?; | ||||
|     let (value, range) = inner_word.take().with_span().parse_next(i)?; | ||||
|     Ok(Token::from_range(range, TokenType::Word, value.to_string())) | ||||
| } | ||||
|  | ||||
| @ -162,9 +162,9 @@ fn inner_single_quote(i: &mut Located<&str>) -> PResult<()> { | ||||
| } | ||||
|  | ||||
| fn string(i: &mut Located<&str>) -> PResult<Token> { | ||||
|     let single_quoted_string = ('\'', inner_single_quote.recognize(), '\''); | ||||
|     let double_quoted_string = ('"', inner_double_quote.recognize(), '"'); | ||||
|     let either_quoted_string = alt((single_quoted_string.recognize(), double_quoted_string.recognize())); | ||||
|     let single_quoted_string = ('\'', inner_single_quote.take(), '\''); | ||||
|     let double_quoted_string = ('"', inner_double_quote.take(), '"'); | ||||
|     let either_quoted_string = alt((single_quoted_string.take(), double_quoted_string.take())); | ||||
|     let (value, range): (&str, _) = either_quoted_string.with_span().parse_next(i)?; | ||||
|     Ok(Token::from_range(range, TokenType::String, value.to_string())) | ||||
| } | ||||
|  | ||||
| Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 107 KiB | 
| Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 135 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/wasm-lib/kcl/tests/outputs/serial_test_example_loft2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 135 KiB | 
| Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 101 KiB | 
| Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 71 KiB | 
| Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 91 KiB | 
| Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 65 KiB | 
| @ -1,3 +1,3 @@ | ||||
| [toolchain] | ||||
| channel = "1.80.1" | ||||
| channel = "1.81.0" | ||||
| components = ["clippy", "rustfmt"] | ||||
|  | ||||
							
								
								
									
										81
									
								
								src/wasm-lib/tests/executor/inputs/slow_lego.kcl.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,81 @@ | ||||
| // 2x8 Lego Brick | ||||
| // A standard Lego brick with 2 bumps wide and 8 bumps long. | ||||
| // Define constants | ||||
| const lbumps = 10 // number of bumps long | ||||
| const wbumps = {{N}} // number of bumps wide | ||||
| const pitch = 8.0 | ||||
| const clearance = 0.1 | ||||
| const bumpDiam = 4.8 | ||||
| const bumpHeight = 1.8 | ||||
| const height = 9.6 | ||||
| const t = (pitch - (2 * clearance) - bumpDiam) / 2.0 | ||||
| const totalLength = lbumps * pitch - (2.0 * clearance) | ||||
| const totalWidth = wbumps * pitch - (2.0 * clearance) | ||||
| // Create the plane for the pegs. This is a hack so that the pegs can be patterned along the face of the lego base. | ||||
| const pegFace = { | ||||
|   plane: { | ||||
|     origin: { x: 0, y: 0, z: height }, | ||||
|     xAxis: { x: 1, y: 0, z: 0 }, | ||||
|     yAxis: { x: 0, y: 1, z: 0 }, | ||||
|     zAxis: { x: 0, y: 0, z: 1 } | ||||
|   } | ||||
| } | ||||
| // Create the plane for the tubes underneath the lego. This is a hack so that the tubes can be patterned underneath the lego. | ||||
| const tubeFace = { | ||||
|     plane: { | ||||
|     origin: { x: 0, y: 0, z: height - t }, | ||||
|     xAxis: { x: 1, y: 0, z: 0 }, | ||||
|     yAxis: { x: 0, y: 1, z: 0 }, | ||||
|     zAxis: { x: 0, y: 0, z: 1 } | ||||
|   } | ||||
| } | ||||
| // Make the base | ||||
| const s = startSketchOn('XY') | ||||
|   |> startProfileAt([-totalWidth / 2, -totalLength / 2], %) | ||||
|   |> line([totalWidth, 0], %) | ||||
|   |> line([0, totalLength], %) | ||||
|   |> line([-totalWidth, 0], %) | ||||
|   |> close(%) | ||||
|   |> extrude(height, %) | ||||
|  | ||||
| // Sketch and extrude a rectangular shape to create the shell underneath the lego. This is a hack until we have a shell function. | ||||
| const shellExtrude = startSketchOn(s, "start") | ||||
|   |> startProfileAt([ | ||||
|        -(totalWidth / 2 - t), | ||||
|        -(totalLength / 2 - t) | ||||
|      ], %) | ||||
|   |> line([totalWidth - (2 * t), 0], %) | ||||
|   |> line([0, totalLength - (2 * t)], %) | ||||
|   |> line([-(totalWidth - (2 * t)), 0], %) | ||||
|   |> close(%) | ||||
|   |> extrude(-(height - t), %) | ||||
|  | ||||
| fn tr = (i) => { | ||||
|   let j = i + 1 | ||||
|   let x = (j/wbumps) * pitch | ||||
|   let y = (j % wbumps) * pitch | ||||
|   return { | ||||
|     translate: [x, y, 0], | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Create the pegs on the top of the base | ||||
| const totalBumps = (wbumps * lbumps)-1 | ||||
| const peg = startSketchOn(s, 'end') | ||||
|   |> circle([ | ||||
|        -(pitch*(wbumps-1)/2), | ||||
|        -(pitch*(lbumps-1)/2) | ||||
|      ], bumpDiam / 2, %) | ||||
|   |> patternLinear2d({ | ||||
|        axis: [1, 0], | ||||
|        repetitions: wbumps-1, | ||||
|        distance: pitch | ||||
|      }, %) | ||||
|   |> patternLinear2d({ | ||||
|        axis: [0, 1], | ||||
|        repetitions: lbumps-1, | ||||
|        distance: pitch | ||||
|      }, %) | ||||
|   |> extrude(bumpHeight, %) | ||||
|   // |> patternTransform(int(totalBumps-1), tr, %) | ||||
|  | ||||
							
								
								
									
										66
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						| @ -2353,72 +2353,6 @@ | ||||
|   resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.8.4.tgz#0ff84b6a0e4b394335cf7ccf759c36b58cbd02eb" | ||||
|   integrity sha512-iO5Ujgw3O1yIxWDe9FgUPNkGjyT657b1WNX52u+Wv1DyBFEpdCdGkuVaky0M3hHFqNWjAmHWTn4wgj9rTr7ZQg== | ||||
|  | ||||
| "@tauri-apps/cli-darwin-arm64@2.0.0-rc.9": | ||||
|   version "2.0.0-rc.9" | ||||
|   resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-rc.9.tgz#d6d9522b549a73ffb2c10ee273e6ac766dfa5914" | ||||
|   integrity sha512-RaCx1KpMX27iS1Cn7MYbVA0Gc5NsjU0Z1Qo42ibzF4OHInOkDcx3qjAaE+xD572Lb9ksBO725cIcYCdgqGu4Vw== | ||||
|  | ||||
| "@tauri-apps/cli-darwin-x64@2.0.0-rc.9": | ||||
|   version "2.0.0-rc.9" | ||||
|   resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.0.0-rc.9.tgz#7ae9abfbeff998f13608d9248bdadba73b1560c0" | ||||
|   integrity sha512-KKUs8kbHYZrcmY/AjKjxEEm7aHGWQsn3+BGsgamKl97k2K5R5Z0KLJUy6QVhUSISEIievjDPmBDIwgA6mlrCLQ== | ||||
|  | ||||
| "@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-rc.9": | ||||
|   version "2.0.0-rc.9" | ||||
|   resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.0.0-rc.9.tgz#8330576565f9ac411011d491a26e94d9116eb5ad" | ||||
|   integrity sha512-OgVCt72g0AnIB3zuKJLEIOCNeviiNeLoQQsVs7ESaqxZ/gMXY35yGVhrFm83eAQ0G4BervHDog15bsY3Dxbc/g== | ||||
|  | ||||
| "@tauri-apps/cli-linux-arm64-gnu@2.0.0-rc.9": | ||||
|   version "2.0.0-rc.9" | ||||
|   resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.0.0-rc.9.tgz#9b4b79dd256c39fed495fd8b7ffdb798078c61ab" | ||||
|   integrity sha512-7kQcXXXpCYB0AWbTRaKAim3JVMKdrxVOiqnOW+7elkqDQxDqmLQho2ah1qHv7LzZ6Z83u5QejrRLeHrrdo3PEg== | ||||
|  | ||||
| "@tauri-apps/cli-linux-arm64-musl@2.0.0-rc.9": | ||||
|   version "2.0.0-rc.9" | ||||
|   resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-rc.9.tgz#5afd06c1601ff823b7d82785236f63af379fd6d4" | ||||
|   integrity sha512-2hqANZrydqZpptUsfAHSL5DIaEfHN73UGEu+5keFCV1Irh+QPydr1CYrqhgFF982ev6Ars7nxALwpPhEODjYlg== | ||||
|  | ||||
| "@tauri-apps/cli-linux-x64-gnu@2.0.0-rc.9": | ||||
|   version "2.0.0-rc.9" | ||||
|   resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.0.0-rc.9.tgz#39185adc857e3e8474008600b7f0a6e0e42abdbf" | ||||
|   integrity sha512-Zjna6eoVSlmZtzAXgH27sgJRnczNzMKRiGsMpY00PFxN9sbQwlsS3yMfB8GHsBeBoq+qJQsteRwhrn1mj6e3Rg== | ||||
|  | ||||
| "@tauri-apps/cli-linux-x64-musl@2.0.0-rc.9": | ||||
|   version "2.0.0-rc.9" | ||||
|   resolved "https://registry.yarnpkg.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-rc.9.tgz#a8d703010892622cf38e87950f5d2920833fac88" | ||||
|   integrity sha512-8ODcbvwZw29sAWns36BeBYJ3iu3Mtv4J3WkcoVbanVCP8nu7ja3401VnWBjckRiI1iDJIm59m6ojVkGYQhAe9Q== | ||||
|  | ||||
| "@tauri-apps/cli-win32-arm64-msvc@2.0.0-rc.9": | ||||
|   version "2.0.0-rc.9" | ||||
|   resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.0.0-rc.9.tgz#8ddea7d990b701357fe3dfd8e8e1783898206d85" | ||||
|   integrity sha512-j6jJId8hlid/W4ezDRNK49DSjxb82W6d1qVqO7zksKdZLy8tVzFkZXwEeKhabzRQsO87KL34I+ciRlmInGis0Q== | ||||
|  | ||||
| "@tauri-apps/cli-win32-ia32-msvc@2.0.0-rc.9": | ||||
|   version "2.0.0-rc.9" | ||||
|   resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.0.0-rc.9.tgz#ffa340d2dbf0e87355fa92650fbd707adc12d84e" | ||||
|   integrity sha512-w9utY58kfzJS+iLCjyQyQbJS8YaCM8YCWkgK2ZkySmHAdnqdGeyJEWig1qrLH1TWd+O6K3TlCNv55ujeAtOE4w== | ||||
|  | ||||
| "@tauri-apps/cli-win32-x64-msvc@2.0.0-rc.9": | ||||
|   version "2.0.0-rc.9" | ||||
|   resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.0.0-rc.9.tgz#93f0cdc8c6999227aeee86741b553c16cb7ac20f" | ||||
|   integrity sha512-+l2RcpTthzYkw3VsmcZkb099Jfl0d21a9VIFxdk+duKeYieRpb0MsIBP6fS7WlNAeqrinC0zi/zt+Nia6mPuyw== | ||||
|  | ||||
| "@tauri-apps/cli@^2.0.0-rc.9": | ||||
|   version "2.0.0-rc.9" | ||||
|   resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.0.0-rc.9.tgz#b641ad224dd055aae4f101c14d0696d2e06862c0" | ||||
|   integrity sha512-cjj5HVKHUlxL87TN7ZZpnlMgcBS+ToIyfLB6jpaNDZ9Op0/qzccWGZpPbW2P/BnfF/qwHzVJNUPGANFyvBSUeg== | ||||
|   optionalDependencies: | ||||
|     "@tauri-apps/cli-darwin-arm64" "2.0.0-rc.9" | ||||
|     "@tauri-apps/cli-darwin-x64" "2.0.0-rc.9" | ||||
|     "@tauri-apps/cli-linux-arm-gnueabihf" "2.0.0-rc.9" | ||||
|     "@tauri-apps/cli-linux-arm64-gnu" "2.0.0-rc.9" | ||||
|     "@tauri-apps/cli-linux-arm64-musl" "2.0.0-rc.9" | ||||
|     "@tauri-apps/cli-linux-x64-gnu" "2.0.0-rc.9" | ||||
|     "@tauri-apps/cli-linux-x64-musl" "2.0.0-rc.9" | ||||
|     "@tauri-apps/cli-win32-arm64-msvc" "2.0.0-rc.9" | ||||
|     "@tauri-apps/cli-win32-ia32-msvc" "2.0.0-rc.9" | ||||
|     "@tauri-apps/cli-win32-x64-msvc" "2.0.0-rc.9" | ||||
|  | ||||
| "@testing-library/dom@^10.0.0": | ||||
|   version "10.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8" | ||||
|  | ||||
