Compare commits

...

6 Commits

32 changed files with 318 additions and 182 deletions

View File

@ -5,6 +5,7 @@ on:
push: push:
branches: branches:
- main - main
- cut-release-v0.25.1-updater-test-build-1
release: release:
types: [published] types: [published]
schedule: schedule:
@ -13,8 +14,8 @@ on:
# Will checkout the last commit from the default branch (main as of 2023-10-04) # Will checkout the last commit from the default branch (main as of 2023-10-04)
env: env:
CUT_RELEASE_PR: ${{ github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }} CUT_RELEASE_PR: true
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }} BUILD_RELEASE: true
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 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 # TODO: see if we can fetch from main instead if no diff at src/wasm-lib
- name: Run build:wasm - name: Run build:wasm
run: "yarn build:wasm${{ env.BUILD_RELEASE == 'true' && '-dev' || ''}}" run: "yarn build:wasm"
- name: Set nightly version - name: Set nightly version
if: github.event_name == 'schedule' if: github.event_name == 'schedule'
@ -156,15 +157,15 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
permissions: permissions:
contents: write 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] needs: [prepare-files, build-apps]
env: env:
VERSION_NO_V: ${{ needs.prepare-files.outputs.version }} 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) }} 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 }} 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) }} 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' }} 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' }} 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' }} URL_CODED_NAME: ${{ github.event_name == 'schedule' && 'Zoo%20Modeling%20App%20%28Nightly%29' || 'Zoo%20Modeling%20App' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -245,6 +246,15 @@ jobs:
parent: false 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 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 - name: Upload update endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.0 uses: google-github-actions/upload-cloud-storage@v2.2.0
with: with:
@ -253,6 +263,15 @@ jobs:
parent: false 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 - name: Upload download endpoint to public bucket
uses: google-github-actions/upload-cloud-storage@v2.2.0 uses: google-github-actions/upload-cloud-storage@v2.2.0
with: with:

View File

