Update onboarding to V1 browser and desktop flows (#6714)
* Remove unused `telemetryLoader` * Remove onboarding redirect behavior * Allow subRoute to be passed to navigateToProject * Replace warning dialog routes with toasts * Wire up new utilities and toasts to UI components * Add home sidebar buttons for tutorial flow * Rename menu item * Add flex-1 so home-layout fills available space * Remove onboarding avatar tests, they are becoming irrelevant * Consolidate onboarding tests to one longer one and update it to not use pixel color checks, and use fixtures. * Shorten warning toast button text * tsc, lint, and circular deps * Update circular dep file * Fix mistakes made in circular update tweaking * One more dumb created circular dep * Update src/routes/Onboarding/utils.tsx Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> * Fix narrow screen home layout breaking * fix: kevin, navigation routes fixed * fix: filename parsing is correct now for onboarding with the last file sep * Fix e2e test state checks that are diff on Linux * Create onboarding project entirely through systemIOMachine * Fix Windows path construction * Make utility to verify a string is an onboarding value * Little biome formatting suggestion fix * Units onboarding step was not using OnboardingButtons * Add type checking of next and previous status, fix useNextClick * Update `OnboardingStatus` type on WASM side * Make onboarding different on browser and web, placeholder component * Show proof of concept with custom content per route * Make text type args not insta dismiss when you click anywhere * Make some utility hooks for the onboarding * Update requestedProjectName along with requestedProjectName * Build out a rough draft of desktop onboarding * Remove unused onboarding route files * Build out rough draft of browser onboarding content * @jgomez720 browser flow feedback * @jgomez420 desktop feedback * tsc and lints * Tweaks * Import is dead, long live Add files * What's up with my inability to type "highlight"? * Codespell and String casting * Update browser sample to be axial fan * lint and tsc * codespell again * Remove unused nightmare function `useDemoCode` * Add a few unit tests * Update desktop to use bulk file creation from #6747 * Oops overwrote main.kcl on the modify with text-to-cad step * Undo the dumb use of `sep` that I introduced * Fix up project test which was fragile to the number of steps in the onboarding smh * Fix up onboarding flow test * typo --------- Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> Co-authored-by: Kevin Nadro <kevin@zoo.dev>
This commit is contained in:
@ -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')
|
||||
@ -90,9 +89,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 })
|
||||
})
|
||||
@ -122,7 +120,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,45 +129,70 @@ 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 helpMenuButton.click()
|
||||
await helpMenuRestartOnboardingButton.click()
|
||||
await homePage.expectState({
|
||||
projectCards: [
|
||||
{ title: 'tutorial-project', fileCount: 7 },
|
||||
{
|
||||
title: 'testDefault',
|
||||
fileCount: 1,
|
||||
},
|
||||
],
|
||||
sortBy: 'last-modified-desc',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
await test.step('Makes a new project', async () => {
|
||||
await expect(toolbar.projectName).toContainText('Tutorial Project 02')
|
||||
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 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,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: '' },
|
||||
|
@ -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 {
|
||||
|
16
src/App.tsx
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"
|
||||
>
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
@ -194,3 +194,8 @@ 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.'
|
||||
/**
|
||||
* 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")
|
||||
`
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
498
src/routes/Onboarding/BrowserOnboardingRoutes.tsx
Normal file
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="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="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="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="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="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="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="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="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
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="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="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="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="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="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="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="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="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="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="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="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="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="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="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>
|
||||
)
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
|
||||
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
|
||||
|
||||
export default function Streaming() {
|
||||
return (
|
||||
<div className="fixed grid justify-start items-center inset-0 z-50 pointer-events-none">
|
||||
<div
|
||||
className={
|
||||
'relative pointer-events-auto 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">Streaming Video</h2>
|
||||
<p className="my-4">
|
||||
Historically, CAD programs run on your computer, so to run
|
||||
performance-heavy apps you have to have a powerful, expensive
|
||||
desktop. But the 3D scene you see here is not running on your
|
||||
computer.
|
||||
</p>
|
||||
<p className="my-4">
|
||||
Instead, our infrastructure spins up our Geometry Engine on a remote
|
||||
GPU, Design Studio sends it a series of commands{' '}
|
||||
<a
|
||||
href="https://zoo.dev/blog/cad-webrtc"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
via Websockets and WebRTC
|
||||
</a>
|
||||
, and the Geometry Engine sends back a video stream of the 3D view.
|
||||
</p>
|
||||
<p className="my-4">
|
||||
This means that you could run our Design Studio on nearly any device
|
||||
with a good internet connection.
|
||||
</p>
|
||||
<p className="my-4">
|
||||
It also means that whatever tools you build on top of our Geometry
|
||||
Engine will be able to run on any device with a browser, and you
|
||||
won't have to worry about the performance of the device.
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons
|
||||
currentSlug={ONBOARDING_SUBPATHS.STREAMING}
|
||||
dismissClassName="right-auto left-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import { SettingsSection } from '@src/components/Settings/SettingsSection'
|
||||
import { type BaseUnit, baseUnitsUnion } from '@src/lib/settings/settingsTypes'
|
||||
import { settingsActor, useSettings } from '@src/lib/singletons'
|
||||
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
|
||||
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
|
||||
|
||||
export default function Units() {
|
||||
const {
|
||||
modeling: { defaultUnit },
|
||||
} = useSettings()
|
||||
|
||||
return (
|
||||
<div className="fixed grid place-content-center inset-0 bg-chalkboard-110/50 z-50">
|
||||
<div className="max-w-3xl bg-chalkboard-10 dark:bg-chalkboard-90 p-8 rounded">
|
||||
<h1 className="text-2xl font-bold">Set your units</h1>
|
||||
<SettingsSection
|
||||
title="Default Unit"
|
||||
description="Which unit to use in modeling dimensions by default"
|
||||
>
|
||||
<select
|
||||
id="base-unit"
|
||||
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
||||
value={defaultUnit.user}
|
||||
onChange={(e) => {
|
||||
settingsActor.send({
|
||||
type: 'set.modeling.defaultUnit',
|
||||
data: {
|
||||
level: 'user',
|
||||
value: e.target.value as BaseUnit,
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
{baseUnitsUnion.map((unit) => (
|
||||
<option key={unit} value={unit}>
|
||||
{unit}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</SettingsSection>
|
||||
<OnboardingButtons
|
||||
currentSlug={ONBOARDING_SUBPATHS.UNITS}
|
||||
className="mt-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useUser } from '@src/lib/singletons'
|
||||
import { OnboardingButtons } from '@src/routes/Onboarding/utils'
|
||||
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
|
||||
|
||||
export default function UserMenu() {
|
||||
const user = useUser()
|
||||
const [avatarErrored, setAvatarErrored] = useState(false)
|
||||
|
||||
const errorOrNoImage = !user?.image || avatarErrored
|
||||
const buttonDescription = errorOrNoImage ? 'the menu button' : 'your avatar'
|
||||
|
||||
// Set up error handling for the user's avatar image,
|
||||
// so the onboarding text can be updated if it fails to load.
|
||||
useEffect(() => {
|
||||
const element = globalThis.document.querySelector(
|
||||
'[data-testid="user-sidebar-toggle"] img'
|
||||
)
|
||||
|
||||
const onError = () => setAvatarErrored(true)
|
||||
if (element?.tagName === 'IMG') {
|
||||
element?.addEventListener('error', onError)
|
||||
}
|
||||
return () => {
|
||||
element?.removeEventListener('error', onError)
|
||||
}
|
||||
}, [])
|
||||
|
||||
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">User Menu</h2>
|
||||
<p className="my-4">
|
||||
Click {buttonDescription} in the upper right to open the user menu.
|
||||
You can change your user-level settings, sign out, report a bug,
|
||||
manage your account, request a feature, and more.
|
||||
</p>
|
||||
<p className="my-4">
|
||||
Many settings can be set either a user or per-project level. User
|
||||
settings will apply to all projects, while project settings will
|
||||
only apply to the current project.
|
||||
</p>
|
||||
</section>
|
||||
<OnboardingButtons currentSlug={ONBOARDING_SUBPATHS.USER_MENU} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,83 +1,30 @@
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
|
||||
import makeUrlPathRelative from '@src/lib/makeUrlPathRelative'
|
||||
import Camera from '@src/routes/Onboarding/Camera'
|
||||
import CmdK from '@src/routes/Onboarding/CmdK'
|
||||
import CodeEditor from '@src/routes/Onboarding/CodeEditor'
|
||||
import Export from '@src/routes/Onboarding/Export'
|
||||
import FutureWork from '@src/routes/Onboarding/FutureWork'
|
||||
import InteractiveNumbers from '@src/routes/Onboarding/InteractiveNumbers'
|
||||
import Introduction from '@src/routes/Onboarding/Introduction'
|
||||
import ParametricModeling from '@src/routes/Onboarding/ParametricModeling'
|
||||
import ProjectMenu from '@src/routes/Onboarding/ProjectMenu'
|
||||
import Sketching from '@src/routes/Onboarding/Sketching'
|
||||
import Streaming from '@src/routes/Onboarding/Streaming'
|
||||
import UserMenu from '@src/routes/Onboarding/UserMenu'
|
||||
import { useDismiss } from '@src/routes/Onboarding/utils'
|
||||
import { ONBOARDING_SUBPATHS } from '@src/lib/onboardingPaths'
|
||||
import { browserOnboardingRoutes } from '@src/routes/Onboarding/BrowserOnboardingRoutes'
|
||||
import { desktopOnboardingRoutes } from '@src/routes/Onboarding/DesktopOnboardingRoutes'
|
||||
import makeUrlPathRelative from '@src/lib/makeUrlPathRelative'
|
||||
|
||||
/** Compile the onboarding routes into one object
|
||||
* for use in the Router.
|
||||
*/
|
||||
export const onboardingRoutes = [
|
||||
{
|
||||
index: true,
|
||||
element: <Introduction />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.CAMERA),
|
||||
element: <Camera />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.STREAMING),
|
||||
element: <Streaming />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.EDITOR),
|
||||
element: <CodeEditor />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.PARAMETRIC_MODELING),
|
||||
element: <ParametricModeling />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.INTERACTIVE_NUMBERS),
|
||||
element: <InteractiveNumbers />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.COMMAND_K),
|
||||
element: <CmdK />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.USER_MENU),
|
||||
element: <UserMenu />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.PROJECT_MENU),
|
||||
element: <ProjectMenu />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.EXPORT),
|
||||
element: <Export />,
|
||||
},
|
||||
// Export / conversion API
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.SKETCHING),
|
||||
element: <Sketching />,
|
||||
},
|
||||
{
|
||||
path: makeUrlPathRelative(ONBOARDING_SUBPATHS.FUTURE_WORK),
|
||||
element: <FutureWork />,
|
||||
},
|
||||
]
|
||||
...browserOnboardingRoutes,
|
||||
...desktopOnboardingRoutes,
|
||||
].map(({ path, ...route }) => ({
|
||||
// react-router-dom wants these path to be relative in Router.tsx
|
||||
path: makeUrlPathRelative(path),
|
||||
...route,
|
||||
}))
|
||||
|
||||
const Onboarding = () => {
|
||||
export const OnboardingRootRoute = () => {
|
||||
const dismiss = useDismiss()
|
||||
useHotkeys('esc', () => dismiss())
|
||||
|
||||
return (
|
||||
<div className="content" data-testid="onboarding-content">
|
||||
{/* Outlet is a magic react-router-dom element that hot-swaps child route content */}
|
||||
<Outlet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Onboarding
|
||||
|
109
src/routes/Onboarding/utils.test.ts
Normal file
109
src/routes/Onboarding/utils.test.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import type { OnboardingStatus } from '@rust/kcl-lib/bindings/OnboardingStatus'
|
||||
import type { OnboardingPath } from '@src/lib/onboardingPaths'
|
||||
import {
|
||||
needsToOnboard,
|
||||
useAdjacentOnboardingSteps,
|
||||
} from '@src/routes/Onboarding/utils'
|
||||
import type { Location } from 'react-router-dom'
|
||||
|
||||
describe('Onboarding utility functions', () => {
|
||||
describe('useAdjacentOnboardingSteps', () => {
|
||||
it('Desktop beginning', () => {
|
||||
const stepResults = useAdjacentOnboardingSteps('/desktop', 'desktop')
|
||||
const expected: OnboardingStatus[] = ['dismissed', '/desktop/scene']
|
||||
expect(stepResults).toEqual(expected)
|
||||
})
|
||||
it('Desktop middle', () => {
|
||||
const stepResults = useAdjacentOnboardingSteps(
|
||||
'/desktop/other-panes',
|
||||
'desktop'
|
||||
)
|
||||
const expected: OnboardingStatus[] = [
|
||||
'/desktop/project-pane',
|
||||
'/desktop/prompt-to-edit',
|
||||
]
|
||||
expect(stepResults).toEqual(expected)
|
||||
})
|
||||
it('Desktop end', () => {
|
||||
const stepResults = useAdjacentOnboardingSteps(
|
||||
'/desktop/conclusion',
|
||||
'desktop'
|
||||
)
|
||||
const expected: OnboardingStatus[] = ['/desktop/exports', 'completed']
|
||||
expect(stepResults).toEqual(expected)
|
||||
})
|
||||
it('Browser beginning', () => {
|
||||
const stepResults = useAdjacentOnboardingSteps('/browser', 'browser')
|
||||
const expected: OnboardingStatus[] = ['dismissed', '/browser/scene']
|
||||
expect(stepResults).toEqual(expected)
|
||||
})
|
||||
it('Browser middle', () => {
|
||||
const stepResults = useAdjacentOnboardingSteps(
|
||||
'/browser/text-to-cad-prompt',
|
||||
'browser'
|
||||
)
|
||||
const expected: OnboardingStatus[] = [
|
||||
'/browser/text-to-cad',
|
||||
'/browser/feature-tree-pane',
|
||||
]
|
||||
expect(stepResults).toEqual(expected)
|
||||
})
|
||||
it('Browser end', () => {
|
||||
const stepResults = useAdjacentOnboardingSteps(
|
||||
'/browser/conclusion',
|
||||
'browser'
|
||||
)
|
||||
const expected: OnboardingStatus[] = [
|
||||
'/browser/prompt-to-edit-result',
|
||||
'completed',
|
||||
]
|
||||
expect(stepResults).toEqual(expected)
|
||||
})
|
||||
it('Errors gracefully', () => {
|
||||
const stepResults = useAdjacentOnboardingSteps(
|
||||
'/bad-path' as unknown as OnboardingPath,
|
||||
'desktop'
|
||||
)
|
||||
const expected: OnboardingStatus[] = ['dismissed', 'completed']
|
||||
expect(stepResults).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('needsToOnboard', () => {
|
||||
it('in onboarding already does not need onboarding', () => {
|
||||
const location: Location = {
|
||||
pathname: '/some-file/onboarding/some-step',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
key: 'default',
|
||||
}
|
||||
expect(needsToOnboard(location, '')).toEqual(false)
|
||||
})
|
||||
it('elsewhere with bad status does need onboarding', () => {
|
||||
const location: Location = {
|
||||
pathname: '/somewhere-else',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
key: 'default',
|
||||
}
|
||||
expect(
|
||||
needsToOnboard(
|
||||
location,
|
||||
'/bad-onboarding-status' as unknown as OnboardingStatus
|
||||
)
|
||||
).toEqual(true)
|
||||
})
|
||||
it('elsewhere with completed does not need onboarding', () => {
|
||||
const location: Location = {
|
||||
pathname: '/somewhere-else',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
key: 'default',
|
||||
}
|
||||
expect(needsToOnboard(location, 'completed')).toEqual(false)
|
||||
})
|
||||
})
|
||||
})
|
@ -1,34 +1,24 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
type NavigateFunction,
|
||||
type useLocation,
|
||||
useNavigate,
|
||||
} from 'react-router-dom'
|
||||
import { waitFor } from 'xstate'
|
||||
import { type SnapshotFrom, waitFor } from 'xstate'
|
||||
|
||||
import { ActionButton } from '@src/components/ActionButton'
|
||||
import { CustomIcon } from '@src/components/CustomIcon'
|
||||
import Tooltip from '@src/components/Tooltip'
|
||||
import { useAbsoluteFilePath } from '@src/hooks/useAbsoluteFilePath'
|
||||
import { useNetworkContext } from '@src/hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from '@src/hooks/useNetworkStatus'
|
||||
import { EngineConnectionStateType } from '@src/lang/std/engineConnection'
|
||||
import { bracket } from '@src/lib/exampleKcl'
|
||||
import { browserAxialFan, fanParts } from '@src/lib/exampleKcl'
|
||||
import makeUrlPathRelative from '@src/lib/makeUrlPathRelative'
|
||||
import { joinRouterPaths, PATHS } from '@src/lib/paths'
|
||||
import {
|
||||
codeManager,
|
||||
editorManager,
|
||||
kclManager,
|
||||
systemIOActor,
|
||||
} from '@src/lib/singletons'
|
||||
import { err, reportRejection, trap } from '@src/lib/trap'
|
||||
import { commandBarActor, systemIOActor } from '@src/lib/singletons'
|
||||
import { err, reportRejection } from '@src/lib/trap'
|
||||
import { settingsActor } from '@src/lib/singletons'
|
||||
import { isKclEmptyOrOnlySettings, parse, resultIsOk } from '@src/lang/wasm'
|
||||
import { updateModelingState } from '@src/lang/modelingWorkflows'
|
||||
import { isKclEmptyOrOnlySettings } from '@src/lang/wasm'
|
||||
import {
|
||||
DEFAULT_PROJECT_KCL_FILE,
|
||||
EXECUTION_TYPE_REAL,
|
||||
ONBOARDING_DATA_ATTRIBUTE,
|
||||
ONBOARDING_PROJECT_NAME,
|
||||
} from '@src/lib/constants'
|
||||
import toast from 'react-hot-toast'
|
||||
@ -39,59 +29,44 @@ import type { KclManager } from '@src/lang/KclSingleton'
|
||||
import { Logo } from '@src/components/Logo'
|
||||
import { SystemIOMachineEvents } from '@src/machines/systemIO/utils'
|
||||
import {
|
||||
isOnboardingSubPath,
|
||||
ONBOARDING_SUBPATHS,
|
||||
isOnboardingPath,
|
||||
type OnboardingPath,
|
||||
onboardingPaths,
|
||||
onboardingStartPath,
|
||||
} from '@src/lib/onboardingPaths'
|
||||
import { useModelingContext } from '@src/hooks/useModelingContext'
|
||||
import type { SidebarType } from '@src/components/ModelingSidebar/ModelingPanes'
|
||||
|
||||
export const kbdClasses =
|
||||
'py-0.5 px-1 text-sm rounded bg-chalkboard-10 dark:bg-chalkboard-100 border border-chalkboard-50 border-b-2'
|
||||
|
||||
// Get the 1-indexed step number of the current onboarding step
|
||||
function useStepNumber(
|
||||
slug?: (typeof ONBOARDING_SUBPATHS)[keyof typeof ONBOARDING_SUBPATHS]
|
||||
function getStepNumber(
|
||||
slug?: OnboardingPath,
|
||||
platform: keyof typeof onboardingPaths = 'browser'
|
||||
) {
|
||||
return slug ? Object.values(ONBOARDING_SUBPATHS).indexOf(slug) + 1 : -1
|
||||
return slug ? Object.values(onboardingPaths[platform]).indexOf(slug) + 1 : -1
|
||||
}
|
||||
|
||||
export function useDemoCode() {
|
||||
const { overallState, immediateState } = useNetworkContext()
|
||||
|
||||
useEffect(() => {
|
||||
async function setCodeToDemoIfNeeded() {
|
||||
// Don't run if the editor isn't loaded or the code is already the bracket
|
||||
if (!editorManager.editorView || codeManager.code === bracket) {
|
||||
return
|
||||
}
|
||||
// Don't run if the network isn't healthy or the connection isn't established
|
||||
if (
|
||||
overallState === NetworkHealthState.Disconnected ||
|
||||
overallState === NetworkHealthState.Issue ||
|
||||
immediateState.type !== EngineConnectionStateType.ConnectionEstablished
|
||||
) {
|
||||
return
|
||||
}
|
||||
const pResult = parse(bracket)
|
||||
if (trap(pResult) || !resultIsOk(pResult)) {
|
||||
return Promise.reject(pResult)
|
||||
}
|
||||
const ast = pResult.program
|
||||
await updateModelingState(ast, EXECUTION_TYPE_REAL, {
|
||||
kclManager: kclManager,
|
||||
editorManager: editorManager,
|
||||
codeManager: codeManager,
|
||||
})
|
||||
}
|
||||
|
||||
setCodeToDemoIfNeeded().catch(reportRejection)
|
||||
}, [editorManager.editorView, immediateState.type, overallState])
|
||||
}
|
||||
export const OnboardingCard = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={`relative max-w-3xl min-w-80 bg-chalkboard-10 dark:bg-chalkboard-90 py-6 px-8 rounded border border-chalkboard-50 dark:border-chalkboard-80 shadow-lg ${className || ''}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export function useNextClick(newStatus: OnboardingStatus) {
|
||||
const filePath = useAbsoluteFilePath()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return useCallback(() => {
|
||||
if (!isOnboardingSubPath(newStatus)) {
|
||||
if (!isOnboardingPath(newStatus)) {
|
||||
return new Error(
|
||||
`Failed to navigate to invalid onboarding status ${newStatus}`
|
||||
)
|
||||
@ -100,7 +75,8 @@ export function useNextClick(newStatus: OnboardingStatus) {
|
||||
type: 'set.app.onboardingStatus',
|
||||
data: { level: 'user', value: newStatus },
|
||||
})
|
||||
navigate(joinRouterPaths(filePath, PATHS.ONBOARDING.INDEX, newStatus))
|
||||
const targetRoute = joinRouterPaths(filePath, PATHS.ONBOARDING, newStatus)
|
||||
navigate(targetRoute)
|
||||
}, [filePath, newStatus, navigate])
|
||||
}
|
||||
|
||||
@ -137,43 +113,74 @@ export function useDismiss() {
|
||||
return settingsCallback
|
||||
}
|
||||
|
||||
export function OnboardingButtons({
|
||||
currentSlug,
|
||||
className,
|
||||
dismissClassName,
|
||||
onNextOverride,
|
||||
...props
|
||||
}: {
|
||||
currentSlug?: (typeof ONBOARDING_SUBPATHS)[keyof typeof ONBOARDING_SUBPATHS]
|
||||
className?: string
|
||||
dismissClassName?: string
|
||||
onNextOverride?: () => void
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const onboardingPathsArray = Object.values(ONBOARDING_SUBPATHS)
|
||||
const dismiss = useDismiss()
|
||||
const stepNumber = useStepNumber(currentSlug)
|
||||
export function useAdjacentOnboardingSteps(
|
||||
currentSlug?: OnboardingPath,
|
||||
platform: undefined | keyof typeof onboardingPaths = 'browser'
|
||||
) {
|
||||
const onboardingPathsArray = Object.values(onboardingPaths[platform])
|
||||
const stepNumber = getStepNumber(currentSlug, platform)
|
||||
const previousStep =
|
||||
!stepNumber || stepNumber <= 1 ? null : onboardingPathsArray[stepNumber]
|
||||
!stepNumber || stepNumber <= 1 ? null : onboardingPathsArray[stepNumber - 2]
|
||||
const nextStep =
|
||||
!stepNumber || stepNumber === onboardingPathsArray.length
|
||||
? null
|
||||
: onboardingPathsArray[stepNumber]
|
||||
|
||||
const previousOnboardingStatus: OnboardingStatus =
|
||||
previousStep ?? ONBOARDING_SUBPATHS.INDEX
|
||||
const previousOnboardingStatus: OnboardingStatus = previousStep ?? 'dismissed'
|
||||
const nextOnboardingStatus: OnboardingStatus = nextStep ?? 'completed'
|
||||
|
||||
return [previousOnboardingStatus, nextOnboardingStatus]
|
||||
}
|
||||
|
||||
export function useOnboardingClicks(
|
||||
currentSlug?: OnboardingPath,
|
||||
platform: undefined | keyof typeof onboardingPaths = 'browser'
|
||||
) {
|
||||
const [previousOnboardingStatus, nextOnboardingStatus] =
|
||||
useAdjacentOnboardingSteps(currentSlug, platform)
|
||||
const goToPrevious = useNextClick(previousOnboardingStatus)
|
||||
const goToNext = useNextClick(nextOnboardingStatus)
|
||||
|
||||
return [goToPrevious, goToNext]
|
||||
}
|
||||
|
||||
export function OnboardingButtons({
|
||||
currentSlug,
|
||||
platform = 'browser',
|
||||
dismissPosition = 'left',
|
||||
className,
|
||||
dismissClassName,
|
||||
onNextOverride,
|
||||
...props
|
||||
}: {
|
||||
currentSlug?: OnboardingPath
|
||||
platform?: keyof typeof onboardingPaths
|
||||
dismissPosition?: 'left' | 'right'
|
||||
className?: string
|
||||
dismissClassName?: string
|
||||
onNextOverride?: () => void
|
||||
} & React.HTMLAttributes<HTMLDivElement>) {
|
||||
const dismiss = useDismiss()
|
||||
const onboardingPathsArray = Object.values(onboardingPaths[platform])
|
||||
const stepNumber = getStepNumber(currentSlug, platform)
|
||||
const [previousStep, nextStep] = useAdjacentOnboardingSteps(
|
||||
currentSlug,
|
||||
platform
|
||||
)
|
||||
const [goToPrevious, goToNext] = useOnboardingClicks(currentSlug, platform)
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dismiss()}
|
||||
className={`group block !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent ${
|
||||
className={`group block !absolute top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent ${
|
||||
dismissClassName
|
||||
}`}
|
||||
style={{
|
||||
left: dismissPosition === 'left' ? 'auto' : '100%',
|
||||
right: dismissPosition === 'left' ? '100%' : 'auto',
|
||||
}}
|
||||
data-testid="onboarding-dismiss"
|
||||
>
|
||||
<CustomIcon
|
||||
@ -190,16 +197,25 @@ export function OnboardingButtons({
|
||||
>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
onClick={() => (previousStep ? goToPrevious() : dismiss())}
|
||||
onClick={() =>
|
||||
previousStep && previousStep !== 'dismissed'
|
||||
? goToPrevious()
|
||||
: dismiss()
|
||||
}
|
||||
iconStart={{
|
||||
icon: previousStep ? 'arrowLeft' : 'close',
|
||||
icon:
|
||||
previousStep && previousStep !== 'dismissed'
|
||||
? 'arrowLeft'
|
||||
: 'close',
|
||||
className: 'text-chalkboard-10',
|
||||
bgClassName: 'bg-destroy-80 group-hover:bg-destroy-80',
|
||||
}}
|
||||
className="hover:border-destroy-40 hover:bg-destroy-10/50 dark:hover:bg-destroy-80/50"
|
||||
data-testid="onboarding-prev"
|
||||
id="onboarding-prev"
|
||||
tabIndex={0}
|
||||
>
|
||||
{previousStep ? 'Back' : 'Dismiss'}
|
||||
{previousStep && previousStep !== 'dismissed' ? 'Back' : 'Dismiss'}
|
||||
</ActionButton>
|
||||
{stepNumber !== undefined && (
|
||||
<p className="font-mono text-xs text-center m-0">
|
||||
@ -208,9 +224,10 @@ export function OnboardingButtons({
|
||||
)}
|
||||
<ActionButton
|
||||
autoFocus
|
||||
tabIndex={0}
|
||||
Element="button"
|
||||
onClick={() => {
|
||||
if (nextStep) {
|
||||
if (nextStep && nextStep !== 'completed') {
|
||||
const result = onNextOverride ? onNextOverride() : goToNext()
|
||||
if (err(result)) {
|
||||
reportRejection(result)
|
||||
@ -220,13 +237,15 @@ export function OnboardingButtons({
|
||||
}
|
||||
}}
|
||||
iconStart={{
|
||||
icon: nextStep ? 'arrowRight' : 'checkmark',
|
||||
icon:
|
||||
nextStep && nextStep !== 'completed' ? 'arrowRight' : 'checkmark',
|
||||
bgClassName: 'dark:bg-chalkboard-80',
|
||||
}}
|
||||
className="dark:hover:bg-chalkboard-80/50"
|
||||
data-testid="onboarding-next"
|
||||
id="onboarding-next"
|
||||
>
|
||||
{nextStep ? 'Next' : 'Finish'}
|
||||
{nextStep && nextStep !== 'completed' ? 'Next' : 'Finish'}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</>
|
||||
@ -247,20 +266,28 @@ export const ERROR_MUST_WARN = 'Must warn user before overwrite'
|
||||
* depending on the platform and the state of the user's code.
|
||||
*/
|
||||
export async function acceptOnboarding(deps: OnboardingUtilDeps) {
|
||||
// Non-path statuses should be coerced to the start path
|
||||
const onboardingStatus = !isOnboardingPath(deps.onboardingStatus)
|
||||
? onboardingStartPath
|
||||
: deps.onboardingStatus
|
||||
if (isDesktop()) {
|
||||
/** TODO: rename this event to be more generic, like `createKCLFileAndNavigate` */
|
||||
/**
|
||||
* Bulk create the assembly and navigate to the project
|
||||
*/
|
||||
systemIOActor.send({
|
||||
type: SystemIOMachineEvents.importFileFromURL,
|
||||
type: SystemIOMachineEvents.bulkCreateKCLFilesAndNavigateToProject,
|
||||
data: {
|
||||
files: fanParts.map((part) => ({
|
||||
requestedProjectName: ONBOARDING_PROJECT_NAME,
|
||||
...part,
|
||||
})),
|
||||
// Make a unique tutorial project each time
|
||||
override: true,
|
||||
requestedProjectName: ONBOARDING_PROJECT_NAME,
|
||||
requestedFileNameWithExtension: DEFAULT_PROJECT_KCL_FILE,
|
||||
requestedCode: bracket,
|
||||
requestedSubRoute: joinRouterPaths(
|
||||
PATHS.ONBOARDING.INDEX,
|
||||
deps.onboardingStatus
|
||||
),
|
||||
requestedSubRoute: joinRouterPaths(PATHS.ONBOARDING, onboardingStatus),
|
||||
},
|
||||
})
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
@ -282,20 +309,25 @@ export async function resetCodeAndAdvanceOnboarding({
|
||||
kclManager,
|
||||
navigate,
|
||||
}: OnboardingUtilDeps) {
|
||||
// Non-path statuses should be coerced to the start path
|
||||
const resolvedOnboardingStatus = !isOnboardingPath(onboardingStatus)
|
||||
? onboardingStartPath
|
||||
: onboardingStatus
|
||||
// We do want to update both the state and editor here.
|
||||
codeManager.updateCodeStateEditor(bracket)
|
||||
codeManager.updateCodeStateEditor(browserAxialFan)
|
||||
codeManager.writeToFile().catch(reportRejection)
|
||||
kclManager.executeCode().catch(reportRejection)
|
||||
navigate(
|
||||
makeUrlPathRelative(
|
||||
joinRouterPaths(PATHS.ONBOARDING.INDEX, onboardingStatus)
|
||||
joinRouterPaths(String(PATHS.ONBOARDING), resolvedOnboardingStatus)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function hasResetReadyCode(codeManager: CodeManager) {
|
||||
return (
|
||||
isKclEmptyOrOnlySettings(codeManager.code) || codeManager.code === bracket
|
||||
isKclEmptyOrOnlySettings(codeManager.code) ||
|
||||
codeManager.code === browserAxialFan
|
||||
)
|
||||
}
|
||||
|
||||
@ -304,7 +336,7 @@ export function needsToOnboard(
|
||||
onboardingStatus: OnboardingStatus
|
||||
) {
|
||||
return (
|
||||
!location.pathname.includes(PATHS.ONBOARDING.INDEX) &&
|
||||
!location.pathname.includes(String(PATHS.ONBOARDING)) &&
|
||||
(onboardingStatus.length === 0 ||
|
||||
!(onboardingStatus === 'completed' || onboardingStatus === 'dismissed'))
|
||||
)
|
||||
@ -443,3 +475,124 @@ export function TutorialWebConfirmationToast(props: OnboardingUtilDeps) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the the element with a given `data-onboarding-id` attribute
|
||||
* and highlight it on mount, unhighlighting on unmount.
|
||||
*/
|
||||
export function useOnboardingHighlight(elementId: string) {
|
||||
useEffect(() => {
|
||||
const elementToHighlight = document.querySelector(
|
||||
`[data-${ONBOARDING_DATA_ATTRIBUTE}="${elementId}"`
|
||||
)
|
||||
if (elementToHighlight === null) {
|
||||
console.error('Text-to-CAD dropdown element not found')
|
||||
return
|
||||
}
|
||||
// There is an ".onboarding-highlight" class defined in index.css
|
||||
elementToHighlight?.classList.add('onboarding-highlight')
|
||||
|
||||
// Remove the highlight on unmount
|
||||
return () => {
|
||||
elementToHighlight?.classList.remove('onboarding-highlight')
|
||||
}
|
||||
}, [elementId])
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility hook to set the pane state on mount and unmount.
|
||||
*/
|
||||
export function useOnboardingPanes(
|
||||
onMount: SidebarType[] | undefined = [],
|
||||
onUnmount: SidebarType[] | undefined = []
|
||||
) {
|
||||
const { send } = useModelingContext()
|
||||
useEffect(() => {
|
||||
send({
|
||||
type: 'Set context',
|
||||
data: {
|
||||
openPanes: onMount,
|
||||
},
|
||||
})
|
||||
|
||||
return () =>
|
||||
send({
|
||||
type: 'Set context',
|
||||
data: {
|
||||
openPanes: onUnmount,
|
||||
},
|
||||
})
|
||||
}, [send])
|
||||
}
|
||||
|
||||
export function isModelingCmdGroupReady(
|
||||
state: SnapshotFrom<typeof commandBarActor>
|
||||
) {
|
||||
// Ensure that the modeling command group is available
|
||||
if (
|
||||
state.context.commands.some((command) => command.groupId === 'modeling')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility onboarding hook to wait for the engine connection to be established
|
||||
*/
|
||||
export function useOnModelingCmdGroupReadyOnce(
|
||||
callback: () => void,
|
||||
deps: React.DependencyList
|
||||
) {
|
||||
const [isReadyOnce, setReadyOnce] = useState(false)
|
||||
|
||||
// Set up a subscription to the command bar actor's
|
||||
// modeling command group
|
||||
useEffect(() => {
|
||||
const isReadyNow = isModelingCmdGroupReady(commandBarActor.getSnapshot())
|
||||
if (isReadyNow) {
|
||||
setReadyOnce(true)
|
||||
} else {
|
||||
const subscription = commandBarActor.subscribe((state) => {
|
||||
if (isModelingCmdGroupReady(state)) {
|
||||
setReadyOnce(true)
|
||||
}
|
||||
})
|
||||
return () => subscription.unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Fire the callback when the modeling command group is ready
|
||||
useEffect(() => {
|
||||
if (isReadyOnce) {
|
||||
callback()
|
||||
}
|
||||
}, [isReadyOnce, ...deps])
|
||||
}
|
||||
|
||||
/**
|
||||
* If your onboarding step opens the command palette it will mess with keyboard focus.
|
||||
* To side-step this, use this hook to override a form submission to advance the onboarding.
|
||||
*/
|
||||
export function useAdvanceOnboardingOnFormSubmit(
|
||||
currentSlug?: OnboardingPath,
|
||||
platform: undefined | keyof typeof onboardingPaths = 'browser'
|
||||
) {
|
||||
const [_prev, goToNext] = useOnboardingClicks(currentSlug, platform)
|
||||
|
||||
useEffect(() => {
|
||||
// Override form submission events so the command palette can't be submitted
|
||||
const formSubmitListener = (e: SubmitEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.stopImmediatePropagation()
|
||||
goToNext()
|
||||
}
|
||||
window.addEventListener('submit', formSubmitListener)
|
||||
// Remove the listener when we leave
|
||||
return () => {
|
||||
window.removeEventListener('submit', formSubmitListener)
|
||||
}
|
||||
}, [goToNext])
|
||||
}
|
||||
|
Reference in New Issue
Block a user