Compare commits

..

13 Commits

Author SHA1 Message Date
7d6427ab64 Add cursor-not-allowed to onboarding backdrops that block clicks (#6789)
* Add `cursor-not-allowed` to onboarding backdrops that block clicks

Some follow-up feedback after #6714 from @jacebrowning, so that users
know why they can't click around.

* Make anything in the onboarding card `cursor-auto`
2025-05-09 13:28:45 -04:00
4abbe0d57a Result of npm prune (#6807)
pierremtb/adhoc/npm-prune
2025-05-09 17:07:06 +00:00
a631ff689f Remove unnecessary checks for execution completion from onboarding test (#6804)
We don't care if the axial fan loads under 15s on a cheap CI machine, at
least not in this test.
2025-05-09 17:01:42 +00:00
e1d401adfe Remove snapshottoken variable and playwright-secrets.env file (#6801)
* Remove snapshottoken
Fixes #6800

* Add placeholder in .env.development

* Clean up language

* Update CONTRIBUTING.md

Co-authored-by: Jace Browning <jacebrowning@gmail.com>

* Add dotenv to secrets for local testing

* Lint

* Reorg things

* Quick fix

* Last one for windows

---------

Co-authored-by: Jace Browning <jacebrowning@gmail.com>
2025-05-09 12:32:35 -04:00
6f49c88382 Remove dev.zoo.dev from contributing (#6799) 2025-05-09 10:20:22 -04:00
374d07b995 Turn on Billing UI in releases (#6788)
* Turn on Billing UI in releases

* Update most snapshots but one, and new masks
2025-05-09 09:04:45 -04:00
3481252082 fix subtract test (#6791)
* fix subtract test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix subtract test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixups

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-05-08 20:28:41 -07:00
035f3b6aed Update known-issues.md (#6790) 2025-05-09 03:21:25 +00:00
923feadfa5 Suggest a list of possible arg labels when an argument is unlabelled (#6755)
Signed-off-by: Nick Cameron <nrc@ncameron.org>
2025-05-09 14:28:04 +12:00
1ea66d6f23 [Fix]: Always show stack trace on the page if an Error shows up (#6785)
* fix: show stack traces

* fix: update GH with report a bug
2025-05-08 21:17:34 -05:00
3b7b4f85a1 Update onboarding to V1 browser and desktop flows (#6714)
* Remove unused `telemetryLoader`

* Remove onboarding redirect behavior

* Allow subRoute to be passed to navigateToProject

* Replace warning dialog routes with toasts

* Wire up new utilities and toasts to UI components

* Add home sidebar buttons for tutorial flow

* Rename menu item

* Add flex-1 so home-layout fills available space

* Remove onboarding avatar tests, they are becoming irrelevant

* Consolidate onboarding tests to one longer one

and update it to not use pixel color checks, and use fixtures.

* Shorten warning toast button text

* tsc, lint, and circular deps

* Update circular dep file

* Fix mistakes made in circular update tweaking

* One more dumb created circular dep

* Update src/routes/Onboarding/utils.tsx

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Fix narrow screen home layout breaking

* fix: kevin, navigation routes fixed

* fix: filename parsing is correct now for onboarding with the last file sep

* Fix e2e test state checks that are diff on Linux

* Create onboarding project entirely through systemIOMachine

* Fix Windows path construction

* Make utility to verify a string is an onboarding value

* Little biome formatting suggestion fix

* Units onboarding step was not using OnboardingButtons

* Add type checking of next and previous status, fix useNextClick

* Update `OnboardingStatus` type on WASM side

* Make onboarding different on browser and web, placeholder component

* Show proof of concept with custom content per route

* Make text type args not insta dismiss when you click anywhere

* Make some utility hooks for the onboarding

* Update requestedProjectName along with requestedProjectName

* Build out a rough draft of desktop onboarding

* Remove unused onboarding route files

* Build out rough draft of browser onboarding content

* @jgomez720 browser flow feedback

* @jgomez420 desktop feedback

* tsc and lints

* Tweaks

* Import is dead, long live Add files

* What's up with my inability to type "highlight"?

* Codespell and String casting

* Update browser sample to be axial fan

* lint and tsc

* codespell again

* Remove unused nightmare function `useDemoCode`

* Add a few unit tests

* Update desktop to use bulk file creation from #6747

* Oops overwrote main.kcl on the modify with text-to-cad step

* Undo the dumb use of `sep` that I introduced

* Fix up project test

which was fragile to the number of steps in the onboarding smh

* Fix up onboarding flow test

* typo

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
2025-05-09 00:37:21 +00:00
9853353512 Update text-to-cad experimental label (#6786)
update text-to-cad label
2025-05-08 22:06:01 +00:00
7b8585f3c3 Add @web tag to new web-only tests for later CI use (#6784)
pierremtb/adhoc/add-web-tag-for-later
2025-05-08 17:58:05 -04:00
106 changed files with 2812 additions and 1498 deletions

View File

@ -9,10 +9,11 @@ VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
VITE_KC_SITE_APP_URL=https://app.dev.zoo.dev
VITE_KC_SKIP_AUTH=false
VITE_KC_CONNECTION_TIMEOUT_MS=5000
#VITE_KC_DEV_TOKEN="optional token from dev.zoo.dev to skip auth in the app"
#VITE_KC_DEV_TOKEN="optional token to skip auth in the app"
#token="required token for playwright. TODO: clean up env vars in #3973"
RUST_BACKTRACE=1
PYO3_PYTHON=/usr/local/bin/python3
#KITTYCAD_API_TOKEN="required token from dev.zoo.dev for engine testing"
#KITTYCAD_API_TOKEN="required token for engine testing"
FAIL_ON_CONSOLE_ERRORS=true

View File

@ -229,7 +229,6 @@ jobs:
max_attempts: 5
env:
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
TAB_API_URL: ${{ secrets.TAB_API_URL }}
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}

View File

@ -63,7 +63,7 @@ If you're not a Zoo employee you won't be able to access the dev environment, yo
### Development environment variables
The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `dev.zoo.dev`). There is an optional environment variable called `VITE_KC_DEV_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service.
The Copilot LSP plugin in the editor requires a Zoo API token to run. In production, we authenticate this with a token via cookie in the browser and device auth token in the desktop environment, but this token is inaccessible in the dev browser version because the cookie is considered "cross-site" (from `localhost` to `zoo.dev`). There is an optional environment variable called `VITE_KC_DEV_TOKEN` that you can populate with a dev token in a `.env.development.local` file to not check it into Git, which will use that token instead of other methods for the LSP service.
### Developing in Chrome
@ -198,15 +198,9 @@ For more information on fuzzing you can check out
### Playwright tests
You will need a `./e2e/playwright/playwright-secrets.env` file:
Prepare these system dependencies:
```bash
$ touch ./e2e/playwright/playwright-secrets.env
$ cat ./e2e/playwright/playwright-secrets.env
token=<dev.zoo.dev/account/api-tokens>
snapshottoken=<zoo.dev/account/api-tokens>
```
or use `export` to set the environment variables `token` and `snapshottoken`.
- Set $token from https://zoo.dev/account/api-tokens
#### Snapshot tests (Google Chrome on Ubuntu only)
@ -302,7 +296,7 @@ Which will run our suite of [Vitest unit](https://vitest.dev/) and [React Testin
Prepare these system dependencies:
- Set `$KITTYCAD_API_TOKEN` from https://dev.zoo.dev/account/api-tokens
- Set `$KITTYCAD_API_TOKEN` from https://zoo.dev/account/api-tokens
- Install `just` following [these instructions](https://just.systems/man/en/packages.html)
then run tests that target the KCL language:

View File

@ -15,12 +15,6 @@ once fixed in engine will just start working here with no language changes.
- **Import**: Right now you can import a file, even if that file has brep data
you cannot edit it, after v1, the engine will account for this.
- **Fillets**: Fillets cannot intersect, you will get an error. Only simple fillet
cases work currently.
- **Chamfers**: Chamfers cannot intersect, you will get an error. Only simple
chamfer cases work currently.
- **Appearance**: Changing the appearance on a loft does not work.
- **CSG Booleans**: Coplanar (bodies that share a plane) unions, subtractions, and intersections are not currently supported.

View File

@ -514,14 +514,18 @@ test.describe('Command bar tests', () => {
})
})
test(`Zoom to fit to shared model on web`, async ({ page, scene }) => {
if (process.env.PLATFORM !== 'web') {
// This test is web-only
return
}
await test.step(`Prepare and navigate to home page with query params`, async () => {
// a quad in the top left corner of the XZ plane (which is out of the current view)
const code = `sketch001 = startSketchOn(XZ)
test(
`Zoom to fit to shared model on web`,
{ tag: ['@web'] },
async ({ page, scene }) => {
if (process.env.PLATFORM !== 'web') {
// This test is web-only
// TODO: re-enable on CI as part of a new @web test suite
return
}
await test.step(`Prepare and navigate to home page with query params`, async () => {
// a quad in the top left corner of the XZ plane (which is out of the current view)
const code = `sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [-484.34, 484.95])
|> yLine(length = -69.1)
|> xLine(length = 66.84)
@ -529,26 +533,27 @@ profile001 = startProfile(sketch001, at = [-484.34, 484.95])
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
`
const targetURL = `?create-file&name=test&units=mm&code=${encodeURIComponent(btoa(code))}&ask-open-desktop`
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
const targetURL = `?create-file&name=test&units=mm&code=${encodeURIComponent(btoa(code))}&ask-open-desktop`
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
})
await test.step(`Submit the command`, async () => {
await page.getByTestId('continue-to-web-app-button').click()
await test.step(`Submit the command`, async () => {
await page.getByTestId('continue-to-web-app-button').click()
await scene.connectionEstablished()
await scene.connectionEstablished()
// This makes SystemIOMachineActors.createKCLFile run after EngineStream/firstPlay
await page.waitForTimeout(3000)
// This makes SystemIOMachineActors.createKCLFile run after EngineStream/firstPlay
await page.waitForTimeout(3000)
await page.getByTestId('command-bar-submit').click()
})
await page.getByTestId('command-bar-submit').click()
})
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
await expectPixelColor(page, [252, 252, 252], { x: 600, y: 260 }, 8)
})
})
await test.step(`Ensure we created the project and are in the modeling scene`, async () => {
await expectPixelColor(page, [252, 252, 252], { x: 600, y: 260 }, 8)
})
}
)
test(`Can add and edit a named parameter or constant`, async ({
page,

View File

@ -1,5 +1,5 @@
import type { Locator, Page } from '@playwright/test'
import { secrets } from '@e2e/playwright/secrets'
import { token } from '@e2e/playwright/test-utils'
export class SignInPageFixture {
public page: Page
@ -25,7 +25,7 @@ export class SignInPageFixture {
// Device flow: stolen from the tauri days
// https://github.com/KittyCAD/modeling-app/blob/d916c7987452e480719004e6d11fd2e595c7d0eb/e2e/tauri/specs/app.spec.ts#L19
const headers = {
Authorization: `Bearer ${secrets.token}`,
Authorization: `Bearer ${token}`,
Accept: 'application/json',
'Content-Type': 'application/json',
}

View File

@ -21,9 +21,8 @@ test.describe('Onboarding tests', () => {
},
})
const bracketComment = '// Shelf Bracket'
const tutorialWelcomeHeading = page.getByText(
'Welcome to Design Studio! This'
'Welcome to Zoo Design Studio'
)
const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
@ -64,7 +63,6 @@ test.describe('Onboarding tests', () => {
shouldNormalise: true,
})
await scene.connectionEstablished()
await expect(toolbar.startSketchBtn).toBeEnabled({ timeout: 15_000 })
})
await test.step('Go home and verify we still see the tutorial button, then begin it.', async () => {
@ -90,11 +88,8 @@ test.describe('Onboarding tests', () => {
// })
await test.step('Ensure we see the welcome screen in a new project', async () => {
await expect(toolbar.projectName).toContainText('Tutorial Project 00')
await expect(toolbar.projectName).toContainText('tutorial-project')
await expect(tutorialWelcomeHeading).toBeVisible()
await editor.expectEditor.toContain(bracketComment)
await scene.connectionEstablished()
await expect(toolbar.startSketchBtn).toBeEnabled({ timeout: 15_000 })
})
await test.step('Test the clicking through the onboarding flow', async () => {
@ -122,7 +117,7 @@ test.describe('Onboarding tests', () => {
})
})
await test.step('Resetting onboarding from inside project should always make a new one', async () => {
await test.step('Resetting onboarding from inside project should always overwrite `tutorial-project`', async () => {
await test.step('Reset onboarding from settings', async () => {
await userMenuButton.click()
await userMenuSettingsButton.click()
@ -131,44 +126,66 @@ test.describe('Onboarding tests', () => {
await restartOnboardingSettingsButton.click()
})
await test.step('Makes a new project', async () => {
await expect(toolbar.projectName).toContainText('Tutorial Project 01')
await test.step('Gets to the onboarding start', async () => {
await expect(toolbar.projectName).toContainText('tutorial-project')
await expect(tutorialWelcomeHeading).toBeVisible()
await editor.expectEditor.toContain(bracketComment)
await scene.connectionEstablished()
await expect(toolbar.startSketchBtn).toBeEnabled({ timeout: 15_000 })
})
await test.step('Dismiss the onboarding', async () => {
await postDismissToast.waitFor({ state: 'detached' })
await postDismissToast.waitFor({ state: 'hidden' })
await page.keyboard.press('Escape')
await expect(postDismissToast).toBeVisible()
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect.poll(() => page.url()).not.toContain('/onboarding')
})
})
await test.step('Resetting onboarding from home help menu makes a new project', async () => {
await test.step('Go home and reset onboarding from lower-right help menu', async () => {
await test.step('Verify no new projects were created', async () => {
await toolbar.logoLink.click()
await expect(homePage.tutorialBtn).not.toBeVisible()
await expect(
homePage.projectCard.getByText('Tutorial Project 00')
).toBeVisible()
await expect(
homePage.projectCard.getByText('Tutorial Project 01')
).toBeVisible()
await homePage.expectState({
projectCards: [
{ title: 'tutorial-project', fileCount: 7 },
{
title: 'testDefault',
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
})
await helpMenuButton.click()
await helpMenuRestartOnboardingButton.click()
await test.step('Resetting onboarding from home help menu overwrites the `tutorial-project`', async () => {
await helpMenuButton.click()
await helpMenuRestartOnboardingButton.click()
await test.step('Gets to the onboarding start', async () => {
await expect(toolbar.projectName).toContainText('tutorial-project')
await expect(tutorialWelcomeHeading).toBeVisible()
await scene.connectionEstablished()
})
await test.step('Makes a new project', async () => {
await expect(toolbar.projectName).toContainText('Tutorial Project 02')
await expect(tutorialWelcomeHeading).toBeVisible()
await editor.expectEditor.toContain(bracketComment)
await scene.connectionEstablished()
await expect(toolbar.startSketchBtn).toBeEnabled({ timeout: 15_000 })
await test.step('Dismiss the onboarding', async () => {
await postDismissToast.waitFor({ state: 'hidden' })
await page.keyboard.press('Escape')
await expect(postDismissToast).toBeVisible()
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect.poll(() => page.url()).not.toContain('/onboarding')
})
await test.step('Verify no new projects were created', async () => {
await toolbar.logoLink.click()
await expect(homePage.tutorialBtn).not.toBeVisible()
await homePage.expectState({
projectCards: [
{ title: 'tutorial-project', fileCount: 7 },
{
title: 'testDefault',
fileCount: 1,
},
],
sortBy: 'last-modified-desc',
})
})
})
})

View File

@ -1986,6 +1986,7 @@ test(
'Original project name persist after onboarding',
{ tag: '@electron' },
async ({ page, toolbar }, testInfo) => {
const nextButton = page.getByTestId('onboarding-next')
await page.setBodyDimensions({ width: 1200, height: 500 })
const getAllProjects = () => page.getByTestId('project-link').all()
@ -2000,10 +2001,10 @@ test(
await page.getByTestId('user-settings').click()
await page.getByRole('button', { name: 'Replay Onboarding' }).click()
const numberOfOnboardingSteps = 12
for (let clicks = 0; clicks < numberOfOnboardingSteps; clicks++) {
await page.getByTestId('onboarding-next').click()
while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.click()
}
await nextButton.click()
await page.getByTestId('project-sidebar-toggle').click()
})
@ -2013,7 +2014,7 @@ test(
})
await test.step('Should show the original project called wrist brace', async () => {
const projectNames = ['Tutorial Project 00', 'wrist brace']
const projectNames = ['tutorial-project', 'wrist brace']
for (const [index, projectLink] of (await getAllProjects()).entries()) {
await expect(projectLink).toContainText(projectNames[index])
}

View File

@ -1,28 +0,0 @@
import { readFileSync } from 'fs'
const secrets: Record<string, string> = {}
const secretsPath = './e2e/playwright/playwright-secrets.env'
try {
const file = readFileSync(secretsPath, 'utf8')
file
.split('\n')
.filter((line) => line && line.length > 1)
.forEach((line) => {
// Allow line comments.
if (line.trimStart().startsWith('#')) return
const [key, value] = line.split('=')
// prefer env vars over secrets file
secrets[key] = process.env[key] || (value as any).replaceAll('"', '')
})
} catch (error: unknown) {
void error
// probably running in CI
console.warn(
`Error reading ${secretsPath}; environment variables will be used`
)
}
secrets.token = secrets.token || process.env.token || ''
secrets.snapshottoken = secrets.snapshottoken || process.env.snapshottoken || ''
// add more env vars here to make them available in CI
export { secrets }

View File

@ -13,31 +13,33 @@ test.describe('Share link tests', () => {
showsErrorOnWindows: true,
},
].forEach(({ codeLength, showsErrorOnWindows }) => {
test(`Open in desktop app with ${codeLength}-long code ${isWindows && showsErrorOnWindows ? 'shows error' : "doesn't show error"}`, async ({
page,
}) => {
if (process.env.PLATFORM !== 'web') {
// This test is web-only
// TODO: re-enable on CI as part of a new web test suite
return
}
test(
`Open in desktop app with ${codeLength}-long code ${isWindows && showsErrorOnWindows ? 'shows error' : "doesn't show error"}`,
{ tag: ['@web'] },
async ({ page }) => {
if (process.env.PLATFORM !== 'web') {
// This test is web-only
// TODO: re-enable on CI as part of a new @web test suite
return
}
const code = Array(codeLength).fill('0').join('')
const targetURL = `?create-file=true&browser=test&code=${code}&ask-open-desktop=true`
expect(targetURL.length).toEqual(codeLength + 58)
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
const button = page.getByRole('button', { name: 'Open in desktop app' })
await button.click()
const toastError = page.getByText(
'The URL is too long to open in the desktop app on Windows'
)
if (isWindows && showsErrorOnWindows) {
await expect(toastError).toBeVisible()
} else {
await expect(toastError).not.toBeVisible()
// TODO: check if we could verify the deep link dialog shows up
const code = Array(codeLength).fill('0').join('')
const targetURL = `?create-file=true&browser=test&code=${code}&ask-open-desktop=true`
expect(targetURL.length).toEqual(codeLength + 58)
await page.goto(page.url() + targetURL)
expect(page.url()).toContain(targetURL)
const button = page.getByRole('button', { name: 'Open in desktop app' })
await button.click()
const toastError = page.getByText(
'The URL is too long to open in the desktop app on Windows'
)
if (isWindows && showsErrorOnWindows) {
await expect(toastError).toBeVisible()
} else {
await expect(toastError).not.toBeVisible()
// TODO: check if we could verify the deep link dialog shows up
}
}
})
)
})
})

View File

@ -5,7 +5,7 @@ import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
import {
getUtils,
headerMasks,
networkingMasks,
lowerRightMasks,
settingsToToml,
} from '@e2e/playwright/test-utils'
import { expect, test } from '@e2e/playwright/zoo-test'
@ -88,7 +88,7 @@ const extrudeDefaultPlane = async (
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
await u.openKclCodePanel()
}
@ -173,7 +173,7 @@ test(
await page.waitForTimeout(500)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
const lineEndClick = () =>
@ -200,7 +200,7 @@ test(
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
await endOfTangentClk()
@ -210,7 +210,7 @@ test(
await threePointArcMidPointMv()
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
await threePointArcMidPointClk()
await page.waitForTimeout(100)
@ -219,7 +219,7 @@ test(
await page.waitForTimeout(500)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
await threePointArcEndPointClk()
@ -239,7 +239,7 @@ test(
await page.waitForTimeout(500)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
await arcEndClk()
}
@ -286,7 +286,7 @@ test(
// Ensure the draft rectangle looks the same as it usually does
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
}
)
@ -328,7 +328,7 @@ test(
// Ensure the draft rectangle looks the same as it usually does
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
await expect(page.locator('.cm-content')).toHaveText(
`sketch001 = startSketchOn(XZ)profile001 = circle(sketch001, center = [366.89, -62.01], radius = 1)`
@ -395,7 +395,7 @@ test.describe(
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
await u.doAndWaitForImageDiff(
@ -408,7 +408,7 @@ test.describe(
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
})
@ -490,7 +490,7 @@ test.describe(
// screen shot should show the sketch
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
// exit sketch
@ -504,7 +504,7 @@ test.describe(
// second screen shot should look almost identical, i.e. scale should be the same.
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
})
}
@ -563,7 +563,7 @@ part002 = startSketchOn(part001, face = seg01)
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
}
)
@ -599,7 +599,7 @@ test(
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
}
)
@ -636,7 +636,7 @@ test(
await expect(page).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
}
)
@ -701,7 +701,7 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask: [...headerMasks(page), ...networkingMasks(page)],
mask: [...headerMasks(page), ...lowerRightMasks(page)],
})
})
@ -722,7 +722,7 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask: [...headerMasks(page), ...networkingMasks(page)],
mask: [...headerMasks(page), ...lowerRightMasks(page)],
})
})
@ -761,7 +761,7 @@ test.describe('Grid visibility', { tag: '@snapshot' }, () => {
await expect(stream).toHaveScreenshot({
maxDiffPixels: 100,
mask: [...headerMasks(page), ...networkingMasks(page)],
mask: [...headerMasks(page), ...lowerRightMasks(page)],
})
})
})
@ -829,7 +829,7 @@ test('theme persists', async ({ page, context }) => {
await expect(page, 'expect screenshot to have light theme').toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
})
@ -870,7 +870,7 @@ sweepSketch = startSketchOn(XY)
await expect(page, 'expect small color widget').toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
})
@ -923,7 +923,7 @@ sweepSketch = startSketchOn(XY)
'expect small color widget to have window open'
).toHaveScreenshot({
maxDiffPixels: 100,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -1,7 +1,6 @@
import type { SaveSettingsPayload } from '@src/lib/settings/settingsTypes'
import { Themes } from '@src/lib/theme'
import type { DeepPartial } from '@src/lib/types'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
import type { Settings } from '@rust/kcl-lib/bindings/Settings'
@ -29,28 +28,6 @@ export const TEST_SETTINGS: DeepPartial<Settings> = {
},
}
export const TEST_SETTINGS_ONBOARDING_USER_MENU: DeepPartial<Settings> = {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
onboarding_status: ONBOARDING_SUBPATHS.USER_MENU,
},
}
export const TEST_SETTINGS_ONBOARDING_EXPORT: DeepPartial<Settings> = {
...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, onboarding_status: ONBOARDING_SUBPATHS.EXPORT },
}
export const TEST_SETTINGS_ONBOARDING_PARAMETRIC_MODELING: DeepPartial<Settings> =
{
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
onboarding_status: ONBOARDING_SUBPATHS.PARAMETRIC_MODELING,
},
}
export const TEST_SETTINGS_ONBOARDING_START: DeepPartial<Settings> = {
...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, onboarding_status: '' },

View File

@ -13,11 +13,15 @@ import fsp from 'fs/promises'
import pixelMatch from 'pixelmatch'
import type { Protocol } from 'playwright-core/types/protocol'
import { PNG } from 'pngjs'
import dotenv from 'dotenv'
const NODE_ENV = process.env.NODE_ENV || 'development'
dotenv.config({ path: [`.env.${NODE_ENV}.local`, `.env.${NODE_ENV}`] })
export const token = process.env.token || ''
import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration'
import { isErrorWhitelisted } from '@e2e/playwright/lib/console-error-whitelist'
import { secrets } from '@e2e/playwright/secrets'
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
import { test } from '@e2e/playwright/zoo-test'
@ -31,8 +35,9 @@ export const headerMasks = (page: Page) => [
page.locator('#sidebar-bottom-ribbon'),
]
export const networkingMasks = (page: Page) => [
export const lowerRightMasks = (page: Page) => [
page.getByTestId('network-toggle'),
page.getByTestId('billing-remaining-bar'),
]
export type TestColor = [number, number, number]
@ -890,7 +895,7 @@ export async function setup(
localStorage.setItem('PLAYWRIGHT_TEST_DIR', PLAYWRIGHT_TEST_DIR)
},
{
token: secrets.token,
token,
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: {
@ -918,7 +923,7 @@ export async function setup(
await context.addCookies([
{
name: COOKIE_NAME,
value: secrets.token,
value: token,
path: '/',
domain: 'localhost',
secure: true,

View File

@ -20,7 +20,7 @@ import {
createProject,
executorInputPath,
getUtils,
networkingMasks,
lowerRightMasks,
settingsToToml,
tomlToSettings,
} from '@e2e/playwright/test-utils'
@ -1061,7 +1061,7 @@ fn cube`
'toggle-settings-initial.png',
{
maxDiffPixels: 15,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
}
)
@ -1078,7 +1078,7 @@ fn cube`
'toggle-settings-initial.png',
{
maxDiffPixels: 15,
mask: networkingMasks(page),
mask: lowerRightMasks(page),
}
)
})

1
package-lock.json generated
View File

@ -2492,7 +2492,6 @@
},
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0",
"extraneous": true,
"inBundle": true,
"license": "MIT",
"engines": {

View File

@ -137,8 +137,8 @@
"test:unit:components": "jest -c jest-component-unit-tests/jest.config.ts --rootDir jest-component-unit-tests/",
"test:unit:kcl-samples": "vitest run --mode development ./src/lang/kclSamples.test.ts",
"test:playwright:electron": "playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot",
"test:playwright:electron:local": "npm run tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot --grep-invert=\"$(curl --silent https://test-analysis-bot.hawk-dinosaur.ts.net/projects/KittyCAD/modeling-app/tests/disabled/regex)\"",
"test:playwright:electron:local-engine": "npm run tronb:vite:dev && NODE_ENV=development playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot|@skipLocalEngine' --grep-invert=\"$(curl --silent https://test-analysis-bot.hawk-dinosaur.ts.net/projects/KittyCAD/modeling-app/tests/disabled/regex)\"",
"test:playwright:electron:local": "npm run tronb:vite:dev && playwright test --config=playwright.electron.config.ts --grep-invert=@snapshot --grep-invert=\"$(curl --silent https://test-analysis-bot.hawk-dinosaur.ts.net/projects/KittyCAD/modeling-app/tests/disabled/regex)\"",
"test:playwright:electron:local-engine": "npm run tronb:vite:dev && playwright test --config=playwright.electron.config.ts --grep-invert='@snapshot|@skipLocalEngine' --grep-invert=\"$(curl --silent https://test-analysis-bot.hawk-dinosaur.ts.net/projects/KittyCAD/modeling-app/tests/disabled/regex)\"",
"test:unit:local": "npm run simpleserver:bg && npm run test:unit; kill-port 3000",
"test:unit:kcl-samples:local": "npm run simpleserver:bg && npm run test:unit:kcl-samples; kill-port 3000"
},

View File

@ -1295,13 +1295,20 @@ impl Node<CallExpressionKw> {
// Build a hashmap from argument labels to the final evaluated values.
let mut fn_args = IndexMap::with_capacity(self.arguments.len());
let mut errors = Vec::new();
for arg_expr in &self.arguments {
let source_range = SourceRange::from(arg_expr.arg.clone());
let metadata = Metadata { source_range };
let value = ctx
.execute_expr(&arg_expr.arg, exec_state, &metadata, &[], StatementKind::Expression)
.await?;
fn_args.insert(arg_expr.label.name.clone(), Arg::new(value, source_range));
let arg = Arg::new(value, source_range);
match &arg_expr.label {
Some(l) => {
fn_args.insert(l.name.clone(), arg);
}
None => errors.push(arg),
}
}
let fn_args = fn_args; // remove mutability
@ -1321,6 +1328,7 @@ impl Node<CallExpressionKw> {
KwArgs {
unlabeled,
labeled: fn_args,
errors,
},
self.into(),
ctx.clone(),
@ -1894,6 +1902,44 @@ fn type_check_params_kw(
}
}
if !args.errors.is_empty() {
let actuals = args.labeled.keys();
let formals: Vec<_> = function_expression
.params
.iter()
.filter_map(|p| {
if !p.labeled {
return None;
}
let name = &p.identifier.name;
if actuals.clone().any(|a| a == name) {
return None;
}
Some(format!("`{name}`"))
})
.collect();
let suggestion = if formals.is_empty() {
String::new()
} else {
format!("; suggested labels: {}", formals.join(", "))
};
let mut errors = args.errors.iter().map(|e| {
CompilationError::err(
e.source_range,
format!("This argument needs a label, but it doesn't have one{suggestion}"),
)
});
let first = errors.next().unwrap();
errors.for_each(|e| exec_state.err(e));
return Err(KclError::Semantic(first.into()));
}
if let Some(arg) = &mut args.unlabeled {
if let Some(p) = function_expression.params.iter().find(|p| !p.labeled) {
if let Some(ty) = &p.type_ {
@ -2313,6 +2359,7 @@ mod test {
let args = KwArgs {
unlabeled: None,
labeled,
errors: Vec::new(),
};
let exec_ctxt = ExecutorContext {
engine: Arc::new(Box::new(
@ -2552,4 +2599,30 @@ a = foo()
parse_execute(program).await.unwrap_err();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_sensible_error_when_missing_equals_in_kwarg() {
for (i, call) in ["f(x=1,y)", "f(x=1,3,z)", "f(x=1,y,z=1)", "f(x=1, 3 + 4, z=1)"]
.into_iter()
.enumerate()
{
let program = format!(
"fn foo() {{ return 0 }}
y = 42
z = 0
fn f(x, y, z) {{ return 0 }}
{call}"
);
let err = parse_execute(&program).await.unwrap_err();
let msg = err.message();
assert!(
msg.contains("This argument needs a label, but it doesn't have one"),
"failed test {i}: {msg}"
);
assert!(msg.contains("`y`"), "failed test {i}, missing `y`: {msg}");
if i == 0 {
assert!(msg.contains("`z`"), "failed test {i}, missing `z`: {msg}");
}
}
}
}

View File

@ -488,7 +488,9 @@ impl CallExpressionKw {
}
hasher.update(slf.arguments.len().to_ne_bytes());
for argument in slf.arguments.iter_mut() {
hasher.update(argument.label.compute_digest());
if let Some(l) = &mut argument.label {
hasher.update(l.compute_digest());
}
hasher.update(argument.arg.compute_digest());
}
});

View File

@ -460,10 +460,12 @@ impl Node<Program> {
crate::walk::Node::CallExpressionKw(call) => {
if call.inner.callee.inner.name.inner.name == "appearance" {
for arg in &call.arguments {
if arg.label.inner.name == "color" {
// Get the value of the argument.
if let Expr::Literal(literal) = &arg.arg {
add_color(literal);
if let Some(l) = &arg.label {
if l.inner.name == "color" {
// Get the value of the argument.
if let Expr::Literal(literal) = &arg.arg {
add_color(literal);
}
}
}
}
@ -1872,7 +1874,7 @@ pub struct CallExpressionKw {
#[ts(export)]
#[serde(tag = "type")]
pub struct LabeledArg {
pub label: Node<Identifier>,
pub label: Option<Node<Identifier>>,
pub arg: Expr,
}
@ -1917,7 +1919,7 @@ impl CallExpressionKw {
self.unlabeled
.iter()
.map(|e| (None, e))
.chain(self.arguments.iter().map(|arg| (Some(&arg.label), &arg.arg)))
.chain(self.arguments.iter().map(|arg| (arg.label.as_ref(), &arg.arg)))
}
pub fn replace_value(&mut self, source_range: SourceRange, new_value: Expr) {

View File

@ -2714,13 +2714,18 @@ fn pipe_sep(i: &mut TokenSlice) -> PResult<()> {
}
fn labeled_argument(i: &mut TokenSlice) -> PResult<LabeledArg> {
separated_pair(
terminated(nameable_identifier, opt(whitespace)),
terminated(one_of((TokenType::Operator, "=")), opt(whitespace)),
(
opt((
terminated(nameable_identifier, opt(whitespace)),
terminated(one_of((TokenType::Operator, "=")), opt(whitespace)),
)),
expression,
)
.map(|(label, arg)| LabeledArg { label, arg })
.parse_next(i)
.map(|(label, arg)| LabeledArg {
label: label.map(|(l, _)| l),
arg,
})
.parse_next(i)
}
/// A type of a function argument.
@ -3040,6 +3045,7 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
return Ok(result);
}
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
enum ArgPlace {
NonCode(Node<NonCodeNode>),
@ -3068,24 +3074,17 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
}
ArgPlace::UnlabeledArg(arg) => {
let followed_by_equals = peek((opt(whitespace), equals)).parse_next(i).is_ok();
let err = if followed_by_equals {
ErrMode::Cut(
if followed_by_equals {
return Err(ErrMode::Cut(
CompilationError::fatal(
SourceRange::from(arg),
"This argument has a label, but no value. Put some value after the equals sign",
)
.into(),
)
));
} else {
ErrMode::Cut(
CompilationError::fatal(
SourceRange::from(arg),
"This argument needs a label, but it doesn't have one",
)
.into(),
)
};
return Err(err);
args.push(LabeledArg { label: None, arg });
}
}
}
Ok((args, non_code_nodes))
@ -3098,7 +3097,9 @@ fn fn_call_kw(i: &mut TokenSlice) -> PResult<Node<CallExpressionKw>> {
// Validate there aren't any duplicate labels.
let mut counted_labels = IndexMap::with_capacity(args.len());
for arg in &args {
*counted_labels.entry(&arg.label.inner.name).or_insert(0) += 1;
if let Some(l) = &arg.label {
*counted_labels.entry(&l.inner.name).or_insert(0) += 1;
}
}
if let Some((duplicated, n)) = counted_labels.iter().find(|(_label, n)| n > &&1) {
let msg = format!(
@ -4923,27 +4924,6 @@ bar = 1
crate::parsing::top_level_parse(some_program_string).unwrap(); // Updated import path
}
#[test]
fn test_sensible_error_when_missing_equals_in_kwarg() {
for (i, program) in ["f(x=1,y)", "f(x=1,y,z)", "f(x=1,y,z=1)", "f(x=1, y, z=1)"]
.into_iter()
.enumerate()
{
let tokens = crate::parsing::token::lex(program, ModuleId::default()).unwrap();
let err = fn_call_kw.parse(tokens.as_slice()).unwrap_err();
let cause = err.inner().cause.as_ref().unwrap();
assert_eq!(
cause.message, "This argument needs a label, but it doesn't have one",
"failed test {i}: {program}"
);
assert_eq!(
cause.source_range.start(),
program.find("y").unwrap(),
"failed test {i}: {program}"
);
}
}
#[test]
fn test_sensible_error_when_missing_rhs_of_kw_arg() {
for (i, program) in ["f(x, y=)"].into_iter().enumerate() {

View File

@ -496,43 +496,84 @@ pub enum OnboardingStatus {
/// The user has dismissed onboarding.
Dismissed,
// Routes
#[serde(rename = "/")]
#[display("/")]
Index,
#[serde(rename = "/camera")]
#[display("/camera")]
Camera,
#[serde(rename = "/streaming")]
#[display("/streaming")]
Streaming,
#[serde(rename = "/editor")]
#[display("/editor")]
Editor,
#[serde(rename = "/parametric-modeling")]
#[display("/parametric-modeling")]
ParametricModeling,
#[serde(rename = "/interactive-numbers")]
#[display("/interactive-numbers")]
InteractiveNumbers,
#[serde(rename = "/command-k")]
#[display("/command-k")]
CommandK,
#[serde(rename = "/user-menu")]
#[display("/user-menu")]
UserMenu,
#[serde(rename = "/project-menu")]
#[display("/project-menu")]
ProjectMenu,
#[serde(rename = "/export")]
#[display("/export")]
Export,
#[serde(rename = "/sketching")]
#[display("/sketching")]
Sketching,
#[serde(rename = "/future-work")]
#[display("/future-work")]
FutureWork,
// Desktop Routes
#[serde(rename = "/desktop")]
#[display("/desktop")]
DesktopWelcome,
#[serde(rename = "/desktop/scene")]
#[display("/desktop/scene")]
DesktopScene,
#[serde(rename = "/desktop/toolbar")]
#[display("/desktop/toolbar")]
DesktopToolbar,
#[serde(rename = "/desktop/text-to-cad")]
#[display("/desktop/text-to-cad")]
DesktopTextToCadWelcome,
#[serde(rename = "/desktop/text-to-cad-prompt")]
#[display("/desktop/text-to-cad-prompt")]
DesktopTextToCadPrompt,
#[serde(rename = "/desktop/feature-tree-pane")]
#[display("/desktop/feature-tree-pane")]
DesktopFeatureTreePane,
#[serde(rename = "/desktop/code-pane")]
#[display("/desktop/code-pane")]
DesktopCodePane,
#[serde(rename = "/desktop/project-pane")]
#[display("/desktop/project-pane")]
DesktopProjectFilesPane,
#[serde(rename = "/desktop/other-panes")]
#[display("/desktop/other-panes")]
DesktopOtherPanes,
#[serde(rename = "/desktop/prompt-to-edit")]
#[display("/desktop/prompt-to-edit")]
DesktopPromptToEditWelcome,
#[serde(rename = "/desktop/prompt-to-edit-prompt")]
#[display("/desktop/prompt-to-edit-prompt")]
DesktopPromptToEditPrompt,
#[serde(rename = "/desktop/prompt-to-edit-result")]
#[display("/desktop/prompt-to-edit-result")]
DesktopPromptToEditResult,
#[serde(rename = "/desktop/imports")]
#[display("/desktop/imports")]
DesktopImports,
#[serde(rename = "/desktop/exports")]
#[display("/desktop/exports")]
DesktopExports,
#[serde(rename = "/desktop/conclusion")]
#[display("/desktop/conclusion")]
DesktopConclusion,
// Browser Routes
#[serde(rename = "/browser")]
#[display("/browser")]
BrowserWelcome,
#[serde(rename = "/browser/scene")]
#[display("/browser/scene")]
BrowserScene,
#[serde(rename = "/browser/toolbar")]
#[display("/browser/toolbar")]
BrowserToolbar,
#[serde(rename = "/browser/text-to-cad")]
#[display("/browser/text-to-cad")]
BrowserTextToCadWelcome,
#[serde(rename = "/browser/text-to-cad-prompt")]
#[display("/browser/text-to-cad-prompt")]
BrowserTextToCadPrompt,
#[serde(rename = "/browser/feature-tree-pane")]
#[display("/browser/feature-tree-pane")]
BrowserFeatureTreePane,
#[serde(rename = "/browser/prompt-to-edit")]
#[display("/browser/prompt-to-edit")]
BrowserPromptToEditWelcome,
#[serde(rename = "/browser/prompt-to-edit-prompt")]
#[display("/browser/prompt-to-edit-prompt")]
BrowserPromptToEditPrompt,
#[serde(rename = "/browser/prompt-to-edit-result")]
#[display("/browser/prompt-to-edit-result")]
BrowserPromptToEditResult,
#[serde(rename = "/browser/conclusion")]
#[display("/browser/conclusion")]
BrowserConclusion,
}
fn is_default<T: Default + PartialEq>(t: &T) -> bool {

View File

@ -62,6 +62,7 @@ pub struct KwArgs {
pub unlabeled: Option<Arg>,
/// Labeled args.
pub labeled: IndexMap<String, Arg>,
pub errors: Vec<Arg>,
}
impl KwArgs {

View File

@ -88,6 +88,7 @@ async fn call_map_closure(
let kw_args = KwArgs {
unlabeled: Some(Arg::new(input, source_range)),
labeled: Default::default(),
errors: Vec::new(),
};
let args = Args::new_kw(
kw_args,
@ -233,6 +234,7 @@ async fn call_reduce_closure(
let kw_args = KwArgs {
unlabeled: Some(Arg::new(elem, source_range)),
labeled,
errors: Vec::new(),
};
let reduce_fn_args = Args::new_kw(
kw_args,

View File

@ -430,6 +430,7 @@ async fn make_transform<T: GeometryTrait>(
let kw_args = KwArgs {
unlabeled: Some(Arg::new(repetition_num, source_range)),
labeled: Default::default(),
errors: Vec::new(),
};
let transform_fn_args = Args::new_kw(
kw_args,

View File

@ -405,9 +405,13 @@ impl CallExpressionKw {
impl LabeledArg {
fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
let label = &self.label.name;
let arg = self.arg.recast(options, indentation_level, ctxt);
format!("{label} = {arg}")
let mut result = String::new();
if let Some(l) = &self.label {
result.push_str(&l.name);
result.push_str(" = ");
}
result.push_str(&self.arg.recast(options, indentation_level, ctxt));
result
}
}

View File

@ -77,8 +77,8 @@ description: Artifact commands subtract_cylinder_from_cube.kcl
"type": "move_path_pen",
"path": "[uuid]",
"to": {
"x": -10.0,
"y": -10.0,
"x": -8.0,
"y": -8.0,
"z": 0.0
}
}
@ -106,8 +106,8 @@ description: Artifact commands subtract_cylinder_from_cube.kcl
"segment": {
"type": "line",
"end": {
"x": 10.0,
"y": -10.0,
"x": 12.0,
"y": -8.0,
"z": 0.0
},
"relative": false
@ -123,8 +123,8 @@ description: Artifact commands subtract_cylinder_from_cube.kcl
"segment": {
"type": "line",
"end": {
"x": 10.0,
"y": 10.0,
"x": 12.0,
"y": 12.0,
"z": 0.0
},
"relative": false
@ -140,8 +140,8 @@ description: Artifact commands subtract_cylinder_from_cube.kcl
"segment": {
"type": "line",
"end": {
"x": -10.0,
"y": 10.0,
"x": -8.0,
"y": 12.0,
"z": 0.0
},
"relative": false
@ -412,8 +412,8 @@ description: Artifact commands subtract_cylinder_from_cube.kcl
},
"y_axis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
"y": 0.0,
"z": 1.0
},
"size": 60.0,
"clobber": false,
@ -439,8 +439,8 @@ description: Artifact commands subtract_cylinder_from_cube.kcl
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": 0.0,
"z": 1.0
"y": -1.0,
"z": 0.0
}
}
},
@ -453,8 +453,8 @@ description: Artifact commands subtract_cylinder_from_cube.kcl
"segment": {
"type": "arc",
"center": {
"x": 2.0,
"y": 2.0
"x": 5.0,
"y": 5.0
},
"radius": 2.0,
"start": {
@ -476,8 +476,8 @@ description: Artifact commands subtract_cylinder_from_cube.kcl
"type": "move_path_pen",
"path": "[uuid]",
"to": {
"x": 4.0,
"y": 2.0,
"x": 7.0,
"y": 5.0,
"z": 0.0
}
}
@ -507,8 +507,8 @@ description: Artifact commands subtract_cylinder_from_cube.kcl
"adjust_camera": false,
"planar_normal": {
"x": 0.0,
"y": 0.0,
"z": 1.0
"y": -1.0,
"z": 0.0
}
}
},
@ -518,7 +518,7 @@ description: Artifact commands subtract_cylinder_from_cube.kcl
"command": {
"type": "extrude",
"target": "[uuid]",
"distance": 5.0,
"distance": 34.0,
"faces": null,
"opposite": "None"
}
@ -594,6 +594,30 @@ description: Artifact commands subtract_cylinder_from_cube.kcl
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [],
"command": {
"type": "set_object_transform",
"object_id": "[uuid]",
"transforms": [
{
"translate": {
"property": {
"x": 0.0,
"y": 0.0,
"z": 3.14
},
"set": false,
"is_local": true
},
"rotate_rpy": null,
"rotate_angle_axis": null,
"scale": null
}
]
}
},
{
"cmdId": "[uuid]",
"range": [],

View File

@ -16,8 +16,8 @@ flowchart LR
1["Plane<br>[27, 44, 0]"]
2["Plane<br>[372, 389, 0]"]
12["Sweep Extrusion<br>[306, 326, 0]"]
13["Sweep Extrusion<br>[436, 455, 0]"]
14["CompositeSolid Subtract<br>[468, 504, 0]"]
13["Sweep Extrusion<br>[436, 456, 0]"]
14["CompositeSolid Subtract<br>[494, 530, 0]"]
15[Wall]
16[Wall]
17[Wall]

View File

@ -765,24 +765,24 @@ description: Result of parsing subtract_cylinder_from_cube.kcl
{
"commentStart": 0,
"end": 0,
"raw": "0",
"raw": "2",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"value": 2.0,
"suffix": "None"
}
},
{
"commentStart": 0,
"end": 0,
"raw": "0",
"raw": "2",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 0.0,
"value": 2.0,
"suffix": "None"
}
}
@ -867,7 +867,7 @@ description: Result of parsing subtract_cylinder_from_cube.kcl
"name": {
"commentStart": 0,
"end": 0,
"name": "XY",
"name": "XZ",
"start": 0,
"type": "Identifier"
},
@ -894,24 +894,24 @@ description: Result of parsing subtract_cylinder_from_cube.kcl
{
"commentStart": 0,
"end": 0,
"raw": "2",
"raw": "5",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 2.0,
"value": 5.0,
"suffix": "None"
}
},
{
"commentStart": 0,
"end": 0,
"raw": "2",
"raw": "5",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 2.0,
"value": 5.0,
"suffix": "None"
}
}
@ -981,12 +981,12 @@ description: Result of parsing subtract_cylinder_from_cube.kcl
"arg": {
"commentStart": 0,
"end": 0,
"raw": "5",
"raw": "34",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 5.0,
"value": 34.0,
"suffix": "None"
}
}
@ -1013,6 +1013,53 @@ description: Result of parsing subtract_cylinder_from_cube.kcl
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
{
"arguments": [
{
"type": "LabeledArg",
"label": {
"commentStart": 0,
"end": 0,
"name": "z",
"start": 0,
"type": "Identifier"
},
"arg": {
"commentStart": 0,
"end": 0,
"raw": "3.14",
"start": 0,
"type": "Literal",
"type": "Literal",
"value": {
"value": 3.14,
"suffix": "None"
}
}
}
],
"callee": {
"abs_path": false,
"commentStart": 0,
"end": 0,
"name": {
"commentStart": 0,
"end": 0,
"name": "translate",
"start": 0,
"type": "Identifier"
},
"path": [],
"start": 0,
"type": "Name"
},
"commentStart": 0,
"end": 0,
"start": 0,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
}
],
"commentStart": 0,

View File

@ -8,9 +8,10 @@ fn cube(center) {
|> extrude(length = 10)
}
part001 = cube(center = [0, 0])
part002 = startSketchOn(XY)
|> circle(center = [2, 2], radius = 2)
|> extrude(length = 5)
part001 = cube(center = [2, 2])
part002 = startSketchOn(XZ)
|> circle(center = [5, 5], radius = 2)
|> extrude(length = 34)
|> translate(z = 3.14)
fullPart = subtract([part001], tools=[part002])

View File

@ -62,7 +62,7 @@ description: Operations executed subtract_cylinder_from_cube.kcl
"value": [
{
"type": "Number",
"value": 0.0,
"value": 2.0,
"ty": {
"type": "Default",
"len": {
@ -75,7 +75,7 @@ description: Operations executed subtract_cylinder_from_cube.kcl
},
{
"type": "Number",
"value": 0.0,
"value": 2.0,
"ty": {
"type": "Default",
"len": {
@ -112,7 +112,7 @@ description: Operations executed subtract_cylinder_from_cube.kcl
"length": {
"value": {
"type": "Number",
"value": 5.0,
"value": 34.0,
"ty": {
"type": "Default",
"len": {

View File

@ -53,13 +53,13 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
"sourceRange": []
},
"from": [
-10.0,
-10.0
-8.0,
-8.0
],
"tag": null,
"to": [
10.0,
-10.0
12.0,
-8.0
],
"type": "ToPoint",
"units": {
@ -72,13 +72,13 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
"sourceRange": []
},
"from": [
10.0,
-10.0
12.0,
-8.0
],
"tag": null,
"to": [
10.0,
10.0
12.0,
12.0
],
"type": "ToPoint",
"units": {
@ -91,13 +91,13 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
"sourceRange": []
},
"from": [
10.0,
10.0
12.0,
12.0
],
"tag": null,
"to": [
-10.0,
10.0
-8.0,
12.0
],
"type": "ToPoint",
"units": {
@ -110,13 +110,13 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
"sourceRange": []
},
"from": [
-10.0,
10.0
-8.0,
12.0
],
"tag": null,
"to": [
-10.0,
-10.0
-8.0,
-8.0
],
"type": "ToPoint",
"units": {
@ -156,12 +156,12 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
},
"start": {
"from": [
-10.0,
-10.0
-8.0,
-8.0
],
"to": [
-10.0,
-10.0
-8.0,
-8.0
],
"units": {
"type": "Mm"
@ -233,13 +233,13 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
"sourceRange": []
},
"from": [
-10.0,
-10.0
-8.0,
-8.0
],
"tag": null,
"to": [
10.0,
-10.0
12.0,
-8.0
],
"type": "ToPoint",
"units": {
@ -252,13 +252,13 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
"sourceRange": []
},
"from": [
10.0,
-10.0
12.0,
-8.0
],
"tag": null,
"to": [
10.0,
10.0
12.0,
12.0
],
"type": "ToPoint",
"units": {
@ -271,13 +271,13 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
"sourceRange": []
},
"from": [
10.0,
10.0
12.0,
12.0
],
"tag": null,
"to": [
-10.0,
10.0
-8.0,
12.0
],
"type": "ToPoint",
"units": {
@ -290,13 +290,13 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
"sourceRange": []
},
"from": [
-10.0,
10.0
-8.0,
12.0
],
"tag": null,
"to": [
-10.0,
-10.0
-8.0,
-8.0
],
"type": "ToPoint",
"units": {
@ -336,12 +336,12 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
},
"start": {
"from": [
-10.0,
-10.0
-8.0,
-8.0
],
"to": [
-10.0,
-10.0
-8.0,
-8.0
],
"units": {
"type": "Mm"
@ -393,18 +393,18 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
},
"ccw": true,
"center": [
2.0,
2.0
5.0,
5.0
],
"from": [
4.0,
2.0
7.0,
5.0
],
"radius": 2.0,
"tag": null,
"to": [
4.0,
2.0
7.0,
5.0
],
"type": "Circle",
"units": {
@ -424,7 +424,7 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
}
},
"type": "plane",
"value": "XY",
"value": "XZ",
"xAxis": {
"x": 1.0,
"y": 0.0,
@ -435,8 +435,8 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0,
"y": 0.0,
"z": 1.0,
"units": {
"type": "Unknown"
}
@ -444,12 +444,12 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
},
"start": {
"from": [
4.0,
2.0
7.0,
5.0
],
"to": [
4.0,
2.0
7.0,
5.0
],
"units": {
"type": "Mm"
@ -466,7 +466,7 @@ description: Variables in memory after executing subtract_cylinder_from_cube.kcl
"type": "Mm"
}
},
"height": 5.0,
"height": 34.0,
"startCapId": "[uuid]",
"endCapId": "[uuid]",
"units": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -12,9 +12,10 @@ fn cube(center) {
|> extrude(length = 10)
}
part001 = cube(center = [0, 0])
part002 = startSketchOn(XY)
|> circle(center = [2, 2], radius = 2)
|> extrude(length = 5)
part001 = cube(center = [2, 2])
part002 = startSketchOn(XZ)
|> circle(center = [5, 5], radius = 2)
|> extrude(length = 34)
|> translate(z = 3.14)
fullPart = subtract([part001], tools = [part002])

View File

@ -42,7 +42,6 @@ import {
ONBOARDING_TOAST_ID,
TutorialRequestToast,
} from '@src/routes/Onboarding/utils'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
// CYCLIC REF
sceneInfra.camControls.engineStreamActor = engineStreamActor
@ -91,10 +90,6 @@ export function App() {
const settings = useSettings()
const authToken = useToken()
const {
app: { onboardingStatus },
} = settings
useHotkeys('backspace', (e) => {
e.preventDefault()
})
@ -110,13 +105,6 @@ export function App() {
toast.success('Your work is auto-saved in real-time')
})
const paneOpacity = [
ONBOARDING_SUBPATHS.CAMERA,
ONBOARDING_SUBPATHS.STREAMING,
].some((p) => p === onboardingStatus.current)
? 'opacity-20'
: ''
useEngineConnectionSubscriptions()
useEffect(() => {
@ -160,7 +148,7 @@ export function App() {
return (
<div className="relative h-full flex flex-col" ref={ref}>
<AppHeader
className={`transition-opacity transition-duration-75 ${paneOpacity}`}
className="transition-opacity transition-duration-75"
project={{ project, file }}
enableMenu={true}
>
@ -168,7 +156,7 @@ export function App() {
<ShareButton />
</AppHeader>
<ModalContainer />
<ModelingSidebar paneOpacity={paneOpacity} />
<ModelingSidebar />
<EngineStream pool={pool} authToken={authToken} />
{/* <CamToggle /> */}
<LowerRightControls navigate={navigate}>

View File

@ -38,7 +38,7 @@ import { reportRejection } from '@src/lib/trap'
import { useToken } from '@src/lib/singletons'
import RootLayout from '@src/Root'
import Home from '@src/routes/Home'
import Onboarding, { onboardingRoutes } from '@src/routes/Onboarding'
import { OnboardingRootRoute, onboardingRoutes } from '@src/routes/Onboarding'
import { Settings } from '@src/routes/Settings'
import SignIn from '@src/routes/SignIn'
import { Telemetry } from '@src/routes/Telemetry'
@ -102,8 +102,8 @@ const router = createRouter([
element: <Settings />,
},
{
path: makeUrlPathRelative(PATHS.ONBOARDING.INDEX),
element: <Onboarding />,
path: makeUrlPathRelative(PATHS.ONBOARDING),
element: <OnboardingRootRoute />,
children: onboardingRoutes,
},
],

View File

@ -195,6 +195,7 @@ export function Toolbar({
return (
<menu
data-current-mode={currentMode}
data-onboarding-id="toolbar"
className="max-w-full whitespace-nowrap rounded-b px-2 py-1 bg-chalkboard-10 dark:bg-chalkboard-90 relative border border-chalkboard-30 dark:border-chalkboard-80 border-t-0 shadow-sm"
>
<ul
@ -231,6 +232,7 @@ export function Toolbar({
Element="button"
key={selectedIcon.id}
data-testid={selectedIcon.id + '-dropdown'}
data-onboarding-id={selectedIcon.id + '-dropdown'}
id={selectedIcon.id + '-dropdown'}
name={maybeIconConfig.id}
className={
@ -265,6 +267,7 @@ export function Toolbar({
Element="button"
id={selectedIcon.id}
data-testid={selectedIcon.id}
data-onboarding-id={selectedIcon.id}
iconStart={{
icon: selectedIcon.icon,
iconColor: selectedIcon.iconColor,
@ -331,6 +334,7 @@ export function Toolbar({
key={itemConfig.id}
id={itemConfig.id}
data-testid={itemConfig.id}
data-onboarding-id={itemConfig.id}
iconStart={{
icon: itemConfig.icon,
iconColor: itemConfig.iconColor,

View File

@ -26,7 +26,10 @@ export function ActionButtonDropdown({
}: ActionButtonSplitProps) {
const baseClassNames = `action-button p-0 m-0 group mono text-xs leading-none flex items-center gap-2 rounded-sm border-solid border border-chalkboard-30 hover:border-chalkboard-40 enabled:dark:border-chalkboard-70 dark:hover:border-chalkboard-60 dark:bg-chalkboard-90/50 text-chalkboard-100 dark:text-chalkboard-10`
return (
<Popover className={`${baseClassNames} ${className}`}>
<Popover
className={`${baseClassNames} ${className}`}
data-onboarding-id={`${props.name}-group`}
>
{({ close }) => (
<>
{children}
@ -37,6 +40,7 @@ export function ActionButtonDropdown({
'enabled:hover:bg-chalkboard-10 dark:enabled:hover:bg-chalkboard-100 ' +
'pressed:!bg-primary pressed:enabled:hover:!text-chalkboard-10 p-0 m-0 rounded-none !outline-none ui-open:border-primary ui-open:bg-primary'
}
data-onboarding-id={`${props.name}-dropdown-button`}
>
<CustomIcon
name="caretDown"
@ -72,6 +76,7 @@ export function ActionButtonDropdown({
tabIndex={-1}
disabled={item.disabled}
data-testid={'dropdown-' + item.id}
data-onboarding-id={`${props.name}-dropdown-item`}
>
<span className="capitalize flex-grow text-left">
{item.label}

View File

@ -23,10 +23,13 @@ export const CommandBar = () => {
const {
context: { selectedCommand, currentArgument, commands },
} = commandBarState
const isSelectionArgument =
const isArgumentThatShouldBeHardToDismiss =
currentArgument?.inputType === 'selection' ||
currentArgument?.inputType === 'selectionMixed'
const WrapperComponent = isSelectionArgument ? Popover : Dialog
currentArgument?.inputType === 'selectionMixed' ||
currentArgument?.inputType === 'text'
const WrapperComponent = isArgumentThatShouldBeHardToDismiss
? Popover
: Dialog
// Close the command bar when navigating
useEffect(() => {
@ -120,13 +123,16 @@ export const CommandBar = () => {
as={Fragment}
>
<WrapperComponent
open={!commandBarState.matches('Closed') || isSelectionArgument}
open={
!commandBarState.matches('Closed') ||
isArgumentThatShouldBeHardToDismiss
}
onClose={() => {
commandBarActor.send({ type: 'Close' })
}}
className={
'fixed inset-0 z-50 overflow-y-auto pb-4 pt-1 ' +
(isSelectionArgument ? 'pointer-events-none' : '')
(isArgumentThatShouldBeHardToDismiss ? 'pointer-events-none' : '')
}
data-testid="command-bar-wrapper"
>

View File

@ -24,10 +24,18 @@ function errorMessage(error: unknown): string {
}
}
function stackTraceMessage(error: unknown): string {
if (error !== undefined && error instanceof Error) {
return error.stack || ''
}
return ''
}
/** Generate a GitHub issue URL from the error */
function generateToUrl(error: unknown) {
const title: string = 'An unexpected error occurred'
const body = errorMessage(error)
const newLine = '%0A'
const body = `${errorMessage(error)} ${newLine} >${stackTraceMessage(error)} ${newLine}`
const result = `https://github.com/KittyCAD/modeling-app/issues/new?title=${title}&body=${body}`
return result
}
@ -43,9 +51,12 @@ export const ErrorPage = () => {
<h1 className="text-4xl mb-8 font-bold" data-testid="unexpected-error">
An unexpected error occurred
</h1>
<p className="mb-8 w-full overflow-aut">
<p className="mb-8 w-full overflow-auto">
<>{errorMessage(error)}</>
</p>
<p className="mb-8 w-full overflow-auto">
<>{stackTraceMessage(error)}</>
</p>
<div className="flex justify-between gap-2 mt-6">
{isDesktop() && (
<ActionButton

View File

@ -13,7 +13,7 @@ import {
acceptOnboarding,
catchOnboardingWarnError,
} from '@src/routes/Onboarding/utils'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
import { onboardingStartPath } from '@src/lib/onboardingPaths'
const HelpMenuDivider = () => (
<div className="h-[1px] bg-chalkboard-110 dark:bg-chalkboard-80" />
@ -29,7 +29,7 @@ export function HelpMenu({
const resetOnboardingWorkflow = () => {
const props = {
onboardingStatus: ONBOARDING_SUBPATHS.INDEX,
onboardingStatus: onboardingStartPath,
navigate,
codeManager,
kclManager,

View File

@ -14,11 +14,7 @@ import Tooltip from '@src/components/Tooltip'
import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import { PATHS } from '@src/lib/paths'
import {
APP_VERSION,
IS_NIGHTLY_OR_DEBUG,
getReleaseUrl,
} from '@src/routes/utils'
import { APP_VERSION, getReleaseUrl } from '@src/routes/utils'
import { billingActor } from '@src/lib/singletons'
@ -39,22 +35,23 @@ export function LowerRightControls({
<section className="fixed bottom-2 right-2 flex flex-col items-end gap-3 pointer-events-none">
{children}
<menu className="flex items-center justify-end gap-3 pointer-events-auto">
{IS_NIGHTLY_OR_DEBUG && (
<Popover className="relative">
<Popover.Button className="p-0 !border-transparent">
<BillingRemaining
mode={BillingRemainingMode.ProgressBarFixed}
billingActor={billingActor}
/>
<Tooltip position="top" contentClassName="text-xs">
Text-to-CAD credits
</Tooltip>
</Popover.Button>
<Popover.Panel className="absolute right-0 left-auto bottom-full mb-1 w-64 flex flex-col gap-1 align-stretch rounded-lg shadow-lg text-sm">
<BillingDialog billingActor={billingActor} />
</Popover.Panel>
</Popover>
)}
<Popover className="relative">
<Popover.Button
className="p-0 !border-transparent"
data-testid="billing-remaining-bar"
>
<BillingRemaining
mode={BillingRemainingMode.ProgressBarFixed}
billingActor={billingActor}
/>
<Tooltip position="top" contentClassName="text-xs">
Text-to-CAD credits
</Tooltip>
</Popover.Button>
<Popover.Panel className="absolute right-0 left-auto bottom-full mb-1 w-64 flex flex-col gap-1 align-stretch rounded-lg shadow-lg text-sm">
<BillingDialog billingActor={billingActor} />
</Popover.Panel>
</Popover>
<a
onClick={openExternalBrowserIfDesktop(getReleaseUrl())}
href={getReleaseUrl()}

View File

@ -5,8 +5,6 @@ import { ActionButton } from '@src/components/ActionButton'
import { ActionIcon } from '@src/components/ActionIcon'
import type { CustomIconName } from '@src/components/CustomIcon'
import Tooltip from '@src/components/Tooltip'
import { useSettings } from '@src/lib/singletons'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
import styles from './ModelingPane.module.css'
@ -68,12 +66,6 @@ export const ModelingPane = ({
title,
...props
}: ModelingPaneProps) => {
const settings = useSettings()
const onboardingStatus = settings.app.onboardingStatus
const pointerEventsCssClass =
onboardingStatus.current === ONBOARDING_SUBPATHS.CAMERA
? 'pointer-events-none '
: 'pointer-events-auto '
return (
<section
{...props}
@ -82,7 +74,6 @@ export const ModelingPane = ({
id={id}
className={
'focus-within:border-primary dark:focus-within:border-chalkboard-50 ' +
pointerEventsCssClass +
styles.panel +
' group ' +
(className || '')

View File

@ -24,17 +24,12 @@ import { SIDEBAR_BUTTON_SUFFIX } from '@src/lib/constants'
import { isDesktop } from '@src/lib/isDesktop'
import { useSettings } from '@src/lib/singletons'
import { commandBarActor } from '@src/lib/singletons'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
import { reportRejection } from '@src/lib/trap'
import { refreshPage } from '@src/lib/utils'
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
import usePlatform from '@src/hooks/usePlatform'
import { settingsActor } from '@src/lib/singletons'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
}
interface BadgeInfoComputed {
value: number | boolean | string
onClick?: MouseEventHandler<any>
@ -46,14 +41,12 @@ function getPlatformString(): 'web' | 'desktop' {
return isDesktop() ? 'desktop' : 'web'
}
export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
export function ModelingSidebar() {
const machineManager = useContext(MachineManagerContext)
const kclContext = useKclContext()
const settings = useSettings()
const onboardingStatus = settings.app.onboardingStatus
const { send, context } = useModelingContext()
const pointerEventsCssClass =
onboardingStatus.current === ONBOARDING_SUBPATHS.CAMERA ||
context.store?.openPanes.length === 0
? 'pointer-events-none '
: 'pointer-events-auto '
@ -225,7 +218,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
return (
<Resizable
className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${pointerEventsCssClass}`}
defaultSize={{
width: '550px',
height: 'auto',
@ -361,7 +354,11 @@ function ModelingPaneButton({
})
return (
<div id={paneConfig.id + '-button-holder'} className="relative">
<div
id={paneConfig.id + '-button-holder'}
className="relative"
data-onboarding-id={`${paneConfig.id}-pane-button`}
>
<button
className="group pointer-events-auto flex items-center justify-center border-transparent dark:border-transparent disabled:!border-transparent p-0 m-0 rounded-sm !outline-0 focus-visible:border-primary"
onClick={onClick}

View File

@ -83,12 +83,12 @@ function AppLogoLink({
to={PATHS.HOME}
className={wrapperClassName + ' hover:before:brightness-110'}
>
<Logo className={logoClassName} />
<Logo data-onboarding-id="app-logo" className={logoClassName} />
<span className="sr-only">{APP_NAME}</span>
</Link>
) : (
<div className={wrapperClassName} data-testid="app-logo">
<Logo className={logoClassName} />
<Logo data-onboarding-id="app-logo" className={logoClassName} />
<span className="sr-only">{APP_NAME}</span>
</div>
)

View File

@ -52,7 +52,8 @@ export function SystemIOMachineLogicListenerDesktop() {
)
const requestedPath = joinRouterPaths(
PATHS.FILE,
safeEncodeForRouterPaths(projectPathWithoutSpecificKCLFile)
safeEncodeForRouterPaths(projectPathWithoutSpecificKCLFile),
requestedProjectName.subRoute || ''
)
navigate(requestedPath)
}, [requestedProjectName])
@ -156,7 +157,10 @@ export function SystemIOMachineLogicListenerDesktop() {
settings: { highlightEdges: settings.modeling.highlightEdges.current },
})
.then(() => {
billingActor.send({ type: BillingTransition.Update, apiToken: token })
billingActor.send({
type: BillingTransition.Update,
apiToken: token,
})
})
.catch(reportRejection)
}, [requestedTextToCadGeneration])

View File

@ -11,7 +11,7 @@ import { SettingsSection } from '@src/components/Settings/SettingsSection'
import { getSettingsFolderPaths } from '@src/lib/desktopFS'
import { isDesktop } from '@src/lib/isDesktop'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
import { onboardingStartPath } from '@src/lib/onboardingPaths'
import { PATHS } from '@src/lib/paths'
import type { Setting } from '@src/lib/settings/initialSettings'
import type {
@ -69,7 +69,7 @@ export const AllSettingsFields = forwardRef(
async function restartOnboarding() {
const props = {
onboardingStatus: ONBOARDING_SUBPATHS.INDEX,
onboardingStatus: onboardingStartPath,
navigate,
codeManager,
kclManager,

View File

@ -198,6 +198,7 @@ code {
#code-mirror-override .cm-content {
@apply caret-primary;
}
.dark #code-mirror-override .cm-content {
@apply caret-chalkboard-10;
}
@ -216,6 +217,7 @@ code {
100% {
opacity: 0;
}
10% {
opacity: 1;
}
@ -258,9 +260,11 @@ code {
#code-mirror-override .cm-tooltip-autocomplete li {
@apply px-2 py-1;
}
#code-mirror-override .cm-tooltip-autocomplete li[aria-selected="true"] {
@apply bg-liquid-10 text-liquid-110;
}
.dark #code-mirror-override .cm-tooltip-autocomplete li[aria-selected="true"] {
@apply bg-liquid-100 text-liquid-20;
}
@ -339,6 +343,22 @@ code {
.outline-appForeground {
@apply outline-chalkboard-100 dark:outline-chalkboard-10;
}
/* highlight an object with a moving dashed outline */
.onboarding-highlight {
@apply outline outline-2;
animation: onboarding-highlight 0.7s ease-in-out infinite alternate-reverse;
}
@keyframes onboarding-highlight {
0% {
outline-offset: 0px;
}
100% {
outline-offset: 4px;
}
}
}
#code-mirror-override .cm-scroller,

View File

@ -223,7 +223,7 @@ export function mutateKwArg(
): boolean | 'no-mutate' {
for (let i = 0; i < node.arguments.length; i++) {
const arg = node.arguments[i]
if (arg.label.name === label) {
if (arg.label?.name === label) {
if (isLiteralArrayOrStatic(val) && isLiteralArrayOrStatic(arg.arg)) {
node.arguments[i].arg = val
return true
@ -259,7 +259,7 @@ export function mutateKwArgOnly(
): boolean {
for (let i = 0; i < node.arguments.length; i++) {
const arg = node.arguments[i]
if (arg.label.name === label) {
if (arg.label?.name === label) {
node.arguments[i].arg = val
return true
}
@ -273,7 +273,7 @@ Mutates the given node by removing the labeled arguments.
*/
export function removeKwArgs(labels: string[], node: CallExpressionKw) {
for (const label of labels) {
const i = node.arguments.findIndex((la) => la.label.name === label)
const i = node.arguments.findIndex((la) => la.label?.name === label)
if (i == -1) {
continue
}

View File

@ -773,7 +773,7 @@ export async function editEdgeTreatment(
// find the index of an argument to update
const index = edgeTreatmentCall.node.arguments.findIndex(
(arg) => arg.label.name === parameterName
(arg) => arg.label?.name === parameterName
)
// create a new argument with the updated value

View File

@ -500,7 +500,7 @@ function modifyAstWithTagForCapFace(
// Check for existing tag with this parameter name
const existingTag = callExp.node.arguments.find(
(arg) => arg.label.name === tagParamName
(arg) => arg.label?.name === tagParamName
)
if (existingTag && existingTag.arg.type === 'TagDeclarator') {

View File

@ -190,7 +190,10 @@ const commonConstraintInfoHelper = (
if (lengthishIndex === undefined) {
return [undefined, undefined]
}
const lengthKey = callExp.arguments[lengthishIndex].label.name
const lengthKey = callExp.arguments[lengthishIndex].label?.name
if (lengthKey === undefined) {
return [undefined, undefined]
}
const lengthVal = callExp.arguments[lengthishIndex].arg
// Note: The order of keys here matters.
// Always assumes the angle was the first param, and then the length followed.
@ -1057,7 +1060,7 @@ export const tangentialArc: SketchLineHelperKw = {
}
for (const arg of callExpression.arguments) {
if (arg.label.name !== ARG_END_ABSOLUTE && arg.label.name !== ARG_TAG) {
if (arg.label?.name !== ARG_END_ABSOLUTE && arg.label?.name !== ARG_TAG) {
console.debug(
'Trying to edit unsupported tangentialArc keyword arguments; skipping'
)

View File

@ -1375,12 +1375,12 @@ export function removeSingleConstraint({
// 1. Filter out any existing tag argument since it will be handled separately
const filteredArgs = existingArgs.filter(
(arg) => arg.label.name !== ARG_TAG
(arg) => arg.label?.name !== ARG_TAG
)
// 2. Map through the args, replacing only the one we want to change
const labeledArgs = filteredArgs.map((arg) => {
if (arg.label.name === toReplace) {
if (arg.label?.name === toReplace) {
// Find the raw value to use for the argument being replaced
const rawArgVersion = rawArgs.find(
(a) => a.type === 'labeledArg' && a.key === toReplace
@ -1430,7 +1430,7 @@ export function removeSingleConstraint({
const labeledArgs = existingArgs.map((arg) => {
// Only modify the specific argument that matches the targeted key
if (
arg.label.name === targetKey &&
arg.label?.name === targetKey &&
arg.arg.type === 'ArrayExpression'
) {
// We're dealing with an array expression within a labeled argument
@ -1560,7 +1560,7 @@ export function removeSingleConstraint({
arrayInput[inputToReplace.key][inputToReplace.index] =
rawLiteralArrayInObjectExpr
let existingKwgForKey = kwArgInput.find(
(kwArg) => kwArg.label.name === currentArg.key
(kwArg) => kwArg.label?.name === currentArg.key
)
if (!existingKwgForKey) {
existingKwgForKey = createLabeledArg(
@ -1586,7 +1586,7 @@ export function removeSingleConstraint({
if (!arrayInput[currentArg.key]) arrayInput[currentArg.key] = []
arrayInput[currentArg.key][currentArg.index] = currentArgExpr
let existingKwgForKey = kwArgInput.find(
(kwArg) => kwArg.label.name === currentArg.key
(kwArg) => kwArg.label?.name === currentArg.key
)
if (!existingKwgForKey) {
existingKwgForKey = createLabeledArg(
@ -2190,7 +2190,7 @@ export function getConstraintLevelFromSourceRange(
}
const arg = findKwArgAny(DETERMINING_ARGS, nodeMeta.node)
if (arg === undefined) {
const argStr = nodeMeta.node.arguments.map((a) => a.label.name)
const argStr = nodeMeta.node.arguments.map((a) => a.label?.name)
return new Error(
`call to expression ${name} has unexpected args: ${argStr} `
)

View File

@ -136,7 +136,7 @@ export function findKwArg(
call: CallExpressionKw
): Expr | undefined {
return call?.arguments?.find((arg) => {
return arg.label.name === label
return arg.label?.name === label
})?.arg
}
@ -149,7 +149,7 @@ export function findKwArgWithIndex(
call: CallExpressionKw
): { expr: Expr; argIndex: number } | undefined {
const index = call.arguments.findIndex((arg) => {
return arg.label.name === label
return arg.label?.name === label
})
return index >= 0
? { expr: call.arguments[index].arg, argIndex: index }
@ -164,7 +164,7 @@ export function findKwArgAny(
call: CallExpressionKw
): Expr | undefined {
return call.arguments.find((arg) => {
return labels.includes(arg.label.name)
return labels.includes(arg.label?.name || '')
})?.arg
}
@ -176,7 +176,7 @@ export function findKwArgAnyIndex(
call: CallExpressionKw
): number | undefined {
const index = call.arguments.findIndex((arg) => {
return labels.includes(arg.label.name)
return labels.includes(arg.label?.name || '')
})
if (index == -1) {
return undefined

View File

@ -36,7 +36,7 @@ export const FILE_PERSIST_KEY = `${PROJECT_FOLDER}-last-opened` as const
/** The default name given to new kcl files in a project */
export const DEFAULT_FILE_NAME = 'Untitled'
/** The default name for a tutorial project */
export const ONBOARDING_PROJECT_NAME = 'Tutorial Project $nn'
export const ONBOARDING_PROJECT_NAME = 'tutorial-project'
/**
* The default starting constant name for various modeling operations.
* These are used to generate unique names for new objects.
@ -193,5 +193,9 @@ export const IS_PLAYWRIGHT_KEY = 'playwright'
/** Should we mark all the ML features as "beta"? */
export const IS_ML_EXPERIMENTAL = true
export const ML_EXPERIMENTAL_MESSAGE =
'This feature is experimental and undergoing constant improvement, stay tuned for updates.'
export const ML_EXPERIMENTAL_MESSAGE = 'This feature is experimental.'
/**
* HTML data-* attribute for tagging elements for highlighting
* while in the onboarding flow.
*/
export const ONBOARDING_DATA_ATTRIBUTE = 'onboarding-id'

View File

@ -1,6 +1,18 @@
import bracket from '@public/kcl-samples/bracket/main.kcl?raw'
import fanAssembly from '@public/kcl-samples/axial-fan/main.kcl?raw'
import fanHousingOriginal from '@public/kcl-samples/axial-fan/fan-housing.kcl?raw'
import fanFan from '@public/kcl-samples/axial-fan/fan.kcl?raw'
import fanMotor from '@public/kcl-samples/axial-fan/motor.kcl?raw'
import fanParameters from '@public/kcl-samples/axial-fan/parameters.kcl?raw'
export { bracket }
export const fanParts = [
{ requestedFileName: 'main.kcl', requestedCode: fanAssembly },
{ requestedFileName: 'fan.kcl', requestedCode: fanFan },
{ requestedFileName: 'motor.kcl', requestedCode: fanMotor },
{ requestedFileName: 'parameters.kcl', requestedCode: fanParameters },
{ requestedFileName: 'fan-housing.kcl', requestedCode: fanHousingOriginal },
] as const
/**
* @throws Error if the search text is not found in the example code.
@ -29,3 +41,533 @@ export const bracketWidthConstantLine = findLineInExampleCode({
export const bracketThicknessCalculationLine = findLineInExampleCode({
searchText: 'thickness =',
})
const fanHousing = `
// Fan Housing
// The plastic housing that contains the fan and the motor
// Set units
@settings(defaultLengthUnit = mm)
// Define Parameters
export fanSize = 120
export fanHeight = 25
export mountingHoleSpacing = 105
export mountingHoleSize = 4.5
// Model the housing which holds the motor, the fan, and the mounting provisions
// Bottom mounting face
bottomFaceSketch = startSketchOn(XY)
|> startProfile(at = [-fanSize / 2, -fanSize / 2])
|> angledLine(angle = 0, length = fanSize, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) + 90, length = fanSize, tag = $rectangleSegmentB001)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $rectangleSegmentD001)
|> close()
|> subtract2d(tool = circle(center = [0, 0], radius = 4))
|> subtract2d(tool = circle(
center = [
mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> subtract2d(tool = circle(
center = [
-mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> subtract2d(tool = circle(
center = [
mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> subtract2d(tool = circle(
center = [
-mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> extrude(length = 4)
// Add large openings to the bottom face to allow airflow through the fan
airflowPattern = startSketchOn(bottomFaceSketch, face = END)
|> startProfile(at = [fanSize * 7 / 25, -fanSize * 9 / 25])
|> angledLine(angle = 140, length = fanSize * 12 / 25, tag = $seg01)
|> tangentialArc(radius = fanSize * 1 / 50, angle = 90)
|> angledLine(angle = -130, length = fanSize * 8 / 25)
|> tangentialArc(radius = fanSize * 1 / 50, angle = 90)
|> angledLine(angle = segAng(seg01) + 180, length = fanSize * 2 / 25)
|> tangentialArc(radius = fanSize * 8 / 25, angle = 40)
|> xLine(length = fanSize * 3 / 25)
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> patternCircular2d(
instances = 4,
center = [0, 0],
arcDegrees = 360,
rotateDuplicates = true,
)
|> extrude(length = -4)
// Create the middle segment of the fan housing body
housingMiddleLength = fanSize / 3
housingMiddleRadius = fanSize / 3 - 1
bodyMiddle = startSketchOn(bottomFaceSketch, face = END)
|> startProfile(at = [
housingMiddleLength / 2,
-housingMiddleLength / 2 - housingMiddleRadius
])
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> yLine(length = housingMiddleLength)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> xLine(length = -housingMiddleLength)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> yLine(length = -housingMiddleLength)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> extrude(length = fanHeight - 4 - 4)
// Cut a hole in the body to accommodate the fan
bodyFanHole = startSketchOn(bodyMiddle, face = END)
|> circle(center = [0, 0], radius = fanSize * 23 / 50)
|> extrude(length = -(fanHeight - 4 - 4))
// Top mounting face. Cut a hole in the face to accommodate the fan
topFaceSketch = startSketchOn(bodyMiddle, face = END)
topHoles = startProfile(topFaceSketch, at = [-fanSize / 2, -fanSize / 2])
|> angledLine(angle = 0, length = fanSize, tag = $rectangleSegmentA002)
|> angledLine(angle = segAng(rectangleSegmentA002) + 90, length = fanSize, tag = $rectangleSegmentB002)
|> angledLine(angle = segAng(rectangleSegmentA002), length = -segLen(rectangleSegmentA002), tag = $rectangleSegmentC002)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $rectangleSegmentD002)
|> close()
|> subtract2d(tool = circle(center = [0, 0], radius = fanSize * 23 / 50))
|> subtract2d(tool = circle(
center = [
mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> subtract2d(tool = circle(
center = [
-mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> subtract2d(tool = circle(
center = [
mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> subtract2d(tool = circle(
center = [
-mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> extrude(length = 4)
// Create a housing for the electric motor to sit
motorHousing = startSketchOn(bottomFaceSketch, face = END)
|> circle(center = [0, 0], radius = 11.2)
|> extrude(length = 16)
startSketchOn(motorHousing, face = END)
|> circle(center = [0, 0], radius = 10)
|> extrude(length = -16)
|> appearance(color = "#a55e2c")
|> fillet(
radius = abs(fanSize - mountingHoleSpacing) / 2,
tags = [
getNextAdjacentEdge(rectangleSegmentA001),
getNextAdjacentEdge(rectangleSegmentB001),
getNextAdjacentEdge(rectangleSegmentC001),
getNextAdjacentEdge(rectangleSegmentD001),
getNextAdjacentEdge(rectangleSegmentA002),
getNextAdjacentEdge(rectangleSegmentB002),
getNextAdjacentEdge(rectangleSegmentC002),
getNextAdjacentEdge(rectangleSegmentD002)
],
)
`
export const modifiedFanHousing = `// Fan Housing
// The plastic housing that contains the fan and the motor
// Set units
@settings(defaultLengthUnit = mm)
export fanSize = 150
export fanHeight = 30
export mountingHoleSpacing = 105
export mountingHoleSize = 4.5
// Model the housing which holds the motor, the fan, and the mounting provisions
// Bottom mounting face
bottomFaceSketch = startSketchOn(XY)
|> startProfile(at = [-fanSize / 2, -fanSize / 2])
|> angledLine(angle = 0, length = fanSize, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) + 90, length = fanSize, tag = $rectangleSegmentB001)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $rectangleSegmentC001)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $rectangleSegmentD001)
|> close()
|> subtract2d(tool = circle(center = [0, 0], radius = 4))
|> subtract2d(tool = circle(
center = [
mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> subtract2d(tool = circle(
center = [
-mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> subtract2d(tool = circle(
center = [
mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> subtract2d(tool = circle(
center = [
-mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> extrude(length = 4)
// Add large openings to the bottom face to allow airflow through the fan
airflowPattern = startSketchOn(bottomFaceSketch, face = END)
|> startProfile(at = [fanSize * 7 / 25, -fanSize * 9 / 25])
|> angledLine(angle = 140, length = fanSize * 12 / 25, tag = $seg01)
|> tangentialArc(radius = fanSize * 1 / 50, angle = 90)
|> angledLine(angle = -130, length = fanSize * 8 / 25)
|> tangentialArc(radius = fanSize * 1 / 50, angle = 90)
|> angledLine(angle = segAng(seg01) + 180, length = fanSize * 2 / 25)
|> tangentialArc(radius = fanSize * 8 / 25, angle = 40)
|> xLine(length = fanSize * 3 / 25)
|> tangentialArc(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> patternCircular2d(
instances = 4,
center = [0, 0],
arcDegrees = 360,
rotateDuplicates = true,
)
|> extrude(length = -4)
// Create the middle segment of the fan housing body
housingMiddleLength = fanSize / 3
housingMiddleRadius = fanSize / 3 - 1
bodyMiddle = startSketchOn(bottomFaceSketch, face = END)
|> startProfile(at = [
housingMiddleLength / 2,
-housingMiddleLength / 2 - housingMiddleRadius
])
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> yLine(length = housingMiddleLength)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> xLine(length = -housingMiddleLength)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> yLine(length = -housingMiddleLength)
|> tangentialArc(radius = housingMiddleRadius, angle = 90)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> extrude(length = fanHeight - 4 - 4)
// Cut a hole in the body to accommodate the fan
bodyFanHole = startSketchOn(bodyMiddle, face = END)
|> circle(center = [0, 0], radius = fanSize * 23 / 50)
|> extrude(length = -(fanHeight - 4 - 4))
// Top mounting face. Cut a hole in the face to accommodate the fan
topFaceSketch = startSketchOn(bodyMiddle, face = END)
topHoles = startProfile(topFaceSketch, at = [-fanSize / 2, -fanSize / 2])
|> angledLine(angle = 0, length = fanSize, tag = $rectangleSegmentA002)
|> angledLine(angle = segAng(rectangleSegmentA002) + 90, length = fanSize, tag = $rectangleSegmentB002)
|> angledLine(angle = segAng(rectangleSegmentA002), length = -segLen(rectangleSegmentA002), tag = $rectangleSegmentC002)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $rectangleSegmentD002)
|> close()
|> subtract2d(tool = circle(center = [0, 0], radius = fanSize * 23 / 50))
|> subtract2d(tool = circle(
center = [
mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> subtract2d(tool = circle(
center = [
-mountingHoleSpacing / 2,
mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> subtract2d(tool = circle(
center = [
mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> subtract2d(tool = circle(
center = [
-mountingHoleSpacing / 2,
-mountingHoleSpacing / 2
],
radius = mountingHoleSize / 2,
))
|> extrude(length = 4)
// Create a housing for the electric motor to sit
motorHousing = startSketchOn(bottomFaceSketch, face = END)
|> circle(center = [0, 0], radius = 11.2)
|> extrude(length = 16)
startSketchOn(motorHousing, face = END)
|> circle(center = [0, 0], radius = 10)
|> extrude(length = -16)
|> appearance(color = "#800080") // Changed color to purple
|> fillet(
radius = abs(fanSize - mountingHoleSpacing) / 2,
tags = [
getNextAdjacentEdge(rectangleSegmentA001),
getNextAdjacentEdge(rectangleSegmentB001),
getNextAdjacentEdge(rectangleSegmentC001),
getNextAdjacentEdge(rectangleSegmentD001),
getNextAdjacentEdge(rectangleSegmentA002),
getNextAdjacentEdge(rectangleSegmentB002),
getNextAdjacentEdge(rectangleSegmentC002),
getNextAdjacentEdge(rectangleSegmentD002)
],
)
`
/**
* GOTCHA: this browser sample is a reconstructed assembly, made by
* concatenating the individual parts together. If the original axial-fan
* KCL sample is updated, it can lead to breaking this export.
*/
export const browserAxialFan = `
${fanHousing}
// Fan
// Spinning axial fan that moves airflow
// Model the center of the fan
fanCenter = startSketchOn(XZ)
|> startProfile(at = [-0.0001, fanHeight])
|> xLine(endAbsolute = -15 + 1.5)
|> tangentialArc(radius = 1.5, angle = 90)
|> yLine(endAbsolute = 4.5)
|> xLine(endAbsolute = -13)
|> yLine(endAbsolute = profileStartY(%) - 5)
|> tangentialArc(radius = 1, angle = -90)
|> xLine(endAbsolute = -1)
|> yLine(length = 2)
|> xLine(length = -0.15)
|> line(endAbsolute = [
profileStartX(%) - 1,
profileStartY(%) - 1.4
])
|> xLine(endAbsolute = profileStartX(%))
|> yLine(endAbsolute = profileStartY(%))
|> close()
|> revolve(axis = {
direction = [0.0, 1.0],
origin = [0.0, 0.0]
})
|> appearance(color = "#f3e2d8")
// Create a function for a lofted fan blade cross section that rotates about the center hub of the fan
fn fanBlade(offsetHeight, startAngle: number(deg)) {
fanBlade = startSketchOn(offsetPlane(XY, offset = offsetHeight))
|> startProfile(at = [
15 * cos(startAngle),
15 * sin(startAngle)
])
|> arc(angleStart = startAngle, angleEnd = startAngle + 14, radius = 15)
|> arc(
endAbsolute = [
fanSize * 22 / 50 * cos(startAngle - 20),
fanSize * 22 / 50 * sin(startAngle - 20)
],
interiorAbsolute = [
fanSize * 11 / 50 * cos(startAngle + 3),
fanSize * 11 / 50 * sin(startAngle + 3)
],
)
|> arc(
endAbsolute = [
fanSize * 22 / 50 * cos(startAngle - 24),
fanSize * 22 / 50 * sin(startAngle - 24)
],
interiorAbsolute = [
fanSize * 22 / 50 * cos(startAngle - 22),
fanSize * 22 / 50 * sin(startAngle - 22)
],
)
|> arc(
endAbsolute = [profileStartX(%), profileStartY(%)],
interiorAbsolute = [
fanSize * 11 / 50 * cos(startAngle - 5),
fanSize * 11 / 50 * sin(startAngle - 5)
],
)
|> close()
return fanBlade
}
// Loft the fan blade cross sections into a single blade, then pattern them about the fan center
crossSections = [
fanBlade(offsetHeight = 4.5, startAngle = 50),
fanBlade(offsetHeight = (fanHeight - 2 - 4) / 2, startAngle = 30),
fanBlade(offsetHeight = fanHeight - 2, startAngle = 0)
]
loft(crossSections)
|> appearance(color = "#f3e2d8")
|> patternCircular3d(
instances = 9,
axis = [0, 0, 1],
center = [0, 0, 0],
arcDegrees = 360,
rotateDuplicates = true,
)
// Motor
// A small electric motor to power the fan
// Model the motor body and stem
topFacePlane = offsetPlane(XY, offset = 4)
motorBody = startSketchOn(topFacePlane)
|> circle(center = [0, 0], radius = 10, tag = $seg04)
|> extrude(length = 17)
|> appearance(color = "#021b55")
|> fillet(radius = 2, tags = [getOppositeEdge(seg04), seg04])
startSketchOn(offsetPlane(XY, offset = 21))
|> circle(center = [0, 0], radius = 1)
|> extrude(length = 3.8)
|> appearance(color = "#dbc89e")
`
/**
* GOTCHA: this browser sample is a reconstructed assembly, made by
* concatenating the individual parts together. If the original axial-fan
* KCL sample is updated, it can lead to breaking this export.
*/
export const browserAxialFanAfterTextToCad = `
${modifiedFanHousing}
// Fan
// Spinning axial fan that moves airflow
// Model the center of the fan
fanCenter = startSketchOn(XZ)
|> startProfile(at = [-0.0001, fanHeight])
|> xLine(endAbsolute = -15 + 1.5)
|> tangentialArc(radius = 1.5, angle = 90)
|> yLine(endAbsolute = 4.5)
|> xLine(endAbsolute = -13)
|> yLine(endAbsolute = profileStartY(%) - 5)
|> tangentialArc(radius = 1, angle = -90)
|> xLine(endAbsolute = -1)
|> yLine(length = 2)
|> xLine(length = -0.15)
|> line(endAbsolute = [
profileStartX(%) - 1,
profileStartY(%) - 1.4
])
|> xLine(endAbsolute = profileStartX(%))
|> yLine(endAbsolute = profileStartY(%))
|> close()
|> revolve(axis = {
direction = [0.0, 1.0],
origin = [0.0, 0.0]
})
|> appearance(color = "#f3e2d8")
// Create a function for a lofted fan blade cross section that rotates about the center hub of the fan
fn fanBlade(offsetHeight, startAngle: number(deg)) {
fanBlade = startSketchOn(offsetPlane(XY, offset = offsetHeight))
|> startProfile(at = [
15 * cos(startAngle),
15 * sin(startAngle)
])
|> arc(angleStart = startAngle, angleEnd = startAngle + 14, radius = 15)
|> arc(
endAbsolute = [
fanSize * 22 / 50 * cos(startAngle - 20),
fanSize * 22 / 50 * sin(startAngle - 20)
],
interiorAbsolute = [
fanSize * 11 / 50 * cos(startAngle + 3),
fanSize * 11 / 50 * sin(startAngle + 3)
],
)
|> arc(
endAbsolute = [
fanSize * 22 / 50 * cos(startAngle - 24),
fanSize * 22 / 50 * sin(startAngle - 24)
],
interiorAbsolute = [
fanSize * 22 / 50 * cos(startAngle - 22),
fanSize * 22 / 50 * sin(startAngle - 22)
],
)
|> arc(
endAbsolute = [profileStartX(%), profileStartY(%)],
interiorAbsolute = [
fanSize * 11 / 50 * cos(startAngle - 5),
fanSize * 11 / 50 * sin(startAngle - 5)
],
)
|> close()
return fanBlade
}
// Loft the fan blade cross sections into a single blade, then pattern them about the fan center
crossSections = [
fanBlade(offsetHeight = 4.5, startAngle = 50),
fanBlade(offsetHeight = (fanHeight - 2 - 4) / 2, startAngle = 30),
fanBlade(offsetHeight = fanHeight - 2, startAngle = 0)
]
loft(crossSections)
|> appearance(color = "#f3e2d8")
|> patternCircular3d(
instances = 9,
axis = [0, 0, 1],
center = [0, 0, 0],
arcDegrees = 360,
rotateDuplicates = true,
)
// Motor
// A small electric motor to power the fan
// Model the motor body and stem
topFacePlane = offsetPlane(XY, offset = 4)
motorBody = startSketchOn(topFacePlane)
|> circle(center = [0, 0], radius = 10, tag = $seg04)
|> extrude(length = 17)
|> appearance(color = "#021b55")
|> fillet(radius = 2, tags = [getOppositeEdge(seg04), seg04])
startSketchOn(offsetPlane(XY, offset = 21))
|> circle(center = [0, 0], radius = 1)
|> extrude(length = 3.8)
|> appearance(color = "#dbc89e")
`

View File

@ -60,7 +60,7 @@ export async function retrieveArgFromPipedCallExpression(
name: string
): Promise<KclCommandValue | undefined> {
const arg = callExpression.arguments.find(
(a) => a.label.type === 'Identifier' && a.label.name === name
(a) => a.label?.type === 'Identifier' && a.label?.name === name
)
if (
arg?.type === 'LabeledArg' &&

View File

@ -1,22 +1,75 @@
import type { OnboardingStatus } from '@rust/kcl-lib/bindings/OnboardingStatus'
import { isDesktop } from '@src/lib/isDesktop'
export const ONBOARDING_SUBPATHS: Record<string, OnboardingStatus> = {
INDEX: '/',
CAMERA: '/camera',
STREAMING: '/streaming',
EDITOR: '/editor',
PARAMETRIC_MODELING: '/parametric-modeling',
INTERACTIVE_NUMBERS: '/interactive-numbers',
COMMAND_K: '/command-k',
USER_MENU: '/user-menu',
PROJECT_MENU: '/project-menu',
EXPORT: '/export',
SKETCHING: '/sketching',
FUTURE_WORK: '/future-work',
} as const
export type OnboardingPath = OnboardingStatus & `/${string}`
export type DesktopOnboardingPath = OnboardingPath & `/desktop${string}`
export type BrowserOnboardingPath = OnboardingPath & `/browser${string}`
export const isOnboardingSubPath = (
// companion to "desktop routes" in `OnboardingRoutes` enum in Rust
export const desktopOnboardingPaths: Record<string, DesktopOnboardingPath> = {
welcome: '/desktop',
scene: '/desktop/scene',
toolbar: '/desktop/toolbar',
textToCadWelcome: '/desktop/text-to-cad',
textToCadPrompt: '/desktop/text-to-cad-prompt',
featureTreePane: '/desktop/feature-tree-pane',
codePane: '/desktop/code-pane',
projectFilesPane: '/desktop/project-pane',
otherPanes: '/desktop/other-panes',
promptToEditWelcome: '/desktop/prompt-to-edit',
promptToEditPrompt: '/desktop/prompt-to-edit-prompt',
promptToEditResult: '/desktop/prompt-to-edit-result',
imports: '/desktop/imports',
exports: '/desktop/exports',
conclusion: '/desktop/conclusion',
}
// companion to "web routes" in `OnboardingRoutes` enum in Rust
export const browserOnboardingPaths: Record<string, BrowserOnboardingPath> = {
welcome: '/browser',
scene: '/browser/scene',
toolbar: '/browser/toolbar',
textToCadWelcome: '/browser/text-to-cad',
textToCadPrompt: '/browser/text-to-cad-prompt',
featureTreePane: '/browser/feature-tree-pane',
promptToEditWelcome: '/browser/prompt-to-edit',
promptToEditPrompt: '/browser/prompt-to-edit-prompt',
promptToEditResult: '/browser/prompt-to-edit-result',
conclusion: '/browser/conclusion',
}
export const onboardingPaths = {
desktop: desktopOnboardingPaths,
browser: browserOnboardingPaths,
}
export const onboardingPathsArray = Object.values(onboardingPaths).flatMap(
(p) => Object.values(p)
)
/** Whatever the first onboarding path on the current platform is. */
export const onboardingStartPath = Object.values(
onboardingPaths[isDesktop() ? 'desktop' : 'browser']
)[0]
export const isOnboardingPath = (input: string): input is OnboardingStatus => {
return Object.values(onboardingPaths)
.flatMap((o) => Object.values(o))
.includes(input as OnboardingPath)
}
export const isDesktopOnboardingPath = (
input: string
): input is OnboardingStatus => {
return Object.values(ONBOARDING_SUBPATHS).includes(input as OnboardingStatus)
return Object.values(onboardingPaths.desktop).includes(
input as DesktopOnboardingPath
)
}
export const isBrowserOnboardingPath = (
input: string
): input is OnboardingStatus => {
return Object.values(onboardingPaths.browser).includes(
input as BrowserOnboardingPath
)
}

View File

@ -10,21 +10,6 @@ import {
import { isDesktop } from '@src/lib/isDesktop'
import { err } from '@src/lib/trap'
import type { DeepPartial } from '@src/lib/types'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
const prependRoutes =
(routesObject: Record<string, string>) => (prepend: string) => {
return Object.fromEntries(
Object.entries(routesObject).map(([constName, path]) => [
constName,
prepend + path,
])
)
}
type OnboardingPaths = {
[K in keyof typeof ONBOARDING_SUBPATHS]: `/onboarding${(typeof ONBOARDING_SUBPATHS)[K]}`
}
const SETTINGS = '/settings'
@ -44,9 +29,7 @@ export const PATHS = {
SETTINGS_PROJECT: `${SETTINGS}?tab=project` as const,
SETTINGS_KEYBINDINGS: `${SETTINGS}?tab=keybindings` as const,
SIGN_IN: '/signin',
ONBOARDING: prependRoutes(ONBOARDING_SUBPATHS)(
'/onboarding'
) as OnboardingPaths,
ONBOARDING: '/onboarding',
TELEMETRY: '/telemetry',
} as const
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`

View File

@ -12,7 +12,12 @@ import {
} from '@src/lib/constants'
import { getProjectInfo } from '@src/lib/desktop'
import { isDesktop } from '@src/lib/isDesktop'
import { BROWSER_PATH, PATHS, getProjectMetaByRouteId } from '@src/lib/paths'
import {
BROWSER_PATH,
PATHS,
getProjectMetaByRouteId,
safeEncodeForRouterPaths,
} from '@src/lib/paths'
import {
loadAndValidateSettings,
readLocalStorageAppSettingsFile,
@ -63,6 +68,17 @@ export const fileLoader: LoaderFunction = async (
}
}
// If we are navigating to the project and want to navigate to its
// default file, redirect to it keeping everything else in the URL the same.
if (projectPath && !currentFileName && fileExists && params.id) {
const encodedId = safeEncodeForRouterPaths(params.id)
const requestUrlWithDefaultFile = routerData.request.url.replace(
encodedId,
safeEncodeForRouterPaths(fallbackFile)
)
return redirect(requestUrlWithDefaultFile)
}
if (!fileExists || !currentFileName || !currentFilePath || !projectName) {
return redirect(
`${PATHS.FILE}/${encodeURIComponent(

View File

@ -41,7 +41,9 @@ export async function refreshPage(method = 'UI button') {
* Get all labels for a keyword call expression.
*/
export function allLabels(callExpression: CallExpressionKw): string[] {
return callExpression.arguments.map((a) => a.label.name)
return callExpression.arguments
.map((a) => a.label?.name)
.filter((a) => a !== undefined)
}
/**

View File

@ -99,6 +99,7 @@ export const systemIOMachine = setup({
files: RequestedKCLFile[]
requestedProjectName: string
override?: boolean
requestedSubRoute?: string
}
}
| {
@ -313,8 +314,9 @@ export const systemIOMachine = setup({
message: string
fileName: string
projectName: string
subRoute: string
}> => {
return { message: '', fileName: '', projectName: '' }
return { message: '', fileName: '', projectName: '', subRoute: '' }
}
),
[SystemIOMachineActors.bulkCreateKCLFilesAndNavigateToProject]: fromPromise(
@ -326,13 +328,15 @@ export const systemIOMachine = setup({
files: RequestedKCLFile[]
rootContext: AppMachineContext
requestedProjectName: string
requestedSubRoute?: string
}
}): Promise<{
message: string
fileName: string
projectName: string
subRoute: string
}> => {
return { message: '', fileName: '', projectName: '' }
return { message: '', fileName: '', projectName: '', subRoute: '' }
}
),
},
@ -550,7 +554,13 @@ export const systemIOMachine = setup({
// Clear on web? not desktop
actions: [
assign({
requestedFileName: ({ context, event }) => {
requestedProjectName: ({ event }) => {
assertEvent(event, SystemIOMachineEvents.done_importFileFromURL)
return {
name: event.output.projectName,
}
},
requestedFileName: ({ event }) => {
assertEvent(event, SystemIOMachineEvents.done_importFileFromURL)
// Gotcha: file could have an ending of .kcl...
const file = event.output.fileName.endsWith('.kcl')
@ -650,6 +660,7 @@ export const systemIOMachine = setup({
rootContext: self.system.get('root').getSnapshot().context,
requestedProjectName: event.data.requestedProjectName,
override: event.data.override,
requestedSubRoute: event.data.requestedSubRoute,
}
},
onDone: {
@ -657,7 +668,10 @@ export const systemIOMachine = setup({
actions: [
assign({
requestedProjectName: ({ event }) => {
return { name: event.output.projectName }
return {
name: event.output.projectName,
subRoute: event.output.subRoute,
}
},
}),
SystemIOMachineActions.toastSuccess,

View File

@ -90,6 +90,7 @@ const sharedBulkCreateWorkflow = async ({
message,
fileName: '',
projectName: '',
subRoute: 'subRoute' in input ? input.subRoute : '',
}
}
@ -336,7 +337,11 @@ export const systemIOMachineDesktop = systemIOMachine.provide({
rootContext: AppMachineContext
}
}) => {
return await sharedBulkCreateWorkflow({ input })
const message = await sharedBulkCreateWorkflow({ input })
return {
...message,
subRoute: '',
}
}
),
[SystemIOMachineActors.bulkCreateKCLFilesAndNavigateToProject]: fromPromise(
@ -349,6 +354,7 @@ export const systemIOMachineDesktop = systemIOMachine.provide({
rootContext: AppMachineContext
requestedProjectName: string
override?: boolean
requestedSubRoute?: string
}
}) => {
const message = await sharedBulkCreateWorkflow({
@ -357,8 +363,11 @@ export const systemIOMachineDesktop = systemIOMachine.provide({
override: input.override,
},
})
message.projectName = input.requestedProjectName
return message
return {
...message,
projectName: input.requestedProjectName,
subRoute: input.requestedSubRoute || '',
}
}
),
},

View File

@ -84,7 +84,7 @@ export type SystemIOContext = {
/** has the application gone through the initialization of systemIOMachine at least once.
* this is required to prevent chokidar from spamming invalid events during initialization. */
hasListedProjects: boolean
requestedProjectName: { name: string }
requestedProjectName: { name: string; subRoute?: string }
requestedFileName: { project: string; file: string; subRoute?: string }
canReadWriteProjectDirectory: { value: boolean; error: unknown }
clearURLParams: { value: boolean }
@ -106,7 +106,9 @@ export type RequestedKCLFile = {
export const waitForIdleState = async ({
systemIOActor,
}: { systemIOActor: ActorRefFrom<typeof systemIOMachine> }) => {
}: {
systemIOActor: ActorRefFrom<typeof systemIOMachine>
}) => {
// Check if already idle before setting up subscription
if (systemIOActor.getSnapshot().matches(SystemIOMachineStates.idle)) {
return Promise.resolve()

View File

@ -1,4 +1,3 @@
import { IS_NIGHTLY_OR_DEBUG } from '@src/routes/utils'
import type { FormEvent, HTMLProps } from 'react'
import { useEffect } from 'react'
import { toast } from 'react-hot-toast'
@ -351,13 +350,11 @@ const Home = () => {
</li>
</ul>
<ul className="flex flex-col">
{IS_NIGHTLY_OR_DEBUG && (
<li className="contents">
<div className="my-2">
<BillingDialog billingActor={billingActor} />
</div>
</li>
)}
<li className="contents">
<div className="my-2">
<BillingDialog billingActor={billingActor} />
</div>
</li>
<li className="contents">
<ActionButton
Element="externalLink"

View File

@ -0,0 +1,498 @@
import {
type BrowserOnboardingPath,
browserOnboardingPaths,
} from '@src/lib/onboardingPaths'
import { useRouteLoaderData, type RouteObject } from 'react-router-dom'
import {
isModelingCmdGroupReady,
OnboardingButtons,
OnboardingCard,
useAdvanceOnboardingOnFormSubmit,
useOnboardingHighlight,
useOnboardingPanes,
useOnModelingCmdGroupReadyOnce,
} from '@src/routes/Onboarding/utils'
import { Spinner } from '@src/components/Spinner'
import {
ONBOARDING_DATA_ATTRIBUTE,
BROWSER_PROJECT_NAME,
PROJECT_ENTRYPOINT,
} from '@src/lib/constants'
import { PATHS, joinRouterPaths } from '@src/lib/paths'
import type { Selections } from '@src/lib/selections'
import { systemIOActor, commandBarActor } from '@src/lib/singletons'
import type { IndexLoaderData } from '@src/lib/types'
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
import { useEffect, useState } from 'react'
import { VITE_KC_SITE_BASE_URL } from '@src/env'
import { openExternalBrowserIfDesktop } from '@src/lib/openWindow'
import {
browserAxialFan,
browserAxialFanAfterTextToCad,
} from '@src/lib/exampleKcl'
type BrowserOnboaringRoute = RouteObject & {
path: keyof typeof browserOnboardingPaths
}
/**
* This is the mapping between browser onboarding paths and the components that will be rendered.
* All components are defined below in this file.
*
* Browser onboarding content is completely separate from desktop onboarding content.
*/
const browserOnboardingComponents: Record<BrowserOnboardingPath, JSX.Element> =
{
'/browser': <Welcome />,
'/browser/scene': <Scene />,
'/browser/toolbar': <Toolbar />,
'/browser/text-to-cad': <TextToCad />,
'/browser/text-to-cad-prompt': <TextToCadPrompt />,
'/browser/feature-tree-pane': <FeatureTreePane />,
'/browser/prompt-to-edit': <PromptToEdit />,
'/browser/prompt-to-edit-prompt': <PromptToEditPrompt />,
'/browser/prompt-to-edit-result': <PromptToEditResult />,
'/browser/conclusion': <OnboardingConclusion />,
}
function Welcome() {
const thisOnboardingStatus: BrowserOnboardingPath = '/browser'
// Ensure panes are closed
useOnboardingPanes()
// Things that happen when we load this route
useEffect(() => {
// Overwrite the code with the browser-version of the axial-fan example
systemIOActor.send({
type: SystemIOMachineEvents.importFileFromURL,
data: {
requestedProjectName: BROWSER_PROJECT_NAME,
requestedFileNameWithExtension: PROJECT_ENTRYPOINT,
requestedCode: browserAxialFan,
requestedSubRoute: joinRouterPaths(
String(PATHS.ONBOARDING),
thisOnboardingStatus
),
},
})
}, [])
return (
<div className="cursor-not-allowed fixed inset-0 z-50 grid items-end justify-center p-2">
<OnboardingCard>
<h1 className="text-xl font-bold">Welcome to Zoo Design Studio</h1>
<p className="my-4">
Here is an axial fan that was made in Zoo Design Studio. It's a part
of a larger CPU cooler assembly sample you can view in the desktop
app, which supports multiple-part assemblies.
</p>
<p className="my-4">
Lets walk through the basics of how to get started, and how you can
use several tools at your disposal to create great designs.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="browser"
/>
</OnboardingCard>
</div>
)
}
function Scene() {
const thisOnboardingStatus: BrowserOnboardingPath = '/browser/scene'
// Things that happen when we load this route
useEffect(() => {
// Navigate to the `main.kcl` file
systemIOActor.send({
type: SystemIOMachineEvents.importFileFromURL,
data: {
requestedProjectName: BROWSER_PROJECT_NAME,
requestedFileNameWithExtension: PROJECT_ENTRYPOINT,
requestedCode: '',
requestedSubRoute: joinRouterPaths(
String(PATHS.ONBOARDING),
thisOnboardingStatus
),
},
})
}, [])
// Ensure panes are closed
useOnboardingPanes()
return (
<div className="pointer-events-none fixed inset-0 z-50 grid items-end justify-center p-2">
<OnboardingCard className="pointer-events-auto">
<h1 className="text-xl font-bold">Scene</h1>
<p className="my-4">
Here is a blank scene. There are three default planes shown when the
scene is empty. Try right-clicking and dragging to orbit around, and
scroll to zoom in and out.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="browser"
/>
</OnboardingCard>
</div>
)
}
function Toolbar() {
// Highlight the toolbar if it's present
useOnboardingHighlight('toolbar')
// Ensure panes are closed
useOnboardingPanes()
return (
<div className="cursor-not-allowed fixed inset-0 z-[99] grid items-start justify-center p-16">
<OnboardingCard>
<h1 className="text-xl font-bold">This is the toolbar</h1>
<p className="my-4">
You can perform modeling and sketching actions by clicking any of the
tools.
</p>
<OnboardingButtons currentSlug="/browser/toolbar" platform="browser" />
</OnboardingCard>
</div>
)
}
function TextToCad() {
// Highlight the text-to-cad button if it's present
useOnboardingHighlight('ai-group')
// Ensure panes are closed
useOnboardingPanes()
return (
<div className="cursor-not-allowed fixed inset-0 z-50 grid items-start justify-center p-16">
<OnboardingCard>
<h1 className="text-xl font-bold">Text-to-CAD</h1>
<p className="my-4">
This last button is Text-to-CAD. This allows you to write up a
description of what you want, and our AI will generate the CAD for
you. Text-to-CAD is currently in an experimental stage. We are
improving it every day.
</p>
<p className="my-4">
<strong>One</strong> Text-to-CAD generation costs{' '}
<strong>one credit per minute</strong>, rounded up to the nearest
minute. A large majority of Text-to-CAD generations take under a
minute. If you are on the free plan, you get 20 free credits per
month. With any of our paid plans, you get unlimited Text-to-CAD
generations.
</p>
<p className="my-4">
Lets walk through an example of how to use Text-to-CAD.
</p>
<OnboardingButtons
currentSlug="/browser/text-to-cad"
platform="browser"
/>
</OnboardingCard>
</div>
)
}
function TextToCadPrompt() {
const thisOnboardingStatus: BrowserOnboardingPath =
'/browser/text-to-cad-prompt'
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const prompt =
'Design a fan housing for a CPU cooler for a 120mm diameter fan with four holes for retaining clips.'
// Ensure panes are closed
useOnboardingPanes()
// Enter the text-to-cad flow with a prebaked prompt
useEffect(() => {
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'application',
name: 'Text-to-CAD',
argDefaultValues: {
method: 'existingProject',
projectName: loaderData?.project?.name,
prompt,
},
},
})
}, [loaderData?.project?.name])
// Make it so submitting the command just advances the onboarding
useAdvanceOnboardingOnFormSubmit(thisOnboardingStatus)
return (
<div className="cursor-not-allowed fixed inset-0 z-[99] grid items-center justify-center">
<OnboardingCard>
<h1 className="text-xl font-bold">Text-to-CAD prompt</h1>
<p className="my-4">
When you click the Text-to-CAD button, it opens the command palette to
where you can input a text prompt. To save you a Text-to-CAD
generation credit, we are going to use a pre-rolled Text-to-CAD prompt
for this example. Click next to see an example of what Text-to-CAD can
generate.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="browser"
/>
</OnboardingCard>
</div>
)
}
function FeatureTreePane() {
const thisOnboardingStatus: BrowserOnboardingPath =
'/browser/feature-tree-pane'
// Highlight the feature tree pane button if it's present
useOnboardingHighlight('feature-tree-pane-button')
// Open the feature tree pane on mount, close on unmount
useOnboardingPanes(['feature-tree'])
// Overwrite the code with the "generated" KCL
useEffect(() => {
systemIOActor.send({
type: SystemIOMachineEvents.importFileFromURL,
data: {
requestedProjectName: BROWSER_PROJECT_NAME,
requestedFileNameWithExtension: PROJECT_ENTRYPOINT,
requestedCode: browserAxialFan,
requestedSubRoute: joinRouterPaths(
String(PATHS.ONBOARDING),
thisOnboardingStatus
),
},
})
}, [])
return (
<div className="fixed inset-0 z-[99] p-8 grid justify-center items-end">
<OnboardingCard className="col-start-3 col-span-2">
<h1 className="text-xl font-bold">CPU Fan Housing</h1>
<p className="my-4">
This is an example of a generated CAD model; it's the same model we
showed you at the start. We skipped the real generation for this
tutorial, but normally you'll be asked to approve the generation
first.
</p>
<p className="my-4">
To the left are the panes. We have opened the feature tree pane for
you. The feature tree pane displays all the CAD functions that were
performed to create this part. You can double click feature tree items
to edit their parameters.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="browser"
/>
</OnboardingCard>
</div>
)
}
function PromptToEdit() {
const thisOnboardingStatus: BrowserOnboardingPath = '/browser/prompt-to-edit'
// Click the text-to-cad dropdown button if it's available
useEffect(() => {
const dropdownButton = document.querySelector(
`[data-${ONBOARDING_DATA_ATTRIBUTE}="ai-dropdown-button"]`
)
if (dropdownButton === null) {
console.error(
`Expected dropdown is not present in onboarding step '${thisOnboardingStatus}'`
)
return
}
if (dropdownButton instanceof HTMLButtonElement) {
dropdownButton.click()
}
}, [])
// Close the panes on mount, close on unmount
useOnboardingPanes()
// Make it so submitting the command just advances the onboarding
useAdvanceOnboardingOnFormSubmit(thisOnboardingStatus)
return (
<div className="cursor-not-allowed fixed inset-0 z-50 grid items-center justify-center p-16">
<OnboardingCard className="col-start-3 col-span-2">
<h1 className="text-xl font-bold">Modify with Zoo Text-to-CAD</h1>
<p className="my-4">
Text-to-CAD not only can <strong>create</strong> a part, but also{' '}
<strong>modify</strong> an existing part. In the dropdown, youll see
Modify with Zoo Text-to-CAD. Once clicked, youll describe the
change you want for your part, and our AI will generate the change.
Once again, this will cost <strong>one credit per minute</strong> it
took to generate. Once again, most of the time, this is under a
minute.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="browser"
/>
</OnboardingCard>
</div>
)
}
function PromptToEditPrompt() {
const thisOnboardingStatus: BrowserOnboardingPath =
'/browser/prompt-to-edit-prompt'
const prompt =
'Change the housing to be for a 150 mm diameter fan, make it 30 mm tall, and change the color to purple.'
// Ensure panes are closed
useOnboardingPanes()
// Enter the prompt-to-edit flow with a prebaked prompt
const [isReady, setIsReady] = useState(
isModelingCmdGroupReady(commandBarActor.getSnapshot())
)
useOnModelingCmdGroupReadyOnce(() => {
setIsReady(true)
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'modeling',
name: 'Prompt-to-edit',
argDefaultValues: {
selection: {
graphSelections: [],
otherSelections: [],
} satisfies Selections,
prompt,
},
},
})
}, [])
// Make it so submitting the command just advances the onboarding
useAdvanceOnboardingOnFormSubmit(thisOnboardingStatus)
return (
<div className="cursor-not-allowed fixed inset-0 z-[99] grid items-center justify-center">
<OnboardingCard className="pointer-events-auto">
<h1 className="text-xl font-bold">Modify with Text-to-CAD prompt</h1>
{!isReady && (
<p className="absolute top-0 right-0 m-4 w-fit flex items-center py-1 px-2 rounded bg-chalkboard-20 dark:bg-chalkboard-80">
<Spinner className="w-5 h-5 inline-block mr-2" />
Waiting for connection...
</p>
)}
<p className="my-4">
To save you a credit, we are using a pre-rolled Text-to-CAD prompt to
edit your existing fan housing. You can see the prompt in the window
above. Click next to see an example of what modifying with Text-to-CAD
would look like.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="browser"
/>
</OnboardingCard>
</div>
)
}
function PromptToEditResult() {
const thisOnboardingStatus: BrowserOnboardingPath =
'/browser/prompt-to-edit-result'
// Open the code pane on mount, close on unmount
useOnboardingPanes(['code'])
// Overwrite the code with the "generated" KCL
useEffect(() => {
systemIOActor.send({
type: SystemIOMachineEvents.importFileFromURL,
data: {
requestedProjectName: BROWSER_PROJECT_NAME,
requestedFileNameWithExtension: PROJECT_ENTRYPOINT,
requestedCode: browserAxialFanAfterTextToCad,
requestedSubRoute: joinRouterPaths(
String(PATHS.ONBOARDING),
thisOnboardingStatus
),
},
})
}, [])
return (
<div className="cursor-not-allowed fixed inset-0 z-[99] p-8 grid justify-center items-end">
<OnboardingCard className="col-start-3 col-span-2">
<h1 className="text-xl font-bold">Result</h1>
<p className="my-4">
This is an example of an edit that Text-to-CAD can make for you. We
skipped the real generation for this tutorial, but normally you'll be
asked to approve the generation first.
</p>
<p className="my-4">
Text-to-CAD will make changes across files in your project, so if you
have named parameters in another file that need to change to complete
your request, it is smart enough to go find their source and change
them.
</p>
<p className="my-4">
All of our Text-to-CAD capabilities are experimental, so please report
any issues to us and stay tuned for updates! We are working on it
every day.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="browser"
/>
</OnboardingCard>
</div>
)
}
function OnboardingConclusion() {
// Close the panes on mount, close on unmount
useOnboardingPanes()
return (
<div className="cursor-not-allowed fixed inset-0 z-50 p-16 grid justify-center items-center">
<OnboardingCard>
<h1 className="text-xl font-bold">Download the desktop app</h1>
<p className="my-4">
We highly encourage you to{' '}
<a
onClick={openExternalBrowserIfDesktop(
`${VITE_KC_SITE_BASE_URL}/modeling-app/download/nightly`
)}
href="https://zoo.dev/modeling-app/download/nightly"
target="_blank"
rel="noopener noreferrer"
>
download our desktop app
</a>{' '}
so you can experience the full functionality of Zoo Design Studio,
including multi-part assemblies, project management, and local file
saving.
</p>
<OnboardingButtons
currentSlug="/browser/conclusion"
platform="browser"
/>
</OnboardingCard>
</div>
)
}
export const browserOnboardingRoutes: BrowserOnboaringRoute[] = [
...Object.values(browserOnboardingPaths).map((path) => ({
path,
element: browserOnboardingComponents[path],
})),
]

View File

@ -1,68 +0,0 @@
import { SettingsSection } from '@src/components/Settings/SettingsSection'
import type { CameraSystem } from '@src/lib/cameraControls'
import { cameraMouseDragGuards, cameraSystems } from '@src/lib/cameraControls'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
import { settingsActor, useSettings } from '@src/lib/singletons'
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
export default function Units() {
const {
modeling: { mouseControls },
} = useSettings()
return (
<div className="fixed inset-0 z-50 grid items-end justify-start px-4 pointer-events-none">
<div
className={
'relative pointer-events-auto max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<SettingsSection
title="Mouse Controls"
description="Choose what buttons you want to use on your mouse or trackpad to move around the 3D view. Try them out above and choose the one that feels most comfortable to you."
className="my-4 last-of-type:mb-12"
headingClassName="text-3xl font-bold"
>
<select
id="camera-controls"
className="block w-full px-3 py-1 bg-transparent border border-chalkboard-30"
value={mouseControls.current}
onChange={(e) => {
settingsActor.send({
type: 'set.modeling.mouseControls',
data: {
level: 'user',
value: e.target.value as CameraSystem,
},
})
}}
>
{cameraSystems.map((program) => (
<option key={program} value={program}>
{program}
</option>
))}
</select>
<ul className="mx-4 my-2 text-sm leading-relaxed">
<li>
<strong>Pan:</strong>{' '}
{cameraMouseDragGuards[mouseControls.current].pan.description}
</li>
<li>
<strong>Zoom:</strong>{' '}
{cameraMouseDragGuards[mouseControls.current].zoom.description}
</li>
<li>
<strong>Rotate:</strong>{' '}
{cameraMouseDragGuards[mouseControls.current].rotate.description}
</li>
</ul>
</SettingsSection>
<OnboardingButtons
currentSlug={ONBOARDING_SUBPATHS.CAMERA}
dismissClassName="right-auto left-full"
/>
</div>
</div>
)
}

View File

@ -1,43 +0,0 @@
import { COMMAND_PALETTE_HOTKEY } from '@src/components/CommandBar/CommandBar'
import usePlatform from '@src/hooks/usePlatform'
import { hotkeyDisplay } from '@src/lib/hotkeyWrapper'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
import { OnboardingButtons, kbdClasses } from '@src/routes/Onboarding/utils'
export default function CmdK() {
const platformName = usePlatform()
return (
<div className="fixed inset-0 z-50 grid items-end justify-center pointer-events-none">
<div
className={
'relative pointer-events-auto max-w-full xl:max-w-4xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<h2 className="text-2xl font-bold">Command Bar</h2>
<p className="my-4">
Press{' '}
<kbd className={kbdClasses}>
{hotkeyDisplay(COMMAND_PALETTE_HOTKEY, platformName)}
</kbd>{' '}
to open the command bar. Try changing your theme with it.
</p>
<p className="my-4">
We are working on a command bar that will allow you to quickly see and
search for any available commands. We are building Zoo Design Studio's
state management system on top of{' '}
<a
href="https://xstate.js.org/"
rel="noreferrer noopener"
target="_blank"
>
XState
</a>
. You can control settings, authentication, and file management from
the command bar, as well as a growing number of modeling commands.
</p>
<OnboardingButtons currentSlug={ONBOARDING_SUBPATHS.COMMAND_K} />
</div>
</div>
)
}

View File

@ -1,76 +0,0 @@
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
import {
OnboardingButtons,
kbdClasses,
useDemoCode,
} from '@src/routes/Onboarding/utils'
export default function OnboardingCodeEditor() {
useDemoCode()
return (
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div
className={
'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1 overflow-y-auto">
<h2 className="text-3xl font-bold">
Editing code with <span className="text-primary">kcl</span>
</h2>
<p className="my-4">
kcl is our language for describing geometry. Building our own
language is difficult, but we chose to do it to have a language
honed for spatial relationships and geometric computation. It'll
always be open-source, and we hope it can grow into a new standard
for describing parametric objects.
</p>
<p className="my-4">
The left pane is where you write your code. It's a code editor with
syntax highlighting and autocompletion for kcl. New features arrive
in kcl before they're available as point-and-click tools, so it's
good to have a link to{' '}
<a
href="https://zoo.dev/docs/kcl"
rel="noreferrer noopener"
target="_blank"
>
our kcl docs
</a>{' '}
handy while you design for now. It's also available in the menu in
the corner of the code pane.
</p>
<p className="my-4">
We've built a{' '}
<a
href="https://github.com/KittyCAD/kcl-lsp"
rel="noreferrer noopener"
target="_blank"
>
language server
</a>{' '}
for kcl that provides documentation and autocompletion automatically
generated from our compiler code. You can try it out by hovering
over some of the function names in the pane now. If you like using
VSCode, you can try out our{' '}
<a
href="https://marketplace.visualstudio.com/items?itemName=KittyCAD.kcl-language-server"
rel="noreferrer noopener"
target="_blank"
>
VSCode extension
</a>
.
</p>
<p className="my-4">
You can resize the pane by dragging the handle on the right, and you
can collapse it by clicking the X button in the pane's title bar or
pressing <kbd className={kbdClasses}>Shift + C</kbd>.
</p>
</section>
<OnboardingButtons currentSlug={ONBOARDING_SUBPATHS.EDITOR} />
</div>
</div>
)
}

View File

@ -0,0 +1,634 @@
import {
type DesktopOnboardingPath,
desktopOnboardingPaths,
} from '@src/lib/onboardingPaths'
import { useRouteLoaderData, type RouteObject } from 'react-router-dom'
import {
isModelingCmdGroupReady,
OnboardingButtons,
OnboardingCard,
useAdvanceOnboardingOnFormSubmit,
useOnboardingHighlight,
useOnboardingPanes,
useOnModelingCmdGroupReadyOnce,
} from '@src/routes/Onboarding/utils'
import { useEffect, useState } from 'react'
import { commandBarActor, systemIOActor } from '@src/lib/singletons'
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
import { joinRouterPaths, PATHS } from '@src/lib/paths'
import {
ONBOARDING_DATA_ATTRIBUTE,
ONBOARDING_PROJECT_NAME,
} from '@src/lib/constants'
import type { IndexLoaderData } from '@src/lib/types'
import type { Selections } from '@src/lib/selections'
import { Spinner } from '@src/components/Spinner'
import { modifiedFanHousing } from '@src/lib/exampleKcl'
type DesktopOnboardingRoute = RouteObject & {
path: keyof typeof desktopOnboardingPaths
}
/**
* This is the mapping between desktop onboarding paths and the components that will be rendered.
* All components are defined below in this file.
*
* Desktop onboarding content is completely separate from browser onboarding content.
*/
const onboardingComponents: Record<DesktopOnboardingPath, JSX.Element> = {
'/desktop': <Welcome />,
'/desktop/scene': <Scene />,
'/desktop/toolbar': <Toolbar />,
'/desktop/text-to-cad': <TextToCad />,
'/desktop/text-to-cad-prompt': <TextToCadPrompt />,
'/desktop/feature-tree-pane': <FeatureTreePane />,
'/desktop/code-pane': <CodePane />,
'/desktop/project-pane': <ProjectPane />,
'/desktop/other-panes': <OtherPanes />,
'/desktop/prompt-to-edit': <PromptToEdit />,
'/desktop/prompt-to-edit-prompt': <PromptToEditPrompt />,
'/desktop/prompt-to-edit-result': <PromptToEditResult />,
'/desktop/imports': <Imports />,
'/desktop/exports': <Exports />,
'/desktop/conclusion': <OnboardingConclusion />,
}
function Welcome() {
const thisOnboardingStatus: DesktopOnboardingPath = '/desktop'
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
// Ensure panes are closed
useOnboardingPanes()
// Things that happen when we load this route
useEffect(() => {
// Navigate to the `main.kcl` file
systemIOActor.send({
type: SystemIOMachineEvents.navigateToFile,
data: {
requestedProjectName:
loaderData?.project?.name || ONBOARDING_PROJECT_NAME,
requestedFileName: 'main.kcl',
requestedSubRoute: joinRouterPaths(
String(PATHS.ONBOARDING),
thisOnboardingStatus
),
},
})
}, [loaderData?.project?.name])
return (
<div className="cursor-not-allowed fixed inset-0 z-50 grid items-end justify-center p-2">
<OnboardingCard>
<h1 className="text-xl font-bold">Welcome to Zoo Design Studio</h1>
<p className="my-4">
Here is an assembly of a CPU fan that was made in Zoo Design Studio.
</p>
<p className="my-4">
Lets walk through the basics of how to get started, and how you can
use several tools at your disposal to create great designs.
</p>
<OnboardingButtons currentSlug="/desktop" platform="desktop" />
</OnboardingCard>
</div>
)
}
function Scene() {
const thisOnboardingStatus: DesktopOnboardingPath = '/desktop/scene'
// Ensure panes are closed
useOnboardingPanes()
// Things that happen when we load this route
useEffect(() => {
// Create if necessary and navigate to the `blank.kcl` file
systemIOActor.send({
type: SystemIOMachineEvents.importFileFromURL,
data: {
requestedProjectName: ONBOARDING_PROJECT_NAME,
requestedFileNameWithExtension: 'blank.kcl',
requestedCode: '',
requestedSubRoute: joinRouterPaths(
String(PATHS.ONBOARDING),
thisOnboardingStatus
),
},
})
}, [])
return (
<div className="pointer-events-none fixed inset-0 z-50 grid items-end justify-center p-2">
<OnboardingCard className="pointer-events-auto">
<h1 className="text-xl font-bold">Scene</h1>
<p className="my-4">
Here is a blank scene. There are three default planes shown when the
scene is empty. Try right-clicking and dragging to orbit around, and
scroll to zoom in and out.
</p>
<OnboardingButtons currentSlug="/desktop/scene" platform="desktop" />
</OnboardingCard>
</div>
)
}
function Toolbar() {
// Highlight the toolbar if it's present
useOnboardingHighlight('toolbar')
// Ensure panes are closed
useOnboardingPanes()
return (
<div className="cursor-not-allowed fixed inset-0 z-[99] grid items-start justify-center p-16">
<OnboardingCard>
<h1 className="text-xl font-bold">This is the toolbar</h1>
<p className="my-4">
You can perform modeling and sketching actions by clicking any of the
tools.
</p>
<OnboardingButtons currentSlug="/desktop/toolbar" platform="desktop" />
</OnboardingCard>
</div>
)
}
function TextToCad() {
// Highlight the text-to-cad button if it's present
useOnboardingHighlight('ai-group')
// Ensure panes are closed
useOnboardingPanes()
return (
<div className="cursor-not-allowed fixed inset-0 z-50 grid items-start justify-center p-16">
<OnboardingCard>
<h1 className="text-xl font-bold">Text-to-CAD</h1>
<p className="my-4">
This last button is Text-to-CAD. This allows you to write up a
description of what you want, and our AI will generate the CAD for
you. Text-to-CAD is currently in an experimental stage. We are
improving it every day.
</p>
<p className="my-4">
<strong>One</strong> Text-to-CAD generation costs{' '}
<strong>one credit per minute</strong>, rounded up to the nearest
minute. A large majority of Text-to-CAD generations take under a
minute. If you are on the free plan, you get 20 free credits per
month. With any of our paid plans, you get unlimited Text-to-CAD
generations.
</p>
<p className="my-4">
Lets walk through an example of how to use Text-to-CAD.
</p>
<OnboardingButtons
currentSlug="/desktop/text-to-cad"
platform="desktop"
/>
</OnboardingCard>
</div>
)
}
function TextToCadPrompt() {
const thisOnboardingStatus: DesktopOnboardingPath =
'/desktop/text-to-cad-prompt'
const loaderData = useRouteLoaderData(PATHS.FILE) as IndexLoaderData
const prompt =
'Design a fan housing for a CPU cooler for a 120mm diameter fan with four holes for retaining clips'
// Ensure panes are closed
useOnboardingPanes()
// Enter the text-to-cad flow with a prebaked prompt
useEffect(() => {
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'application',
name: 'Text-to-CAD',
argDefaultValues: {
method: 'existingProject',
projectName: loaderData?.project?.name,
prompt,
},
},
})
}, [loaderData?.project?.name])
// Make it so submitting the command just advances the onboarding
useAdvanceOnboardingOnFormSubmit(thisOnboardingStatus, 'desktop')
return (
<div className="cursor-not-allowed fixed inset-0 z-[99] grid items-center justify-center">
<OnboardingCard className="pointer-events-auto">
<h1 className="text-xl font-bold">Text-to-CAD prompt</h1>
<p className="my-4">
When you click the Text-to-CAD button, it opens the command palette to
where you can input a text prompt. To save you a Text-to-CAD
generation credit, we are going to use a pre-rolled Text-to-CAD prompt
for this example. Click next to see an example of what Text-to-CAD can
generate.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="desktop"
/>
</OnboardingCard>
</div>
)
}
function FeatureTreePane() {
const thisOnboardingStatus: DesktopOnboardingPath =
'/desktop/feature-tree-pane'
const generatedFileName = 'fan-housing.kcl'
// Highlight the feature tree pane button if it's present
useOnboardingHighlight('feature-tree-pane-button')
// Open the feature tree pane on mount, close on unmount
useOnboardingPanes(['feature-tree'])
// navigate to the "generated" file
useEffect(() => {
systemIOActor.send({
type: SystemIOMachineEvents.navigateToFile,
data: {
requestedProjectName: ONBOARDING_PROJECT_NAME,
requestedFileName: generatedFileName,
requestedSubRoute: joinRouterPaths(
String(PATHS.ONBOARDING),
thisOnboardingStatus
),
},
})
}, [])
return (
<div className="cursor-not-allowed fixed inset-0 z-[99] p-8 grid justify-center items-end">
<OnboardingCard className="col-start-3 col-span-2">
<h1 className="text-xl font-bold">CPU Fan Housing</h1>
<p className="my-4">
This is an example of a generated CAD model. We skipped the real
generation for this tutorial, but normally you'll be asked to approve
the generation first.
</p>
<p className="my-4">
To the left are the panes. We have opened the feature tree pane for
you. The feature tree pane displays all the CAD functions that were
performed to create this part. You can double click feature tree items
to edit their parameters.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="desktop"
/>
</OnboardingCard>
</div>
)
}
function CodePane() {
// Highlight the feature tree pane button if it's present
useOnboardingHighlight('code-pane-button')
// Open the code pane on mount, close on unmount
useOnboardingPanes(['code'])
return (
<div className="cursor-not-allowed fixed inset-0 z-50 p-8 grid justify-center items-end">
<OnboardingCard className="col-start-3 col-span-2">
<h1 className="text-xl font-bold">KCL Code</h1>
<p className="my-4">
This is the KCL Pane. KCL (KittyCAD Language) is a scripting language
we created to describe CAD geometries. This code is the source of
truth, everything you do to the model will change the code.
</p>
<p className="my-4">
KCL boasts other scripting features such as imports, functions and
logic. Not only can you edit your geometry from the feature tree, but
you can also edit the code directly.
</p>
<OnboardingButtons
currentSlug="/desktop/code-pane"
platform="desktop"
/>
</OnboardingCard>
</div>
)
}
function ProjectPane() {
const thisOnboardingStatus: DesktopOnboardingPath = '/desktop/project-pane'
// Highlight the feature tree pane button if it's present
useOnboardingHighlight('files-pane-button')
// Open the code pane on mount, close on unmount
useOnboardingPanes(['files'])
return (
<div className="cursor-not-allowed fixed inset-0 z-50 p-8 grid justify-center items-end">
<OnboardingCard className="col-start-3 col-span-2">
<h1 className="text-xl font-bold">Files Pane</h1>
<p className="my-4">
The next pane is the Project Files Pane. Here you can see all of the
files you have in this project. This can be other KCL files as well as
external CAD files (STEP, STL, OBJ, etc.).
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="desktop"
/>
</OnboardingCard>
</div>
)
}
function OtherPanes() {
const thisOnboardingStatus: DesktopOnboardingPath = '/desktop/other-panes'
// Highlight the log and variable panes button if it's present
useOnboardingHighlight('logs-pane-button')
useOnboardingHighlight('variables-pane-button')
// Open the panes on mount, close on unmount
useOnboardingPanes(['logs', 'variables'])
return (
<div className="cursor-not-allowed fixed inset-0 z-50 p-8 grid justify-center items-end">
<OnboardingCard className="col-start-3 col-span-2">
<h1 className="text-xl font-bold">Other panes</h1>
<p className="my-4">
These last two panes are the Variables Pane and Logs Pane. The
Variables pane will display the numeric values of any parameters you
made, along with other entities and types created in your file. The
Logs pane will show error logs.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="desktop"
/>
</OnboardingCard>
</div>
)
}
function PromptToEdit() {
const thisOnboardingStatus: DesktopOnboardingPath = '/desktop/prompt-to-edit'
// Click the text-to-cad dropdown button if it's available
useEffect(() => {
const dropdownButton = document.querySelector(
`[data-${ONBOARDING_DATA_ATTRIBUTE}="ai-dropdown-button"]`
)
if (dropdownButton === null) {
console.error(
`Expected dropdown is not present in onboarding step '${thisOnboardingStatus}'`
)
return
}
if (dropdownButton instanceof HTMLButtonElement) {
dropdownButton.click()
}
}, [])
// Close the panes on mount, close on unmount
useOnboardingPanes()
// Make it so pressing Enter advances instead of toggling the dropdown
useAdvanceOnboardingOnFormSubmit(thisOnboardingStatus, 'desktop')
return (
<div className="cursor-not-allowed fixed inset-0 z-50 p-8 grid justify-center items-center">
<OnboardingCard className="col-start-3 col-span-2">
<h1 className="text-xl font-bold">Modify with Zoo Text-to-CAD</h1>
<p className="my-4">
Text-to-CAD not only can <strong>create</strong> a part, but also{' '}
<strong>modify</strong> an existing part. In the dropdown, youll see
Modify with Zoo Text-to-CAD. Once clicked, youll describe the
change you want for your part, and our AI will generate the change.
Once again, this will cost <strong>one credit per minute</strong> it
took to generate. Once again, most of the time, this is under a
minute.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="desktop"
/>
</OnboardingCard>
</div>
)
}
function PromptToEditPrompt() {
const thisOnboardingStatus: DesktopOnboardingPath =
'/desktop/prompt-to-edit-prompt'
const prompt =
'Change the housing to be for a 150 mm diameter fan, make it 30 mm tall, and change the color to purple.'
// Ensure panes are closed
useOnboardingPanes()
// Enter the prompt-to-edit flow with a prebaked prompt
const [isReady, setIsReady] = useState(
isModelingCmdGroupReady(commandBarActor.getSnapshot())
)
useOnModelingCmdGroupReadyOnce(() => {
setIsReady(true)
commandBarActor.send({
type: 'Find and select command',
data: {
groupId: 'modeling',
name: 'Prompt-to-edit',
argDefaultValues: {
selection: {
graphSelections: [],
otherSelections: [],
} satisfies Selections,
prompt,
},
},
})
}, [])
// Make it so submitting the command just advances the onboarding
useAdvanceOnboardingOnFormSubmit(thisOnboardingStatus, 'desktop')
return (
<div className="cursor-not-allowed fixed inset-0 z-[99] grid items-center justify-center">
<OnboardingCard className="pointer-events-auto">
<h1 className="text-xl font-bold">Modify with Text-to-CAD prompt</h1>
{!isReady && (
<p className="absolute top-0 right-0 m-4 w-fit flex items-center py-1 px-2 rounded bg-chalkboard-20 dark:bg-chalkboard-80">
<Spinner className="w-5 h-5 inline-block mr-2" />
Waiting for connection...
</p>
)}
<p className="my-4">
To save you a credit, we are using a pre-rolled Text-to-CAD prompt to
edit your existing fan housing. You can see the prompt in the window
above. Click next to see an example of what modifying with Text-to-CAD
would look like.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="desktop"
/>
</OnboardingCard>
</div>
)
}
function PromptToEditResult() {
const thisOnboardingStatus: DesktopOnboardingPath =
'/desktop/prompt-to-edit-result'
// Open the code pane on mount, close on unmount
useOnboardingPanes(['code'])
useEffect(() => {
// Navigate to the `main.kcl` file
systemIOActor.send({
type: SystemIOMachineEvents.bulkCreateKCLFilesAndNavigateToProject,
data: {
requestedProjectName: ONBOARDING_PROJECT_NAME,
files: [
{
requestedFileName: 'fan-housing.kcl',
requestedProjectName: ONBOARDING_PROJECT_NAME,
requestedCode: modifiedFanHousing,
},
],
override: true,
requestedSubRoute: joinRouterPaths(
String(PATHS.ONBOARDING),
thisOnboardingStatus
),
},
})
}, [])
return (
<div className="cursor-not-allowed fixed inset-0 z-[99] p-8 grid justify-center items-end">
<OnboardingCard className="col-start-3 col-span-2">
<h1 className="text-xl font-bold">Result</h1>
<p className="my-4">
This is an example of an edit that Text-to-CAD can make for you. We
skipped the real generation for this tutorial, but normally you'll be
asked to approve the generation first.
</p>
<p className="my-4">
Text-to-CAD will make changes across files in your project, so if you
have named parameters in another file that need to change to complete
your request, it is smart enough to go find their source and change
them.
</p>
<p className="my-4">
All of our Text-to-CAD capabilities are experimental, so please report
any issues to us and stay tuned for updates! We are working on it
every day.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="desktop"
/>
</OnboardingCard>
</div>
)
}
function Imports() {
const thisOnboardingStatus: DesktopOnboardingPath = '/desktop/imports'
// Highlight the import and insert buttons if they're present
useOnboardingHighlight('add-file-to-project-pane-button')
useOnboardingHighlight('insert')
// Close the panes on mount, close on unmount
useOnboardingPanes()
return (
<div className="cursor-not-allowed fixed inset-0 z-50 p-16 flex flex-col gap-8 items-center">
<OnboardingCard>
<h1 className="text-xl font-bold">Add file(s) to project</h1>
<p className="my-4">
"Add file(s) to project" is available in the left sidebar. Use it to
bring files into your project, whether from the sample library or from
your local drive.
</p>
<h1 className="text-xl font-bold">Insert parts</h1>
<p className="my-4">
Once a file has been added to your project, you can add it to the
scene using insert. Insert is available in the toolbar. This is the
first step to making assemblies!
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="desktop"
/>
</OnboardingCard>
</div>
)
}
function Exports() {
const thisOnboardingStatus: DesktopOnboardingPath = '/desktop/exports'
// Highlight the export button if it's present
useOnboardingHighlight('export-pane-button')
// Close the panes on mount, close on unmount
useOnboardingPanes()
return (
<div className="cursor-not-allowed fixed inset-0 z-50 p-16 grid justify-start items-center">
<OnboardingCard>
<h1 className="text-xl font-bold">Exporting</h1>
<p className="my-4">
You can export the currently-opened part by clicking the Export button
in the left sidebar. We support exporting to STEP, gLTF, STL, OBJ, and
more.
</p>
<OnboardingButtons
currentSlug={thisOnboardingStatus}
platform="desktop"
dismissPosition="right"
/>
</OnboardingCard>
</div>
)
}
function OnboardingConclusion() {
// Highlight the App logo
useOnboardingHighlight('app-logo')
// Close the panes on mount, close on unmount
useOnboardingPanes(
['feature-tree', 'code', 'files'],
['feature-tree', 'code', 'files']
)
return (
<div className="cursor-not-allowed fixed inset-0 z-50 p-16 grid justify-center items-center">
<OnboardingCard>
<h1 className="text-xl font-bold">Time to start building</h1>
<p className="my-4">
We appreciate you downloading Zoo Design Studio and taking the time to
walk through the basics. To navigate back home to create your own
project, click the Zoo button in the top left (gesture). To learn more
detailed and advanced techniques, go here (TODO tutorials).
</p>
<OnboardingButtons
currentSlug="/desktop/conclusion"
platform="desktop"
/>
</OnboardingCard>
</div>
)
}
export const desktopOnboardingRoutes: DesktopOnboardingRoute[] = [
...Object.values(desktopOnboardingPaths).map((path) => ({
path,
index: true,
element: onboardingComponents[path],
})),
]

View File

@ -1,56 +0,0 @@
import { APP_NAME } from '@src/lib/constants'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
export default function Export() {
return (
<div className="fixed grid justify-center items-end inset-0 z-50 pointer-events-none">
<div
className={
'relative pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1">
<h2 className="text-2xl font-bold">Export</h2>
<p className="my-4">
In addition to the "Export current part" button in the project menu,
you can also click the Export button icon at the bottom of the left
sidebar. Try clicking it now.
</p>
<p className="my-4">
{APP_NAME} uses{' '}
<a
href="https://zoo.dev/gltf-format-extension"
rel="noopener noreferrer"
target="_blank"
>
our open-source extension proposal
</a>{' '}
for the glTF file format.{' '}
<a
href="https://zoo.dev/docs/api/convert-cad-file"
rel="noopener noreferrer"
target="_blank"
>
Our conversion API
</a>{' '}
can convert to and from most common CAD file formats, allowing
export to almost any CAD software.
</p>
<p className="my-4">
Our teammate Katie is working on the file format, check out{' '}
<a
href="https://github.com/KhronosGroup/glTF/pull/2343"
target="_blank"
rel="noreferrer noopener"
>
her standards proposal on GitHub
</a>
!
</p>
</section>
<OnboardingButtons currentSlug={ONBOARDING_SUBPATHS.EXPORT} />
</div>
</div>
)
}

View File

@ -1,65 +0,0 @@
import { useEffect } from 'react'
import { useModelingContext } from '@src/hooks/useModelingContext'
import { APP_NAME } from '@src/lib/constants'
import { sceneInfra } from '@src/lib/singletons'
import { OnboardingButtons, useDemoCode } from '@src/routes/Onboarding/utils'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
export default function FutureWork() {
const { send } = useModelingContext()
// Reset the code, the camera, and the modeling state
useDemoCode()
useEffect(() => {
send({ type: 'Cancel' }) // in case the user hit 'Next' while still in sketch mode
// eslint-disable-next-line @typescript-eslint/no-floating-promises
sceneInfra.camControls.resetCameraPosition()
}, [send])
return (
<div className="fixed grid justify-center items-center inset-0 bg-chalkboard-100/50 z-50">
<div className="relative max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded">
<h1 className="text-2xl font-bold">Future Work</h1>
<p className="my-4">
We have curves, cuts, multi-profile sketch mode, and many more CAD
features coming soon. We want your feedback on this user interface,
and we want to know what features you want to see next. Please message
us in{' '}
<a
href="https://discord.gg/JQEpHR7Nt2"
target="_blank"
rel="noreferrer noopener"
>
our Discord server
</a>
and{' '}
<a
href="https://github.com/KittyCAD/modeling-app/issues/new/choose"
rel="noreferrer noopener"
target="_blank"
>
open issues on GitHub
</a>
.
</p>
<p className="my-4">
If you make anything with the app we'd love to see it, feel free to{' '}
<a
href="https://twitter.com/zoodotdev"
target="_blank"
rel="noreferrer noopener"
>
tag us on X
</a>
! Thank you for taking time to try out {APP_NAME}, and build the
future of hardware design with us.
</p>
<p className="my-4">💚 The Zoo Team</p>
<OnboardingButtons
currentSlug={ONBOARDING_SUBPATHS.FUTURE_WORK}
className="mt-6"
/>
</div>
</div>
)
}

View File

@ -1,93 +0,0 @@
import { bracketWidthConstantLine } from '@src/lib/exampleKcl'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
import {
OnboardingButtons,
kbdClasses,
useDemoCode,
} from '@src/routes/Onboarding/utils'
export default function OnboardingInteractiveNumbers() {
useDemoCode()
return (
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div
className={
'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1 overflow-y-auto mb-6">
<h2 className="text-3xl font-bold">Hybrid editing</h2>
<p className="my-4">
We believe editing in Design Studio should feel fluid between code
and point-and-click, so that you can work in the way that feels most
natural to you. Let's try something out that demonstrates this
principle, by editing numbers without typing.
</p>
<ol className="pl-6 my-4 list-decimal">
<li className="list-decimal">
Press and hold the <kbd className={kbdClasses}>Alt</kbd> (or{' '}
<kbd className={kbdClasses}>Option</kbd>) key
</li>
<li>
Hover over the number assigned to "width" on{' '}
<em>
<strong>line {bracketWidthConstantLine}</strong>
</em>
</li>
<li>Drag the number left and right to change its value</li>
</ol>
<p className="my-4">
You can hold down different modifier keys to change the value by
different increments:
</p>
<ul className="flex flex-col text-sm my-4 mx-12 divide-y divide-chalkboard-20 dark:divide-chalkboard-70">
<li className="flex justify-between m-0 px-0 py-2">
<kbd className={kbdClasses}>Alt + Shift + Cmd/Win</kbd>
±0.01
</li>
<li className="flex justify-between m-0 px-0 py-2">
<kbd className={kbdClasses}>Alt + Cmd/Win</kbd>
±0.1
</li>
<li className="flex justify-between m-0 px-0 py-2">
<kbd className={kbdClasses}>Alt</kbd>±1
</li>
<li className="flex justify-between m-0 px-0 py-2">
<kbd className={kbdClasses}>Alt + Shift</kbd>
±10
</li>
</ul>
<p className="my-4">
Our code editor is built with{' '}
<a
href="https://codemirror.net/"
target="_blank"
rel="noreferrer noopeneer"
>
CodeMirror
</a>
, a great open-source project with extensions that make it even more
dynamic and interactive, including{' '}
<a
href="https://github.com/replit/codemirror-interact/"
target="_blank"
rel="noreferrer noopeneer"
>
one by the Replit team
</a>{' '}
lets you interact with numbers in your code by dragging them around.
</p>
<p className="my-4">
We're going to keep extending the text editor, and we'd love to hear
your ideas for how to make it better.
</p>
</section>
<OnboardingButtons
currentSlug={ONBOARDING_SUBPATHS.INTERACTIVE_NUMBERS}
/>
</div>
</div>
)
}

View File

@ -1,78 +0,0 @@
import { APP_NAME } from '@src/lib/constants'
import { isDesktop } from '@src/lib/isDesktop'
import { Themes, getSystemTheme } from '@src/lib/theme'
import { useSettings } from '@src/lib/singletons'
import { OnboardingButtons, useDemoCode } from '@src/routes/Onboarding/utils'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
export default function Introduction() {
// Reset the code to the bracket code
useDemoCode()
const {
app: { theme },
} = useSettings()
const getLogoTheme = () =>
theme.current === Themes.Light ||
(theme.current === Themes.System && getSystemTheme() === Themes.Light)
? '-dark'
: ''
return (
<div className="fixed inset-0 z-50 grid place-content-center bg-chalkboard-110/50">
<div className="relative max-w-3xl p-8 rounded bg-chalkboard-10 dark:bg-chalkboard-90">
<h1 className="flex flex-wrap items-center gap-4 text-3xl font-bold">
<img
src={`${isDesktop() ? '.' : ''}/zma-logomark${getLogoTheme()}.svg`}
alt={APP_NAME}
className="h-20 max-w-full"
/>
<span className="px-3 py-1 text-base rounded-full bg-primary/10 text-primary">
Alpha
</span>
</h1>
<section className="my-12">
<p className="my-4">
Welcome to {APP_NAME}! This is a hardware design tool that lets you
edit visually, with code, or both. It's powered by the KittyCAD
Design API, the first API created for anyone to build hardware
design tools. The 3D view is not running on your computer, but is
instead being streamed to you from an instance of our Geometry
Engine on a remote GPU as video.
</p>
<p className="my-4">
This is an alpha release, so you will encounter bugs and missing
features. You can read our{' '}
<a
href="https://gist.github.com/jgomez720/5cd53fb7e8e54079f6dc0d2625de5393"
target="_blank"
rel="noreferrer noopener"
>
expectations for alpha users here
</a>
, and please give us feedback on your experience{' '}
<a
href="https://discord.com/invite/JQEpHR7Nt2"
target="_blank"
rel="noreferrer noopener"
>
our Discord
</a>
! We are trying to release as early as possible to get feedback from
users like you.
</p>
<p>
As you go through the onboarding, we'll be changing and resetting
your code occasionally, so that we can reference specific code
features. So hold off on writing production KCL code until you're
done with the onboarding 😉
</p>
</section>
<OnboardingButtons
currentSlug={ONBOARDING_SUBPATHS.INDEX}
className="mt-6"
/>
</div>
</div>
)
}

View File

@ -1,80 +0,0 @@
import { bracketThicknessCalculationLine } from '@src/lib/exampleKcl'
import { isDesktop } from '@src/lib/isDesktop'
import { Themes, getSystemTheme } from '@src/lib/theme'
import { useSettings } from '@src/lib/singletons'
import { OnboardingButtons, useDemoCode } from '@src/routes/Onboarding/utils'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
export default function OnboardingParametricModeling() {
useDemoCode()
const {
app: {
theme: { current: theme },
},
} = useSettings()
const getImageTheme = () =>
theme === Themes.Light ||
(theme === Themes.System && getSystemTheme() === Themes.Light)
? '-dark'
: ''
return (
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div
className={
'relative pointer-events-auto z-10 max-w-xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg h-[75vh] flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1 overflow-y-auto mb-6">
<h2 className="text-3xl font-bold">Parametric modeling with kcl</h2>
<p className="my-4">
This example script shows how a code representation of your design
makes easy work of tedious tasks in traditional CAD software, such
as calculating a safety factor.
</p>
<p className="my-4">
We've received this sketch from a designer highlighting an{' '}
<em>
<strong>aluminum bracket</strong>
</em>{' '}
they need for this shelf:
</p>
<figure className="my-4 w-2/3 mx-auto">
<img
src={`${
isDesktop() ? '.' : ''
}/onboarding-bracket${getImageTheme()}.png`}
alt="Bracket"
/>
<figcaption className="text-small italic text-center">
A simplified shelf bracket
</figcaption>
</figure>
<p className="my-4">
We are able to easily calculate the thickness of the material based
on the width of the bracket to meet a set safety factor on{' '}
<em>
<strong>line {bracketThicknessCalculationLine}</strong>
</em>
.
</p>
<figure className="my-4 w-2/3 mx-auto">
<img
src={`${
isDesktop() ? '.' : ''
}/onboarding-bracket-dimensions${getImageTheme()}.png`}
alt="Bracket Dimensions"
/>
<figcaption className="text-small italic text-center">
Bracket Dimensions
</figcaption>
</figure>
</section>
<OnboardingButtons
currentSlug={ONBOARDING_SUBPATHS.PARAMETRIC_MODELING}
/>
</div>
</div>
)
}

View File

@ -1,62 +0,0 @@
import { isDesktop } from '@src/lib/isDesktop'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
export default function ProjectMenu() {
const onDesktop = isDesktop()
return (
<div className="fixed grid justify-center items-start inset-0 z-50 pointer-events-none">
<div
className={
'relative pointer-events-auto max-w-xl flex flex-col border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<section className="flex-1">
<h2 className="text-2xl font-bold">Project Menu</h2>
<p className="my-4">
Click on {onDesktop ? `your part's name` : `the app name`} in the
upper left to open the project menu, where you can open the project
settings and export your current part.
{onDesktop && (
<> You can click the Zoo logo to quickly navigate home.</>
)}
</p>
{onDesktop ? (
<>
<p className="my-4">
From here you can manage files in your project and export your
current part. Your projects are{' '}
<strong>all saved locally</strong> as a folder on your device.
You can configure where projects are saved in the settings.
</p>
<p className="my-4">
We are working to support assemblies as separate kcl files
importing parts from each other, but for now you can only open
and export individual parts.
</p>
</>
) : (
<>
<p className="my-4">
You can't manage separate files and separate projects from the
browser; you have to{' '}
<a
href="https://zoo.dev/modeling-app/download"
target="_blank"
rel="noreferrer noopener"
>
download the desktop app
</a>{' '}
for that. We aren't hosting files for you at this time but are
considering supporting it in the future, so we're building
Design Studio with a browser-first experience in mind.
</p>
</>
)}
</section>
<OnboardingButtons currentSlug={ONBOARDING_SUBPATHS.PROJECT_MENU} />
</div>
</div>
)
}

View File

@ -1,49 +0,0 @@
import { useEffect } from 'react'
import { codeManager, kclManager } from '@src/lib/singletons'
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
export default function Sketching() {
useEffect(() => {
async function clearEditor() {
// We do want to update both the state and editor here.
codeManager.updateCodeStateEditor('')
await kclManager.executeCode()
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
clearEditor()
}, [])
return (
<div className="fixed grid justify-center items-end inset-0 z-50 pointer-events-none">
<div
className={
'relative pointer-events-auto max-w-full xl:max-w-2xl border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg flex flex-col justify-center bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded'
}
>
<h1 className="text-2xl font-bold">Sketching</h1>
<p className="my-4">
Our 3D modeling tools are still very much a work in progress, but we
want to show you some early features. Try sketching by clicking Start
Sketch in the top toolbar and selecting a plane to draw on. Now you
can start clicking to draw lines and shapes.
</p>
<p className="my-4">
The Line tool will be equipped by default, but you can switch it to as
you go by clicking another tool in the toolbar, or unequip it by
clicking the Line tool button. With no tool selected, you can move
points and add constraints to your sketch.
</p>
<p className="my-4">
Watch the code pane as you click. Point-and-click interactions are
always just modifying and generating code in Zoo Design Studio.
</p>
<OnboardingButtons
currentSlug={ONBOARDING_SUBPATHS.SKETCHING}
className="mt-6"
/>
</div>
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More