Compare commits
	
		
			6 Commits
		
	
	
		
			v0.33.0
			...
			cut-releas
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 04a77efae3 | |||
| 379cd1e067 | |||
| 8c3d438f6d | |||
| ac15049e2c | |||
| 466da6be55 | |||
| 38d5be001b | 
							
								
								
									
										31
									
								
								.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' | ||||
| @ -156,15 +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: ${{ 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 | ||||
| @ -245,6 +246,15 @@ 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: | ||||
| @ -253,6 +263,15 @@ jobs: | ||||
|           parent: false | ||||
|           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 | ||||
|         with: | ||||
|  | ||||
							
								
								
									
										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 | ||||
|  | ||||
| @ -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', | ||||
|  | ||||
| @ -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,6 +1,6 @@ | ||||
| { | ||||
|   "name": "zoo-modeling-app", | ||||
|   "version": "0.25.0", | ||||
|   "version": "0.25.1", | ||||
|   "private": true, | ||||
|   "productName": "Zoo Modeling App", | ||||
|   "author": { | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -56,11 +56,6 @@ body.dark { | ||||
|   .dark .body-bg { | ||||
|     @apply bg-chalkboard-100; | ||||
|   } | ||||
|  | ||||
|   body { | ||||
|     scrollbar-color: var(--color-chalkboard-70) var(--color-chalkboard-90); | ||||
|     @apply text-chalkboard-10; | ||||
|   } | ||||
| } | ||||
|  | ||||
| select { | ||||
| @ -300,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, | ||||
|  | ||||
| @ -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 ( | ||||
|  | ||||
| @ -70,17 +70,11 @@ const SignIn = () => { | ||||
|     > | ||||
|       <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)', | ||||
|             ...(isDesktop() ? { '-webkit-app-region': 'no-drag' } : {}), | ||||
|           } 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"> | ||||
| @ -204,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" | ||||
|                 > | ||||
|  | ||||
							
								
								
									
										19
									
								
								src/wasm-lib/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						| @ -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", | ||||
| @ -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", | ||||
| ] | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -18,7 +18,7 @@ base64 = "0.22.1" | ||||
| chrono = "0.4.38" | ||||
| 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" | ||||
| @ -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"); | ||||
|  | ||||
| @ -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,8 +1,10 @@ | ||||
| //! Functions related to extruding. | ||||
|  | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use anyhow::Result; | ||||
| use derive_docs::stdlib; | ||||
| use kittycad::types::ExtrusionFaceCapType; | ||||
| use kittycad::types::{ExtrusionFaceCapType, ExtrusionFaceInfo}; | ||||
| use schemars::JsonSchema; | ||||
| use uuid::Uuid; | ||||
|  | ||||
| @ -99,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, | ||||
| @ -112,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()) | ||||
| @ -121,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. | ||||
| @ -165,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, | ||||
| @ -218,26 +219,11 @@ pub(crate) async fn do_post_extrude( | ||||
|         .await?; | ||||
|     } | ||||
|  | ||||
|     // 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, mut end_cap_id) = if args.ctx.is_mock { | ||||
|         (Some(Uuid::new_v4()), Some(Uuid::new_v4())) | ||||
|     } else { | ||||
|         (None, None) | ||||
|     }; | ||||
|     for face_info in face_infos { | ||||
|         match face_info.cap { | ||||
|             ExtrusionFaceCapType::Bottom => start_cap_id = face_info.face_id, | ||||
|             ExtrusionFaceCapType::Top => end_cap_id = face_info.face_id, | ||||
|             ExtrusionFaceCapType::None => { | ||||
|                 if let Some(curve_id) = face_info.curve_id { | ||||
|                     face_id_map.insert(curve_id, face_info.face_id); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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 new_value = sketch_group | ||||
|         .value | ||||
| @ -301,3 +287,37 @@ pub(crate) async fn do_post_extrude( | ||||
|         edge_cuts: vec![], | ||||
|     })) | ||||
| } | ||||
|  | ||||
| #[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 | ||||
| } | ||||
|  | ||||
| @ -170,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 | ||||
| } | ||||
|  | ||||
| @ -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)] | ||||
|  | ||||
| @ -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())) | ||||
| } | ||||
|  | ||||
| @ -84,7 +84,7 @@ fn inner_word(i: &mut Located<&str>) -> PResult<()> { | ||||
| } | ||||
|  | ||||
| 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 | 
| Before Width: | Height: | Size: 126 KiB 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 | 
							
								
								
									
										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, %) | ||||
|  | ||||
