Compare commits
13 Commits
kcl-68
...
franknoiro
Author | SHA1 | Date | |
---|---|---|---|
7d6427ab64 | |||
4abbe0d57a | |||
a631ff689f | |||
e1d401adfe | |||
6f49c88382 | |||
374d07b995 | |||
3481252082 | |||
035f3b6aed | |||
923feadfa5 | |||
1ea66d6f23 | |||
3b7b4f85a1 | |||
9853353512 | |||
7b8585f3c3 |
@ -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
|
||||
|
1
.github/workflows/e2e-tests.yml
vendored
@ -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 }}
|
||||
|
@ -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:
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
}
|
||||
|
@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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])
|
||||
}
|
||||
|
@ -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 }
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -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),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
@ -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: '' },
|
||||
|
@ -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,
|
||||
|
@ -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
@ -2492,7 +2492,6 @@
|
||||
},
|
||||
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"extraneous": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
@ -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) {
|
||||
|
@ -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() {
|
||||
|
@ -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 {
|
||||
|
@ -62,6 +62,7 @@ pub struct KwArgs {
|
||||
pub unlabeled: Option<Arg>,
|
||||
/// Labeled args.
|
||||
pub labeled: IndexMap<String, Arg>,
|
||||
pub errors: Vec<Arg>,
|
||||
}
|
||||
|
||||
impl KwArgs {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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": [],
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
@ -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])
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 74 KiB |
@ -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])
|
||||
|
16
src/App.tsx
@ -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}>
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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()}
|
||||
|
@ -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 || '')
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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])
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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') {
|
||||
|
@ -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'
|
||||
)
|
||||
|
@ -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} `
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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")
|
||||
`
|
||||
|
@ -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' &&
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
@ -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}`
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
|
@ -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 || '',
|
||||
}
|
||||
}
|
||||
),
|
||||
},
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
|
498
src/routes/Onboarding/BrowserOnboardingRoutes.tsx
Normal 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">
|
||||
Let’s 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">
|
||||
Let’s 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, you’ll see
|
||||
“Modify with Zoo Text-to-CAD”. Once clicked, you’ll 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],
|
||||
})),
|
||||
]
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
634
src/routes/Onboarding/DesktopOnboardingRoutes.tsx
Normal 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">
|
||||
Let’s 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">
|
||||
Let’s 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, you’ll see
|
||||
“Modify with Zoo Text-to-CAD”. Once clicked, you’ll 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],
|
||||
})),
|
||||
]
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|