@ -28,6 +28,7 @@ jobs:
dir: ['src/wasm-lib'] dir: ['src/wasm-lib']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: taiki-e/install-action@just
- name: Install latest rust - name: Install latest rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
@ -41,7 +42,7 @@ jobs:
- name: Run clippy - name: Run clippy
run: | run: |
cd "${{ matrix.dir }}" cd "${{ matrix.dir }}"
cargo clippy --all --tests --benches -- -D warnings just lint
# If this fails, run "cargo check" to update Cargo.lock, # If this fails, run "cargo check" to update Cargo.lock,
# then add Cargo.lock to the PR. # then add Cargo.lock to the PR.
- name: Check Cargo.lock doesn't need updating - name: Check Cargo.lock doesn't need updating

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -112,7 +112,8 @@ test.describe('when using the file tree to', () => {
}) })
const { const {
panesOpen, openKclCodePanel,
openFilePanel,
createAndSelectProject, createAndSelectProject,
pasteCodeInEditor, pasteCodeInEditor,
createNewFileAndSelect, createNewFileAndSelect,
@ -124,9 +125,9 @@ test.describe('when using the file tree to', () => {
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await panesOpen(['files', 'code'])
await createAndSelectProject('project-000') await createAndSelectProject('project-000')
await openKclCodePanel()
await openFilePanel()
// File the main.kcl with contents // File the main.kcl with contents
const kclCube = await fsp.readFile( const kclCube = await fsp.readFile(
'src/wasm-lib/tests/executor/inputs/cube.kcl', 'src/wasm-lib/tests/executor/inputs/cube.kcl',

View File

@ -548,13 +548,16 @@ export async function getUtils(page: Page, test_?: typeof test) {
createNewFileAndSelect: async (name: string) => { createNewFileAndSelect: async (name: string) => {
return test?.step(`Create a file named ${name}, select it`, async () => { return test?.step(`Create a file named ${name}, select it`, async () => {
await openFilePanel(page)
await page.getByTestId('create-file-button').click() await page.getByTestId('create-file-button').click()
await page.getByTestId('file-rename-field').fill(name) await page.getByTestId('file-rename-field').fill(name)
await page.keyboard.press('Enter') await page.keyboard.press('Enter')
await page const newFile = page
.locator('[data-testid="file-pane-scroll-container"] button') .locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: name }) .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[]) => { panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => { return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript( await page.addInitScript(

View File

@ -288,7 +288,7 @@ test.describe('Testing settings', () => {
}) })
await test.step('Refresh the application and see project setting applied', async () => { 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 expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
await settingsCloseButton.click() await settingsCloseButton.click()
@ -364,47 +364,48 @@ test.describe('Testing settings', () => {
async ({ browser: _ }, testInfo) => { async ({ browser: _ }, testInfo) => {
const { electronApp, page } = await setupElectron({ const { electronApp, page } = await setupElectron({
testInfo, 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 { const {
panesOpen, openKclCodePanel,
createAndSelectProject, openFilePanel,
pasteCodeInEditor, waitForPageLoad,
clickPane, selectFile,
createNewFileAndSelect,
editorTextMatches, editorTextMatches,
} = await getUtils(page, test) } = await getUtils(page, test)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await panesOpen([]) await test.step('Precondition: Open to second project file', async () => {
await test.step('Precondition: No projects exist', async () => {
await expect(page.getByTestId('home-section')).toBeVisible() await expect(page.getByTestId('home-section')).toBeVisible()
const projectLinksPre = page.getByTestId('project-link') await page.getByText('project-000').click()
await expect(projectLinksPre).toHaveCount(0) 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', { const settingsOpenButton = page.getByRole('link', {
name: 'settings Settings', name: 'settings Settings',
}) })
@ -412,6 +413,9 @@ test.describe('Testing settings', () => {
await test.step('Open and close settings', async () => { await test.step('Open and close settings', async () => {
await settingsOpenButton.click() await settingsOpenButton.click()
await expect(
page.getByRole('heading', { name: 'Settings', exact: true })
).toBeVisible()
await settingsCloseButton.click() await settingsCloseButton.click()
}) })

View File

@ -534,7 +534,7 @@ test.describe('Text-to-CAD tests', () => {
// Ensure the final toast remains. // Ensure the final toast remains.
await expect(page.getByText(`a 2x10 lego`)).not.toBeVisible() 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() await expect(page.getByText(`a 2x4 lego`)).toBeVisible()
// Ensure you can copy the code for the final model. // Ensure you can copy the code for the final model.
@ -690,40 +690,53 @@ test(
'Text-to-CAD functionality', 'Text-to-CAD functionality',
{ tag: '@electron' }, { tag: '@electron' },
async ({ browserName }, testInfo) => { async ({ browserName }, testInfo) => {
const projectName = 'project-000'
const prompt = 'lego 2x4'
const textToCadFileName = 'lego-2x4.kcl'
const { electronApp, page, dir } = await setupElectron({ testInfo }) const { electronApp, page, dir } = await setupElectron({ testInfo })
const fileExists = () => 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 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 // Create and navigate to the project
await createAndSelectProject('project-000') await createAndSelectProject('project-000')
// Wait for Start Sketch otherwise you will not have access Text-to-CAD command // Wait for Start Sketch otherwise you will not have access Text-to-CAD command
await expect( await waitForPageLoad()
page.getByRole('button', { name: 'Start Sketch' }) await openFilePanel()
).toBeEnabled({ await openKclCodePanel()
timeout: 20_000,
})
await test.step(`Test file creation`, async () => { 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 // File is considered created if it shows up in the Project Files pane
const file = page.getByRole('button', { name: 'lego-2x4.kcl' }) await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 })
await expect(file).toBeVisible({ timeout: 20_000 })
expect(fileExists()).toBeTruthy() expect(fileExists()).toBeTruthy()
}) })
await test.step(`Test file navigation`, async () => { await test.step(`Test file navigation`, async () => {
const file = page.getByRole('button', { name: 'lego-2x4.kcl' }) await expect(projectMenuButton).toContainText('main.kcl')
await file.click() await textToCadFileButton.click()
const kclComment = page.getByText('Lego 2x4 Brick')
// File can be navigated and loaded assuming a specific KCL comment is loaded into the KCL code pane // 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 () => { await test.step(`Test file deletion on rejection`, async () => {
@ -737,6 +750,8 @@ test(
) )
await expect(submittingToastMessage).toBeVisible() await expect(submittingToastMessage).toBeVisible()
expect(fileExists()).toBeFalsy() expect(fileExists()).toBeFalsy()
// Confirm we've navigated back to the main.kcl file after deletion
await expect(projectMenuButton).toContainText('main.kcl')
}) })
await electronApp.close() await electronApp.close()

View File

@ -79,5 +79,5 @@ linux:
publish: publish:
- provider: generic - 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 channel: latest

View File

@ -1,6 +1,6 @@
{ {
"name": "zoo-modeling-app", "name": "zoo-modeling-app",
"version": "0.25.0", "version": "0.25.1",
"private": true, "private": true,
"productName": "Zoo Modeling App", "productName": "Zoo Modeling App",
"author": { "author": {

View File

@ -8,7 +8,7 @@ import { moveValueIntoNewVariable } from 'lang/modifyAst'
import { isNodeSafeToReplace } from 'lang/queryAst' import { isNodeSafeToReplace } from 'lang/queryAst'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useModelingContext } from './useModelingContext' import { useModelingContext } from './useModelingContext'
import { PathToNode, SourceRange, parse, recast } from 'lang/wasm' import { PathToNode, SourceRange } from 'lang/wasm'
import { useKclContext } from 'lang/KclProvider' import { useKclContext } from 'lang/KclProvider'
export const getVarNameModal = createSetVarNameModal(SetVarNameModal) export const getVarNameModal = createSetVarNameModal(SetVarNameModal)
@ -23,8 +23,7 @@ export function useConvertToVariable(range?: SourceRange) {
}, [enable]) }, [enable])
useEffect(() => { useEffect(() => {
const parsed = parse(recast(ast)) const parsed = ast
if (trap(parsed)) return
const meta = isNodeSafeToReplace( const meta = isNodeSafeToReplace(
parsed, parsed,

View File

@ -56,11 +56,6 @@ body.dark {
.dark .body-bg { .dark .body-bg {
@apply bg-chalkboard-100; @apply bg-chalkboard-100;
} }
body {
scrollbar-color: var(--color-chalkboard-70) var(--color-chalkboard-90);
@apply text-chalkboard-10;
}
} }
select { select {
@ -300,32 +295,11 @@ code {
} }
@layer utilities { @layer utilities {
/* Modified from the very helpful https://www.transition.style/#in:circle:hesitate */ /*
@keyframes circle-in-hesitate { This is where your own custom Tailwind utility classes can go,
0% { which lets you use them with @apply in your CSS, and get
clip-path: circle( autocomplete in classNames in your JSX.
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;
}
} }
#code-mirror-override .cm-scroller, #code-mirror-override .cm-scroller,

View File

@ -5,7 +5,7 @@ import {
kclManager, kclManager,
sceneEntitiesManager, sceneEntitiesManager,
} from 'lib/singletons' } 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 { ModelingMachineEvent } from 'machines/modelingMachine'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { EditorSelection, SelectionRange } from '@codemirror/state' import { EditorSelection, SelectionRange } from '@codemirror/state'
@ -300,8 +300,7 @@ export function processCodeMirrorRanges({
} }
function updateSceneObjectColors(codeBasedSelections: Selection[]) { function updateSceneObjectColors(codeBasedSelections: Selection[]) {
const updated = parse(recast(kclManager.ast)) const updated = kclManager.ast
if (err(updated)) return
Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => { Object.values(sceneEntitiesManager.activeSegments).forEach((segmentGroup) => {
if ( if (

View File

@ -70,17 +70,11 @@ const SignIn = () => {
> >
<div <div
style={ style={
{ isDesktop()
height: 'calc(100vh - 16px)', ? ({ '-webkit-app-region': 'no-drag' } as CSSProperties)
'--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
} }
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="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"> <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"> <div className="flex gap-4 flex-wrap items-center">
<ActionButton <ActionButton
Element="externalLink" 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' }} iconStart={{ icon: 'settings' }}
className="border-chalkboard-30 dark:border-chalkboard-80" className="border-chalkboard-30 dark:border-chalkboard-80"
> >

View File

@ -620,9 +620,9 @@ dependencies = [
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.0.1" version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
@ -1357,7 +1357,7 @@ dependencies = [
"clap", "clap",
"convert_case", "convert_case",
"criterion", "criterion",
"dashmap 6.0.1", "dashmap 6.1.0",
"databake", "databake",
"derive-docs", "derive-docs",
"expectorate", "expectorate",
@ -1399,7 +1399,7 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"winnow 0.5.40", "winnow",
"zip", "zip",
] ]
@ -3117,7 +3117,7 @@ dependencies = [
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
"winnow 0.6.18", "winnow",
] ]
[[package]] [[package]]
@ -3800,15 +3800,6 @@ version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.18" version = "0.6.18"

View File

@ -2,3 +2,6 @@
new-test name: new-test name:
echo "kcl_test!(\"{{name}}\", {{name}});" >> tests/executor/visuals.rs echo "kcl_test!(\"{{name}}\", {{name}});" >> tests/executor/visuals.rs
TWENTY_TWENTY=overwrite cargo nextest run --test executor -E 'test(=visuals::{{name}})' TWENTY_TWENTY=overwrite cargo nextest run --test executor -E 'test(=visuals::{{name}})'
lint:
cargo clippy --all --tests --benches -- -D warnings

View File

@ -18,7 +18,7 @@ base64 = "0.22.1"
chrono = "0.4.38" chrono = "0.4.38"
clap = { version = "4.5.17", 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" convert_case = "0.6.0"
dashmap = "6.0.1" dashmap = "6.1.0"
databake = { version = "0.1.8", features = ["derive"] } databake = { version = "0.1.8", features = ["derive"] }
derive-docs = { version = "0.1.26", path = "../derive-docs" } derive-docs = { version = "0.1.26", path = "../derive-docs" }
form_urlencoded = "1.2.1" form_urlencoded = "1.2.1"
@ -47,7 +47,7 @@ url = { version = "2.5.2", features = ["serde"] }
urlencoding = "2.1.3" urlencoding = "2.1.3"
uuid = { version = "1.10.0", features = ["v4", "js", "serde"] } uuid = { version = "1.10.0", features = ["v4", "js", "serde"] }
validator = { version = "0.18.1", features = ["derive"] } validator = { version = "0.18.1", features = ["derive"] }
winnow = "0.5.40" winnow = "0.6.18"
zip = { version = "2.0.0", default-features = false } zip = { version = "2.0.0", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]

View File

@ -1,5 +1,5 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; 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; use tokio::runtime::Runtime;
pub fn bench_execute(c: &mut Criterion) { 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 // Configure Criterion.rs to detect smaller differences and increase sample size to improve
// precision and counteract the resulting noise. // precision and counteract the resulting noise.
group.sample_size(10); 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(); let rt = Runtime::new().unwrap();
// Spawn a future onto the runtime // Spawn a future onto the runtime
b.iter(|| { b.iter(|| {
rt.block_on(test_server::execute_and_snapshot( rt.block_on(test_server::execute_and_snapshot(s, Mm)).unwrap();
s,
kcl_lib::settings::types::UnitLength::Mm,
))
.unwrap();
}); });
}); });
group.finish(); 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); criterion_main!(benches);
const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl"); const KITT_PROGRAM: &str = include_str!("../../tests/executor/inputs/kittycad_svg.kcl");
const CUBE_PROGRAM: &str = include_str!("../../tests/executor/inputs/cube.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_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 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");

View File

@ -927,7 +927,7 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> {
match body_items_within_function.parse_next(i) { match body_items_within_function.parse_next(i) {
Err(ErrMode::Backtrack(_)) => { Err(ErrMode::Backtrack(_)) => {
i.reset(start); i.reset(&start);
break; break;
} }
Err(e) => return Err(e), Err(e) => return Err(e),
@ -937,7 +937,7 @@ pub fn function_body(i: TokenSlice) -> PResult<Program> {
} }
} }
(Err(ErrMode::Backtrack(_)), _) => { (Err(ErrMode::Backtrack(_)), _) => {
i.reset(start); i.reset(&start);
break; break;
} }
(Err(e), _) => return Err(e), (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. /// Consume tokens that make up a binary expression, but don't actually return them.
/// Why not? /// 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>> { fn binary_expression_tokens(i: TokenSlice) -> PResult<Vec<BinaryExpressionToken>> {
let first = operand.parse_next(i).map(BinaryExpressionToken::from)?; let first = operand.parse_next(i).map(BinaryExpressionToken::from)?;
let remaining: Vec<_> = repeat( let remaining: Vec<_> = repeat(
@ -1308,7 +1308,7 @@ fn binary_expression(i: TokenSlice) -> PResult<BinaryExpression> {
} }
fn binary_expr_in_parens(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 n = span_with_brackets.len();
let mut span_no_brackets = &span_with_brackets[1..n - 1]; let mut span_no_brackets = &span_with_brackets[1..n - 1];
let expr = binary_expression.parse_next(&mut span_no_brackets)?; let expr = binary_expression.parse_next(&mut span_no_brackets)?;

View File

@ -1,5 +1,6 @@
use winnow::{ use winnow::{
error::{ErrorKind, ParseError, StrContext}, error::{ErrorKind, ParseError, StrContext},
stream::Stream,
Located, 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] #[inline]
fn from_error_kind(_input: &I, _kind: ErrorKind) -> Self { fn from_error_kind(_input: &I, _kind: ErrorKind) -> Self {
Self::default() Self::default()
} }
#[inline] #[inline]
fn append(self, _input: &I, _kind: ErrorKind) -> Self { fn append(self, _input: &I, _input_checkpoint: &<I as Stream>::Checkpoint, _kind: ErrorKind) -> Self {
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] #[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.context.push(ctx);
self self
} }

View File

@ -1,8 +1,10 @@
//! Functions related to extruding. //! Functions related to extruding.
use std::collections::HashMap;
use anyhow::Result; use anyhow::Result;
use derive_docs::stdlib; use derive_docs::stdlib;
use kittycad::types::ExtrusionFaceCapType; use kittycad::types::{ExtrusionFaceCapType, ExtrusionFaceInfo};
use schemars::JsonSchema; use schemars::JsonSchema;
use uuid::Uuid; use uuid::Uuid;
@ -99,7 +101,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
) )
.await?; .await?;
args.send_modeling_cmd( args.batch_modeling_cmd(
id, id,
kittycad::types::ModelingCmd::Extrude { kittycad::types::ModelingCmd::Extrude {
target: sketch_group.id, target: sketch_group.id,
@ -112,7 +114,7 @@ async fn inner_extrude(length: f64, sketch_group_set: SketchGroupSet, args: Args
// Disable the sketch mode. // Disable the sketch mode.
args.batch_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {}) args.batch_modeling_cmd(uuid::Uuid::new_v4(), kittycad::types::ModelingCmd::SketchModeDisable {})
.await?; .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()) 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( pub(crate) async fn do_post_extrude(
sketch_group: SketchGroup, sketch_group: SketchGroup,
length: f64, length: f64,
id: Uuid,
args: Args, args: Args,
) -> Result<Box<ExtrudeGroup>, KclError> { ) -> Result<Box<ExtrudeGroup>, KclError> {
// Bring the object to the front of the scene. // Bring the object to the front of the scene.
@ -165,7 +166,7 @@ pub(crate) async fn do_post_extrude(
let solid3d_info = args let solid3d_info = args
.send_modeling_cmd( .send_modeling_cmd(
id, uuid::Uuid::new_v4(),
kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo { kittycad::types::ModelingCmd::Solid3DGetExtrusionFaceInfo {
edge_id, edge_id,
object_id: sketch_group.id, object_id: sketch_group.id,
@ -218,26 +219,11 @@ pub(crate) async fn do_post_extrude(
.await?; .await?;
} }
// Create a hashmap for quick id lookup let Faces {
let mut face_id_map = std::collections::HashMap::new(); sides: face_id_map,
// creating fake ids for start and end caps is to make extrudes mock-execute safe start_cap_id,
let (mut start_cap_id, mut end_cap_id) = if args.ctx.is_mock { end_cap_id,
(Some(Uuid::new_v4()), Some(Uuid::new_v4())) } = analyze_faces(&args, face_infos);
} 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);
}
}
}
}
// Iterate over the sketch_group.value array and add face_id to GeoMeta // Iterate over the sketch_group.value array and add face_id to GeoMeta
let new_value = sketch_group let new_value = sketch_group
.value .value
@ -301,3 +287,37 @@ pub(crate) async fn do_post_extrude(
edge_cuts: vec![], 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
}

View File

@ -170,5 +170,5 @@ async fn inner_loft(
.await?; .await?;
// Using the first sketch as the base curve, idk we might want to change this later. // 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
} }

View File

@ -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)] #[cfg(test)]

View File

@ -50,13 +50,13 @@ pub fn token(i: &mut Located<&str>) -> PResult<Token> {
} }
fn block_comment(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)?; let (value, range) = inner.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::BlockComment, value.to_string())) Ok(Token::from_range(range, TokenType::BlockComment, value.to_string()))
} }
fn line_comment(i: &mut Located<&str>) -> PResult<Token> { 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)?; let (value, range) = inner.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::LineComment, value.to_string())) 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. // No digits before the decimal point.
('.', digit1).map(|_| ()), ('.', 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())) 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> { 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())) 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> { fn string(i: &mut Located<&str>) -> PResult<Token> {
let single_quoted_string = ('\'', inner_single_quote.recognize(), '\''); let single_quoted_string = ('\'', inner_single_quote.take(), '\'');
let double_quoted_string = ('"', inner_double_quote.recognize(), '"'); let double_quoted_string = ('"', inner_double_quote.take(), '"');
let either_quoted_string = alt((single_quoted_string.recognize(), double_quoted_string.recognize())); 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)?; let (value, range): (&str, _) = either_quoted_string.with_span().parse_next(i)?;
Ok(Token::from_range(range, TokenType::String, value.to_string())) Ok(Token::from_range(range, TokenType::String, value.to_string()))
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View 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, %)