Add export to cmd bar (#1593)
* Add new exportFile icon * Isolate exportFromEngine command * Naive initial export command * Update types to accept functions for arg defaultValue, required, and options * Make existing helper functions and configs work with new types * Make UI components work with new types support resolving function values and conditional logic * Add full export command to command bar * Replace ExportButton with thin wrapper on cmd bar command * fmt * Fix stale tests and bugs found by good tests * fmt * Update src/components/CommandBar/CommandArgOptionInput.tsx * Update snapshot tests and onboarding wording * Move the panel open click into doExport * Don't need to input storage step in export tests anymore * Remove console logs, fmt, select options if we need to * Increase test timeout --------- Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
This commit is contained in:
@ -743,12 +743,12 @@ test('Command bar works and can change a setting', async ({ page }) => {
|
|||||||
const themeOption = page.getByRole('option', { name: 'Set Theme' })
|
const themeOption = page.getByRole('option', { name: 'Set Theme' })
|
||||||
await expect(themeOption).toBeVisible()
|
await expect(themeOption).toBeVisible()
|
||||||
await themeOption.click()
|
await themeOption.click()
|
||||||
const themeInput = page.getByPlaceholder('Select an option')
|
const themeInput = page.getByPlaceholder('system')
|
||||||
await expect(themeInput).toBeVisible()
|
await expect(themeInput).toBeVisible()
|
||||||
await expect(themeInput).toBeFocused()
|
await expect(themeInput).toBeFocused()
|
||||||
// Select dark theme
|
// Select dark theme
|
||||||
await page.keyboard.press('ArrowDown')
|
await page.keyboard.press('ArrowDown')
|
||||||
await page.keyboard.press('ArrowDown')
|
await page.keyboard.press('ArrowUp')
|
||||||
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
|
await expect(page.getByRole('option', { name: Themes.Dark })).toHaveAttribute(
|
||||||
'data-headlessui-state',
|
'data-headlessui-state',
|
||||||
'active'
|
'active'
|
||||||
|
@ -29,7 +29,7 @@ test.beforeEach(async ({ context, page }) => {
|
|||||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test.setTimeout(60000)
|
test.setTimeout(120_000)
|
||||||
|
|
||||||
test('exports of each format should work', async ({ page, context }) => {
|
test('exports of each format should work', async ({ page, context }) => {
|
||||||
// FYI this test doesn't work with only engine running locally
|
// FYI this test doesn't work with only engine running locally
|
||||||
@ -90,8 +90,6 @@ const part001 = startSketchOn('-XZ')
|
|||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
await u.clearAndCloseDebugPanel()
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
await page.getByRole('button', { name: APP_NAME }).click()
|
|
||||||
|
|
||||||
interface Paths {
|
interface Paths {
|
||||||
modelPath: string
|
modelPath: string
|
||||||
imagePath: string
|
imagePath: string
|
||||||
@ -100,19 +98,21 @@ const part001 = startSketchOn('-XZ')
|
|||||||
const doExport = async (
|
const doExport = async (
|
||||||
output: Models['OutputFormat_type']
|
output: Models['OutputFormat_type']
|
||||||
): Promise<Paths> => {
|
): Promise<Paths> => {
|
||||||
await page.getByRole('button', { name: 'Export Model' }).click()
|
await page.getByRole('button', { name: APP_NAME }).click()
|
||||||
|
await page.getByRole('button', { name: 'Export Part' }).click()
|
||||||
const exportSelect = page.getByTestId('export-type')
|
|
||||||
await exportSelect.selectOption({ label: output.type })
|
|
||||||
|
|
||||||
|
// Go through export via command bar
|
||||||
|
await page.getByRole('option', { name: output.type, exact: false }).click()
|
||||||
if ('storage' in output) {
|
if ('storage' in output) {
|
||||||
const storageSelect = page.getByTestId('export-storage')
|
await page.getByRole('button', { name: 'storage', exact: false }).click()
|
||||||
await storageSelect.selectOption({ label: output.storage })
|
await page
|
||||||
|
.getByRole('option', { name: output.storage, exact: false })
|
||||||
|
.click()
|
||||||
}
|
}
|
||||||
|
await page.getByRole('button', { name: 'Submit command' }).click()
|
||||||
|
|
||||||
const downloadPromise = page.waitForEvent('download')
|
// Handle download
|
||||||
await page.getByRole('button', { name: 'Export', exact: true }).click()
|
const download = await page.waitForEvent('download')
|
||||||
const download = await downloadPromise
|
|
||||||
const downloadLocationer = (extra = '', isImage = false) =>
|
const downloadLocationer = (extra = '', isImage = false) =>
|
||||||
`./e2e/playwright/export-snapshots/${output.type}-${
|
`./e2e/playwright/export-snapshots/${output.type}-${
|
||||||
'storage' in output ? output.storage : ''
|
'storage' in output ? output.storage : ''
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Combobox } from '@headlessui/react'
|
import { Combobox } from '@headlessui/react'
|
||||||
import Fuse from 'fuse.js'
|
import Fuse from 'fuse.js'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { CommandArgumentOption } from 'lib/commandTypes'
|
import { CommandArgument, CommandArgumentOption } from 'lib/commandTypes'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
function CommandArgOptionInput({
|
function CommandArgOptionInput({
|
||||||
options,
|
options,
|
||||||
@ -11,51 +11,89 @@ function CommandArgOptionInput({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
placeholder,
|
placeholder,
|
||||||
}: {
|
}: {
|
||||||
options: CommandArgumentOption<unknown>[]
|
options: (CommandArgument<unknown> & { inputType: 'options' })['options']
|
||||||
argName: string
|
argName: string
|
||||||
stepBack: () => void
|
stepBack: () => void
|
||||||
onSubmit: (data: unknown) => void
|
onSubmit: (data: unknown) => void
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
}) {
|
}) {
|
||||||
const { commandBarSend, commandBarState } = useCommandsContext()
|
const { commandBarSend, commandBarState } = useCommandsContext()
|
||||||
|
const resolvedOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
typeof options === 'function'
|
||||||
|
? options(commandBarState.context)
|
||||||
|
: options,
|
||||||
|
[argName, options, commandBarState.context]
|
||||||
|
)
|
||||||
|
// The initial current option is either an already-input value or the configured default
|
||||||
|
const currentOption = useMemo(
|
||||||
|
() =>
|
||||||
|
resolvedOptions.find(
|
||||||
|
(o) => o.value === commandBarState.context.argumentsToSubmit[argName]
|
||||||
|
) || resolvedOptions.find((o) => o.isCurrent),
|
||||||
|
[commandBarState.context.argumentsToSubmit, argName, resolvedOptions]
|
||||||
|
)
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const formRef = useRef<HTMLFormElement>(null)
|
const formRef = useRef<HTMLFormElement>(null)
|
||||||
const [argValue, setArgValue] = useState<(typeof options)[number]['value']>(
|
const [selectedOption, setSelectedOption] = useState<
|
||||||
options.find((o) => 'isCurrent' in o && o.isCurrent)?.value ||
|
CommandArgumentOption<unknown>
|
||||||
commandBarState.context.argumentsToSubmit[argName] ||
|
>(currentOption || resolvedOptions[0])
|
||||||
options[0].value
|
const initialQuery = useMemo(() => '', [options, argName])
|
||||||
|
const [query, setQuery] = useState(initialQuery)
|
||||||
|
const [filteredOptions, setFilteredOptions] =
|
||||||
|
useState<typeof resolvedOptions>()
|
||||||
|
|
||||||
|
// Create a new Fuse instance when the options change
|
||||||
|
const fuse = useMemo(
|
||||||
|
() =>
|
||||||
|
new Fuse(resolvedOptions, {
|
||||||
|
keys: ['name', 'description'],
|
||||||
|
threshold: 0.3,
|
||||||
|
}),
|
||||||
|
[argName, resolvedOptions]
|
||||||
)
|
)
|
||||||
const [query, setQuery] = useState('')
|
|
||||||
const [filteredOptions, setFilteredOptions] = useState<typeof options>()
|
|
||||||
|
|
||||||
const fuse = new Fuse(options, {
|
// Reset the query and selected option when the argName changes
|
||||||
keys: ['name', 'description'],
|
useEffect(() => {
|
||||||
threshold: 0.3,
|
setQuery(initialQuery)
|
||||||
})
|
setSelectedOption(currentOption || resolvedOptions[0])
|
||||||
|
}, [argName])
|
||||||
|
|
||||||
|
// Auto focus and select the input when the component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
inputRef.current?.select()
|
inputRef.current?.select()
|
||||||
}, [inputRef])
|
}, [inputRef])
|
||||||
|
|
||||||
|
// Filter the options based on the query,
|
||||||
|
// resetting the query when the options change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const results = fuse.search(query).map((result) => result.item)
|
const results = fuse.search(query).map((result) => result.item)
|
||||||
setFilteredOptions(query.length > 0 ? results : options)
|
setFilteredOptions(query.length > 0 ? results : resolvedOptions)
|
||||||
}, [query])
|
}, [query, resolvedOptions, fuse])
|
||||||
|
|
||||||
function handleSelectOption(option: CommandArgumentOption<unknown>) {
|
function handleSelectOption(option: CommandArgumentOption<unknown>) {
|
||||||
setArgValue(option)
|
// We deal with the whole option object internally
|
||||||
|
setSelectedOption(option)
|
||||||
|
|
||||||
|
// But we only submit the value
|
||||||
onSubmit(option.value)
|
onSubmit(option.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onSubmit(argValue)
|
|
||||||
|
// We submit the value of the selected option, not the whole object
|
||||||
|
onSubmit(selectedOption.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
|
<form id="arg-form" onSubmit={handleSubmit} ref={formRef}>
|
||||||
<Combobox value={argValue} onChange={handleSelectOption} name="options">
|
<Combobox
|
||||||
|
value={selectedOption}
|
||||||
|
onChange={handleSelectOption}
|
||||||
|
name="options"
|
||||||
|
>
|
||||||
<div className="flex items-center mx-4 mt-4 mb-2">
|
<div className="flex items-center mx-4 mt-4 mb-2">
|
||||||
<label
|
<label
|
||||||
htmlFor="option-input"
|
htmlFor="option-input"
|
||||||
@ -75,10 +113,12 @@ function CommandArgOptionInput({
|
|||||||
stepBack()
|
stepBack()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
value={query}
|
||||||
placeholder={
|
placeholder={
|
||||||
(argValue as CommandArgumentOption<unknown>)?.name ||
|
currentOption?.name ||
|
||||||
placeholder ||
|
placeholder ||
|
||||||
'Select an option for ' + argName
|
argName ||
|
||||||
|
'Select an option'
|
||||||
}
|
}
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
@ -98,7 +138,7 @@ function CommandArgOptionInput({
|
|||||||
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
|
className="flex items-center gap-2 px-4 py-1 first:mt-2 last:mb-2 ui-active:bg-energy-10/50 dark:ui-active:bg-chalkboard-90"
|
||||||
>
|
>
|
||||||
<p className="flex-grow">{option.name} </p>
|
<p className="flex-grow">{option.name} </p>
|
||||||
{'isCurrent' in option && option.isCurrent && (
|
{option.value === currentOption?.value && (
|
||||||
<small className="text-chalkboard-70 dark:text-chalkboard-50">
|
<small className="text-chalkboard-70 dark:text-chalkboard-50">
|
||||||
current
|
current
|
||||||
</small>
|
</small>
|
||||||
|
@ -29,12 +29,6 @@ export const CommandBarProvider = ({
|
|||||||
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
|
const [commandBarState, commandBarSend] = useMachine(commandBarMachine, {
|
||||||
devTools: true,
|
devTools: true,
|
||||||
guards: {
|
guards: {
|
||||||
'Arguments are ready': (context, _) => {
|
|
||||||
return context.selectedCommand?.args
|
|
||||||
? context.argumentsToSubmit.length ===
|
|
||||||
Object.keys(context.selectedCommand.args)?.length
|
|
||||||
: false
|
|
||||||
},
|
|
||||||
'Command has no arguments': (context, _event) => {
|
'Command has no arguments': (context, _event) => {
|
||||||
return (
|
return (
|
||||||
!context.selectedCommand?.args ||
|
!context.selectedCommand?.args ||
|
||||||
@ -81,7 +75,12 @@ export const CommandBar = () => {
|
|||||||
function stepBack() {
|
function stepBack() {
|
||||||
if (!currentArgument) {
|
if (!currentArgument) {
|
||||||
if (commandBarState.matches('Review')) {
|
if (commandBarState.matches('Review')) {
|
||||||
const entries = Object.entries(selectedCommand?.args || {})
|
const entries = Object.entries(selectedCommand?.args || {}).filter(
|
||||||
|
([_, argConfig]) =>
|
||||||
|
typeof argConfig.required === 'function'
|
||||||
|
? argConfig.required(commandBarState.context)
|
||||||
|
: argConfig.required
|
||||||
|
)
|
||||||
|
|
||||||
const currentArgName = entries[entries.length - 1][0]
|
const currentArgName = entries[entries.length - 1][0]
|
||||||
const currentArg = {
|
const currentArg = {
|
||||||
@ -89,19 +88,12 @@ export const CommandBar = () => {
|
|||||||
...entries[entries.length - 1][1],
|
...entries[entries.length - 1][1],
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandBarState.matches('Review')) {
|
commandBarSend({
|
||||||
commandBarSend({
|
type: 'Edit argument',
|
||||||
type: 'Edit argument',
|
data: {
|
||||||
data: {
|
arg: currentArg,
|
||||||
arg: currentArg,
|
},
|
||||||
},
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
commandBarSend({
|
|
||||||
type: 'Remove argument',
|
|
||||||
data: { [currentArgName]: currentArg },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
commandBarSend({ type: 'Deselect command' })
|
commandBarSend({ type: 'Deselect command' })
|
||||||
}
|
}
|
||||||
@ -124,11 +116,6 @@ export const CommandBar = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => console.log(commandBarState.context.argumentsToSubmit),
|
|
||||||
[commandBarState.context.argumentsToSubmit]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root
|
<Transition.Root
|
||||||
show={!commandBarState.matches('Closed') || false}
|
show={!commandBarState.matches('Closed') || false}
|
||||||
|
@ -76,72 +76,82 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
|||||||
)}
|
)}
|
||||||
{selectedCommand?.name}
|
{selectedCommand?.name}
|
||||||
</p>
|
</p>
|
||||||
{Object.entries(selectedCommand?.args || {}).map(
|
{Object.entries(selectedCommand?.args || {})
|
||||||
([argName, arg], i) => (
|
.filter(([_, argConfig]) =>
|
||||||
<button
|
typeof argConfig.required === 'function'
|
||||||
disabled={!isReviewing && currentArgument?.name === argName}
|
? argConfig.required(commandBarState.context)
|
||||||
onClick={() => {
|
: argConfig.required
|
||||||
commandBarSend({
|
|
||||||
type: isReviewing
|
|
||||||
? 'Edit argument'
|
|
||||||
: 'Change current argument',
|
|
||||||
data: { arg: { ...arg, name: argName } },
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
key={argName}
|
|
||||||
className={`relative w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
|
|
||||||
argName === currentArgument?.name
|
|
||||||
? 'disabled:bg-energy-10/50 dark:disabled:bg-energy-10/20 disabled:border-energy-10 dark:disabled:border-energy-10 disabled:text-chalkboard-100 dark:disabled:text-chalkboard-10'
|
|
||||||
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="capitalize">{argName}</span>
|
|
||||||
{argumentsToSubmit[argName] ? (
|
|
||||||
arg.inputType === 'selection' ? (
|
|
||||||
getSelectionTypeDisplayText(
|
|
||||||
argumentsToSubmit[argName] as Selections
|
|
||||||
)
|
|
||||||
) : arg.inputType === 'kcl' ? (
|
|
||||||
roundOff(
|
|
||||||
Number(
|
|
||||||
(argumentsToSubmit[argName] as KclCommandValue)
|
|
||||||
.valueCalculated
|
|
||||||
),
|
|
||||||
4
|
|
||||||
)
|
|
||||||
) : typeof argumentsToSubmit[argName] === 'object' ? (
|
|
||||||
JSON.stringify(argumentsToSubmit[argName])
|
|
||||||
) : (
|
|
||||||
<em>{argumentsToSubmit[argName] as ReactNode}</em>
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
{showShortcuts && (
|
|
||||||
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
|
|
||||||
<span className="sr-only">Hotkey: </span>
|
|
||||||
{i + 1}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
{arg.inputType === 'kcl' &&
|
|
||||||
!!argumentsToSubmit[argName] &&
|
|
||||||
'variableName' in
|
|
||||||
(argumentsToSubmit[argName] as KclCommandValue) && (
|
|
||||||
<>
|
|
||||||
<CustomIcon name="make-variable" className="w-4 h-4" />
|
|
||||||
<Tooltip position="blockEnd">
|
|
||||||
New variable:{' '}
|
|
||||||
{
|
|
||||||
(
|
|
||||||
argumentsToSubmit[
|
|
||||||
argName
|
|
||||||
] as KclExpressionWithVariable
|
|
||||||
).variableName
|
|
||||||
}
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)
|
)
|
||||||
)}
|
.map(([argName, arg], i) => {
|
||||||
|
const argValue =
|
||||||
|
(typeof argumentsToSubmit[argName] === 'function'
|
||||||
|
? (argumentsToSubmit[argName] as Function)(
|
||||||
|
commandBarState.context
|
||||||
|
)
|
||||||
|
: argumentsToSubmit[argName]) || ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
disabled={!isReviewing && currentArgument?.name === argName}
|
||||||
|
onClick={() => {
|
||||||
|
commandBarSend({
|
||||||
|
type: isReviewing
|
||||||
|
? 'Edit argument'
|
||||||
|
: 'Change current argument',
|
||||||
|
data: { arg: { ...arg, name: argName } },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
key={argName}
|
||||||
|
className={`relative w-fit px-2 py-1 rounded-sm flex gap-2 items-center border ${
|
||||||
|
argName === currentArgument?.name
|
||||||
|
? 'disabled:bg-energy-10/50 dark:disabled:bg-energy-10/20 disabled:border-energy-10 dark:disabled:border-energy-10 disabled:text-chalkboard-100 dark:disabled:text-chalkboard-10'
|
||||||
|
: 'bg-chalkboard-20/50 dark:bg-chalkboard-80/50 border-chalkboard-20 dark:border-chalkboard-80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="capitalize">{argName}</span>
|
||||||
|
{argValue ? (
|
||||||
|
arg.inputType === 'selection' ? (
|
||||||
|
getSelectionTypeDisplayText(argValue as Selections)
|
||||||
|
) : arg.inputType === 'kcl' ? (
|
||||||
|
roundOff(
|
||||||
|
Number((argValue as KclCommandValue).valueCalculated),
|
||||||
|
4
|
||||||
|
)
|
||||||
|
) : typeof argValue === 'object' ? (
|
||||||
|
JSON.stringify(argValue)
|
||||||
|
) : (
|
||||||
|
<em>{argValue}</em>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
{showShortcuts && (
|
||||||
|
<small className="absolute -top-[1px] right-full translate-x-1/2 px-0.5 rounded-sm bg-chalkboard-80 text-chalkboard-10 dark:bg-energy-10 dark:text-chalkboard-100">
|
||||||
|
<span className="sr-only">Hotkey: </span>
|
||||||
|
{i + 1}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
{arg.inputType === 'kcl' &&
|
||||||
|
!!argValue &&
|
||||||
|
'variableName' in (argValue as KclCommandValue) && (
|
||||||
|
<>
|
||||||
|
<CustomIcon
|
||||||
|
name="make-variable"
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<Tooltip position="blockEnd">
|
||||||
|
New variable:{' '}
|
||||||
|
{
|
||||||
|
(
|
||||||
|
argumentsToSubmit[
|
||||||
|
argName
|
||||||
|
] as KclExpressionWithVariable
|
||||||
|
).variableName
|
||||||
|
}
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
{isReviewing ? <ReviewingButton /> : <GatheringArgsButton />}
|
{isReviewing ? <ReviewingButton /> : <GatheringArgsButton />}
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,7 +48,8 @@ function CommandBarReview({ stepBack }: { stepBack: () => void }) {
|
|||||||
if (!arg) return
|
if (!arg) return
|
||||||
})
|
})
|
||||||
|
|
||||||
function submitCommand() {
|
function submitCommand(e: React.FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault()
|
||||||
commandBarSend({
|
commandBarSend({
|
||||||
type: 'Submit command',
|
type: 'Submit command',
|
||||||
data: argumentsToSubmit,
|
data: argumentsToSubmit,
|
||||||
|
@ -29,7 +29,7 @@ function CommandBarSelectionInput({
|
|||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
const [hasSubmitted, setHasSubmitted] = useState(false)
|
const [hasSubmitted, setHasSubmitted] = useState(false)
|
||||||
const selection = useSelector(arg.actor, selectionSelector)
|
const selection = useSelector(arg.machineActor, selectionSelector)
|
||||||
const [selectionsByType, setSelectionsByType] = useState<
|
const [selectionsByType, setSelectionsByType] = useState<
|
||||||
'none' | ResolvedSelectionType[]
|
'none' | ResolvedSelectionType[]
|
||||||
>(
|
>(
|
||||||
|
@ -9,6 +9,7 @@ export type CustomIconName =
|
|||||||
| 'clipboardCheckmark'
|
| 'clipboardCheckmark'
|
||||||
| 'close'
|
| 'close'
|
||||||
| 'equal'
|
| 'equal'
|
||||||
|
| 'exportFile'
|
||||||
| 'extrude'
|
| 'extrude'
|
||||||
| 'file'
|
| 'file'
|
||||||
| 'filePlus'
|
| 'filePlus'
|
||||||
@ -194,6 +195,22 @@ export const CustomIcon = ({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
case 'exportFile':
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M4 3H4.5H11H11.2071L11.3536 3.14645L15.8536 7.64646L16 7.7929V8.00001V11.3773C15.6992 11.1362 15.3628 10.9376 15 10.7908V8.50001H11H10.5V8.00001V4H5V16H9.79076C9.93763 16.3628 10.1362 16.6992 10.3773 17H4.5H4V16.5V3.5V3ZM11.5 4.70711L14.2929 7.50001H11.5V4.70711ZM16.3904 14.1877L14.3904 11.6877L13.6096 12.3124L14.9597 14H11V15H14.9597L13.6096 16.6877L14.3904 17.3124L16.3904 14.8124L16.6403 14.5L16.3904 14.1877Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
case 'extrude':
|
case 'extrude':
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
@ -1,238 +0,0 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { faFileExport, faXmark } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import { ActionButton } from './ActionButton'
|
|
||||||
import Modal from 'react-modal'
|
|
||||||
import React from 'react'
|
|
||||||
import { useFormik } from 'formik'
|
|
||||||
import { Models } from '@kittycad/lib'
|
|
||||||
import { engineCommandManager } from '../lang/std/engineConnection'
|
|
||||||
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
|
|
||||||
|
|
||||||
type OutputFormat = Models['OutputFormat_type']
|
|
||||||
type OutputTypeKey = OutputFormat['type']
|
|
||||||
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
|
|
||||||
type StorageUnion = ExtractStorageTypes<OutputFormat>
|
|
||||||
|
|
||||||
export interface ExportButtonProps extends React.PropsWithChildren {
|
|
||||||
className?: {
|
|
||||||
button?: string
|
|
||||||
icon?: string
|
|
||||||
bg?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExportButton = ({ children, className }: ExportButtonProps) => {
|
|
||||||
const [modalIsOpen, setIsOpen] = React.useState(false)
|
|
||||||
const {
|
|
||||||
settings: {
|
|
||||||
state: {
|
|
||||||
context: { baseUnit },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} = useGlobalStateContext()
|
|
||||||
|
|
||||||
const defaultType = 'gltf'
|
|
||||||
const [type, setType] = React.useState<OutputTypeKey>(defaultType)
|
|
||||||
const defaultStorage = 'embedded'
|
|
||||||
const [storage, setStorage] = React.useState<StorageUnion>(defaultStorage)
|
|
||||||
|
|
||||||
function openModal() {
|
|
||||||
setIsOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal() {
|
|
||||||
setIsOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to gltf and embedded.
|
|
||||||
const initialValues: OutputFormat = {
|
|
||||||
type: defaultType,
|
|
||||||
storage: defaultStorage,
|
|
||||||
presentation: 'pretty',
|
|
||||||
}
|
|
||||||
const formik = useFormik({
|
|
||||||
initialValues,
|
|
||||||
onSubmit: (values: OutputFormat) => {
|
|
||||||
// Set the default coords.
|
|
||||||
if (
|
|
||||||
values.type === 'obj' ||
|
|
||||||
values.type === 'ply' ||
|
|
||||||
values.type === 'step' ||
|
|
||||||
values.type === 'stl'
|
|
||||||
) {
|
|
||||||
// Set the default coords.
|
|
||||||
// In the future we can make this configurable.
|
|
||||||
// But for now, its probably best to keep it consistent with the
|
|
||||||
// UI.
|
|
||||||
values.coords = {
|
|
||||||
forward: {
|
|
||||||
axis: 'y',
|
|
||||||
direction: 'negative',
|
|
||||||
},
|
|
||||||
up: {
|
|
||||||
axis: 'z',
|
|
||||||
direction: 'positive',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
values.type === 'obj' ||
|
|
||||||
values.type === 'stl' ||
|
|
||||||
values.type === 'ply'
|
|
||||||
) {
|
|
||||||
values.units = baseUnit
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
values.type === 'ply' ||
|
|
||||||
values.type === 'stl' ||
|
|
||||||
values.type === 'gltf'
|
|
||||||
) {
|
|
||||||
// Set the storage type.
|
|
||||||
values.storage = storage
|
|
||||||
}
|
|
||||||
if (values.type === 'ply' || values.type === 'stl') {
|
|
||||||
values.selection = { type: 'default_scene' }
|
|
||||||
}
|
|
||||||
engineCommandManager.sendSceneCommand({
|
|
||||||
type: 'modeling_cmd_req',
|
|
||||||
cmd: {
|
|
||||||
type: 'export',
|
|
||||||
// By default let's leave this blank to export the whole scene.
|
|
||||||
// In the future we might want to let the user choose which entities
|
|
||||||
// in the scene to export. In that case, you'd pass the IDs thru here.
|
|
||||||
entity_ids: [],
|
|
||||||
format: values,
|
|
||||||
source_unit: baseUnit,
|
|
||||||
},
|
|
||||||
cmd_id: uuidv4(),
|
|
||||||
})
|
|
||||||
|
|
||||||
closeModal()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ActionButton
|
|
||||||
onClick={openModal}
|
|
||||||
Element="button"
|
|
||||||
icon={{
|
|
||||||
icon: faFileExport,
|
|
||||||
className: 'p-1',
|
|
||||||
size: 'sm',
|
|
||||||
iconClassName: className?.icon,
|
|
||||||
bgClassName: className?.bg,
|
|
||||||
}}
|
|
||||||
className={className?.button}
|
|
||||||
>
|
|
||||||
{children || 'Export'}
|
|
||||||
</ActionButton>
|
|
||||||
<Modal
|
|
||||||
isOpen={modalIsOpen}
|
|
||||||
onRequestClose={closeModal}
|
|
||||||
contentLabel="Export"
|
|
||||||
overlayClassName="z-40 fixed inset-0 grid place-items-center"
|
|
||||||
className="rounded p-4 bg-chalkboard-10 dark:bg-chalkboard-100 border max-w-xl w-full"
|
|
||||||
>
|
|
||||||
<h1 className="text-2xl font-bold">Export your design</h1>
|
|
||||||
<form onSubmit={formik.handleSubmit}>
|
|
||||||
<div className="flex flex-wrap justify-between gap-8 items-center w-full my-8">
|
|
||||||
<label htmlFor="type" className="flex-1">
|
|
||||||
<p className="mb-2">Type</p>
|
|
||||||
<select
|
|
||||||
id="type"
|
|
||||||
name="type"
|
|
||||||
data-testid="export-type"
|
|
||||||
onChange={(e) => {
|
|
||||||
setType(e.target.value as OutputTypeKey)
|
|
||||||
if (e.target.value === 'gltf') {
|
|
||||||
// Set default to embedded.
|
|
||||||
setStorage('embedded')
|
|
||||||
} else if (e.target.value === 'ply') {
|
|
||||||
// Set default to ascii.
|
|
||||||
setStorage('ascii')
|
|
||||||
} else if (e.target.value === 'stl') {
|
|
||||||
// Set default to ascii.
|
|
||||||
setStorage('ascii')
|
|
||||||
}
|
|
||||||
formik.handleChange(e)
|
|
||||||
}}
|
|
||||||
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
|
||||||
>
|
|
||||||
<option value="gltf">gltf</option>
|
|
||||||
<option value="obj">obj</option>
|
|
||||||
<option value="ply">ply</option>
|
|
||||||
<option value="step">step</option>
|
|
||||||
<option value="stl">stl</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
{(type === 'gltf' || type === 'ply' || type === 'stl') && (
|
|
||||||
<label htmlFor="storage" className="flex-1">
|
|
||||||
<p className="mb-2">Storage</p>
|
|
||||||
<select
|
|
||||||
id="storage"
|
|
||||||
name="storage"
|
|
||||||
data-testid="export-storage"
|
|
||||||
onChange={(e) => {
|
|
||||||
setStorage(e.target.value as StorageUnion)
|
|
||||||
formik.handleChange(e)
|
|
||||||
}}
|
|
||||||
className="bg-chalkboard-20 dark:bg-chalkboard-90 w-full"
|
|
||||||
>
|
|
||||||
{type === 'gltf' && (
|
|
||||||
<>
|
|
||||||
<option value="embedded">embedded</option>
|
|
||||||
<option value="binary">binary</option>
|
|
||||||
<option value="standard">standard</option>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{type === 'stl' && (
|
|
||||||
<>
|
|
||||||
<option value="ascii">ascii</option>
|
|
||||||
<option value="binary">binary</option>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{type === 'ply' && (
|
|
||||||
<>
|
|
||||||
<option value="ascii">ascii</option>
|
|
||||||
<option value="binary_little_endian">
|
|
||||||
binary_little_endian
|
|
||||||
</option>
|
|
||||||
<option value="binary_big_endian">
|
|
||||||
binary_big_endian
|
|
||||||
</option>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between mt-6">
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
onClick={closeModal}
|
|
||||||
icon={{
|
|
||||||
icon: faXmark,
|
|
||||||
className: 'p-1',
|
|
||||||
bgClassName: 'bg-destroy-80',
|
|
||||||
iconClassName:
|
|
||||||
'text-destroy-20 group-hover:text-destroy-10 hover:text-destroy-10',
|
|
||||||
}}
|
|
||||||
className="hover:border-destroy-40"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</ActionButton>
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
type="submit"
|
|
||||||
icon={{ icon: faFileExport, className: 'p-1' }}
|
|
||||||
>
|
|
||||||
Export
|
|
||||||
</ActionButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -57,27 +57,30 @@ export const GlobalStateProvider = ({
|
|||||||
>
|
>
|
||||||
)
|
)
|
||||||
|
|
||||||
const [settingsState, settingsSend] = useMachine(settingsMachine, {
|
const [settingsState, settingsSend, settingsActor] = useMachine(
|
||||||
context: persistedSettings,
|
settingsMachine,
|
||||||
actions: {
|
{
|
||||||
toastSuccess: (context, event) => {
|
context: persistedSettings,
|
||||||
const truncatedNewValue =
|
actions: {
|
||||||
'data' in event && event.data instanceof Object
|
toastSuccess: (context, event) => {
|
||||||
? (context[Object.keys(event.data)[0] as keyof typeof context]
|
const truncatedNewValue =
|
||||||
.toString()
|
'data' in event && event.data instanceof Object
|
||||||
.substring(0, 28) as any)
|
? (String(
|
||||||
: undefined
|
context[Object.keys(event.data)[0] as keyof typeof context]
|
||||||
toast.success(
|
).substring(0, 28) as any)
|
||||||
event.type +
|
: undefined
|
||||||
(truncatedNewValue
|
toast.success(
|
||||||
? ` to "${truncatedNewValue}${
|
event.type +
|
||||||
truncatedNewValue.length === 28 ? '...' : ''
|
(truncatedNewValue
|
||||||
}"`
|
? ` to "${truncatedNewValue}${
|
||||||
: '')
|
truncatedNewValue.length === 28 ? '...' : ''
|
||||||
)
|
}"`
|
||||||
|
: '')
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
})
|
)
|
||||||
settingsStateRef = settingsState.context
|
settingsStateRef = settingsState.context
|
||||||
|
|
||||||
useStateMachineCommands({
|
useStateMachineCommands({
|
||||||
@ -85,6 +88,7 @@ export const GlobalStateProvider = ({
|
|||||||
state: settingsState,
|
state: settingsState,
|
||||||
send: settingsSend,
|
send: settingsSend,
|
||||||
commandBarConfig: settingsCommandBarConfig,
|
commandBarConfig: settingsCommandBarConfig,
|
||||||
|
actor: settingsActor,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Listen for changes to the system theme and update the app theme accordingly
|
// Listen for changes to the system theme and update the app theme accordingly
|
||||||
@ -105,7 +109,7 @@ export const GlobalStateProvider = ({
|
|||||||
}, [settingsState.context])
|
}, [settingsState.context])
|
||||||
|
|
||||||
// Auth machine setup
|
// Auth machine setup
|
||||||
const [authState, authSend] = useMachine(authMachine, {
|
const [authState, authSend, authActor] = useMachine(authMachine, {
|
||||||
actions: {
|
actions: {
|
||||||
goToSignInPage: () => {
|
goToSignInPage: () => {
|
||||||
navigate(paths.SIGN_IN)
|
navigate(paths.SIGN_IN)
|
||||||
@ -125,6 +129,7 @@ export const GlobalStateProvider = ({
|
|||||||
state: authState,
|
state: authState,
|
||||||
send: authSend,
|
send: authSend,
|
||||||
commandBarConfig: authCommandBarConfig,
|
commandBarConfig: authCommandBarConfig,
|
||||||
|
actor: authActor,
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -38,6 +38,10 @@ import { getSketchQuaternion } from 'clientSideScene/sceneEntities'
|
|||||||
import { startSketchOnDefault } from 'lang/modifyAst'
|
import { startSketchOnDefault } from 'lang/modifyAst'
|
||||||
import { Program } from 'lang/wasm'
|
import { Program } from 'lang/wasm'
|
||||||
import { isSingleCursorInPipe } from 'lang/queryAst'
|
import { isSingleCursorInPipe } from 'lang/queryAst'
|
||||||
|
import { TEST } from 'env'
|
||||||
|
import { exportFromEngine } from 'lib/exportFromEngine'
|
||||||
|
import { Models } from '@kittycad/lib/dist/types/src'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -54,7 +58,12 @@ export const ModelingMachineProvider = ({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const { auth } = useGlobalStateContext()
|
const {
|
||||||
|
auth,
|
||||||
|
settings: {
|
||||||
|
context: { baseUnit },
|
||||||
|
},
|
||||||
|
} = useGlobalStateContext()
|
||||||
const { code } = useKclContext()
|
const { code } = useKclContext()
|
||||||
const token = auth?.context?.token
|
const token = auth?.context?.token
|
||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
@ -170,6 +179,56 @@ export const ModelingMachineProvider = ({
|
|||||||
}
|
}
|
||||||
return { selectionRangeTypeMap }
|
return { selectionRangeTypeMap }
|
||||||
}),
|
}),
|
||||||
|
'Engine export': (_, event) => {
|
||||||
|
if (event.type !== 'Export' || TEST) return
|
||||||
|
const format = {
|
||||||
|
...event.data,
|
||||||
|
} as Partial<Models['OutputFormat_type']>
|
||||||
|
|
||||||
|
// Set all the un-configurable defaults here.
|
||||||
|
if (format.type === 'gltf') {
|
||||||
|
format.presentation = 'pretty'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
format.type === 'obj' ||
|
||||||
|
format.type === 'ply' ||
|
||||||
|
format.type === 'step' ||
|
||||||
|
format.type === 'stl'
|
||||||
|
) {
|
||||||
|
// Set the default coords.
|
||||||
|
// In the future we can make this configurable.
|
||||||
|
// But for now, its probably best to keep it consistent with the
|
||||||
|
// UI.
|
||||||
|
format.coords = {
|
||||||
|
forward: {
|
||||||
|
axis: 'y',
|
||||||
|
direction: 'negative',
|
||||||
|
},
|
||||||
|
up: {
|
||||||
|
axis: 'z',
|
||||||
|
direction: 'positive',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
format.type === 'obj' ||
|
||||||
|
format.type === 'stl' ||
|
||||||
|
format.type === 'ply'
|
||||||
|
) {
|
||||||
|
format.units = baseUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format.type === 'ply' || format.type === 'stl') {
|
||||||
|
format.selection = { type: 'default_scene' }
|
||||||
|
}
|
||||||
|
|
||||||
|
exportFromEngine({
|
||||||
|
source_unit: baseUnit,
|
||||||
|
format: format as Models['OutputFormat_type'],
|
||||||
|
}).catch((e) => toast.error('Error while exporting', e)) // TODO I think we need to throw the error from engineCommandManager
|
||||||
|
},
|
||||||
},
|
},
|
||||||
guards: {
|
guards: {
|
||||||
'has valid extrude selection': ({ selectionRanges }) => {
|
'has valid extrude selection': ({ selectionRanges }) => {
|
||||||
@ -192,6 +251,8 @@ export const ModelingMachineProvider = ({
|
|||||||
selectionRanges
|
selectionRanges
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'Has exportable geometry': () =>
|
||||||
|
kclManager.kclErrors.length === 0 && kclManager.ast.body.length > 0,
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
'AST-undo-startSketchOn': async ({ sketchPathToNode }) => {
|
'AST-undo-startSketchOn': async ({ sketchPathToNode }) => {
|
||||||
|
@ -5,7 +5,6 @@ import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
|||||||
import { GlobalStateProvider } from './GlobalStateProvider'
|
import { GlobalStateProvider } from './GlobalStateProvider'
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import { ExportButtonProps } from './ExportButton'
|
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const projectWellFormed = {
|
const projectWellFormed = {
|
||||||
@ -38,15 +37,6 @@ const projectWellFormed = {
|
|||||||
},
|
},
|
||||||
} satisfies ProjectWithEntryPointMetadata
|
} satisfies ProjectWithEntryPointMetadata
|
||||||
|
|
||||||
const mockExportButton = vi.fn()
|
|
||||||
vi.mock('/src/components/ExportButton', () => ({
|
|
||||||
// engineCommandManager method call in ExportButton causes vitest to hang
|
|
||||||
ExportButton: (props: ExportButtonProps) => {
|
|
||||||
mockExportButton(props)
|
|
||||||
return <button>Fake export button</button>
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe('ProjectSidebarMenu tests', () => {
|
describe('ProjectSidebarMenu tests', () => {
|
||||||
test('Renders the project name', () => {
|
test('Renders the project name', () => {
|
||||||
render(
|
render(
|
||||||
|
@ -5,12 +5,12 @@ import { type IndexLoaderData } from 'lib/types'
|
|||||||
import { paths } from 'lib/paths'
|
import { paths } from 'lib/paths'
|
||||||
import { isTauri } from '../lib/isTauri'
|
import { isTauri } from '../lib/isTauri'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ExportButton } from './ExportButton'
|
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { FileTree } from './FileTree'
|
import { FileTree } from './FileTree'
|
||||||
import { sep } from '@tauri-apps/api/path'
|
import { sep } from '@tauri-apps/api/path'
|
||||||
import { Logo } from './Logo'
|
import { Logo } from './Logo'
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
|
||||||
const ProjectSidebarMenu = ({
|
const ProjectSidebarMenu = ({
|
||||||
project,
|
project,
|
||||||
@ -21,6 +21,8 @@ const ProjectSidebarMenu = ({
|
|||||||
project?: IndexLoaderData['project']
|
project?: IndexLoaderData['project']
|
||||||
file?: IndexLoaderData['file']
|
file?: IndexLoaderData['file']
|
||||||
}) => {
|
}) => {
|
||||||
|
const { commandBarSend } = useCommandsContext()
|
||||||
|
|
||||||
return renderAsLink ? (
|
return renderAsLink ? (
|
||||||
<Link
|
<Link
|
||||||
to={paths.HOME}
|
to={paths.HOME}
|
||||||
@ -112,13 +114,19 @@ const ProjectSidebarMenu = ({
|
|||||||
<div className="flex-1 overflow-hidden" />
|
<div className="flex-1 overflow-hidden" />
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90">
|
<div className="flex flex-col gap-2 p-4 dark:bg-chalkboard-90">
|
||||||
<ExportButton
|
<ActionButton
|
||||||
className={{
|
Element="button"
|
||||||
button: 'border-transparent dark:border-transparent',
|
icon={{ icon: 'exportFile', className: 'p-1' }}
|
||||||
}}
|
className="border-transparent dark:border-transparent"
|
||||||
|
onClick={() =>
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: { name: 'Export', ownerMachine: 'modeling' },
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Export Model
|
Export Part
|
||||||
</ExportButton>
|
</ActionButton>
|
||||||
{isTauri() && (
|
{isTauri() && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="link"
|
Element="link"
|
||||||
|
@ -28,7 +28,7 @@ interface UseStateMachineCommandsArgs<
|
|||||||
machineId: T['id']
|
machineId: T['id']
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
send: Function
|
send: Function
|
||||||
actor?: InterpreterFrom<T>
|
actor: InterpreterFrom<T>
|
||||||
commandBarConfig?: CommandSetConfig<T, S>
|
commandBarConfig?: CommandSetConfig<T, S>
|
||||||
allCommandsRequireNetwork?: boolean
|
allCommandsRequireNetwork?: boolean
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
|
@ -28,7 +28,8 @@ export const homeCommandBarConfig: CommandSetConfig<
|
|||||||
name: {
|
name: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
options: (context) =>
|
options: [],
|
||||||
|
optionsFromContext: (context) =>
|
||||||
context.projects.map((p) => ({
|
context.projects.map((p) => ({
|
||||||
name: p.name!,
|
name: p.name!,
|
||||||
value: p.name!,
|
value: p.name!,
|
||||||
@ -43,7 +44,7 @@ export const homeCommandBarConfig: CommandSetConfig<
|
|||||||
name: {
|
name: {
|
||||||
inputType: 'string',
|
inputType: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.defaultProjectName,
|
defaultValueFromContext: (context) => context.defaultProjectName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -55,7 +56,8 @@ export const homeCommandBarConfig: CommandSetConfig<
|
|||||||
name: {
|
name: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
options: (context) =>
|
options: [],
|
||||||
|
optionsFromContext: (context) =>
|
||||||
context.projects.map((p) => ({
|
context.projects.map((p) => ({
|
||||||
name: p.name!,
|
name: p.name!,
|
||||||
value: p.name!,
|
value: p.name!,
|
||||||
@ -71,7 +73,8 @@ export const homeCommandBarConfig: CommandSetConfig<
|
|||||||
oldName: {
|
oldName: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
options: (context) =>
|
options: [],
|
||||||
|
optionsFromContext: (context) =>
|
||||||
context.projects.map((p) => ({
|
context.projects.map((p) => ({
|
||||||
name: p.name!,
|
name: p.name!,
|
||||||
value: p.name!,
|
value: p.name!,
|
||||||
@ -80,7 +83,7 @@ export const homeCommandBarConfig: CommandSetConfig<
|
|||||||
newName: {
|
newName: {
|
||||||
inputType: 'string',
|
inputType: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.defaultProjectName,
|
defaultValueFromContext: (context) => context.defaultProjectName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
|
import { Models } from '@kittycad/lib'
|
||||||
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
import { CommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { modelingMachine } from 'machines/modelingMachine'
|
import { modelingMachine } from 'machines/modelingMachine'
|
||||||
|
|
||||||
|
type OutputFormat = Models['OutputFormat_type']
|
||||||
|
type OutputTypeKey = OutputFormat['type']
|
||||||
|
type ExtractStorageTypes<T> = T extends { storage: infer U } ? U : never
|
||||||
|
type StorageUnion = ExtractStorageTypes<OutputFormat>
|
||||||
|
|
||||||
export const EXTRUSION_RESULTS = [
|
export const EXTRUSION_RESULTS = [
|
||||||
'new',
|
'new',
|
||||||
'add',
|
'add',
|
||||||
@ -11,6 +17,10 @@ export const EXTRUSION_RESULTS = [
|
|||||||
|
|
||||||
export type ModelingCommandSchema = {
|
export type ModelingCommandSchema = {
|
||||||
'Enter sketch': {}
|
'Enter sketch': {}
|
||||||
|
Export: {
|
||||||
|
type: OutputTypeKey
|
||||||
|
storage?: StorageUnion
|
||||||
|
}
|
||||||
Extrude: {
|
Extrude: {
|
||||||
selection: Selections // & { type: 'face' } would be cool to lock that down
|
selection: Selections // & { type: 'face' } would be cool to lock that down
|
||||||
// result: (typeof EXTRUSION_RESULTS)[number]
|
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||||
@ -26,6 +36,80 @@ export const modelingMachineConfig: CommandSetConfig<
|
|||||||
description: 'Enter sketch mode.',
|
description: 'Enter sketch mode.',
|
||||||
icon: 'sketch',
|
icon: 'sketch',
|
||||||
},
|
},
|
||||||
|
Export: {
|
||||||
|
description: 'Export the current model.',
|
||||||
|
icon: 'exportFile',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
type: {
|
||||||
|
inputType: 'options',
|
||||||
|
defaultValue: 'gltf',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ name: 'gLTF', isCurrent: true, value: 'gltf' },
|
||||||
|
{ name: 'OBJ', isCurrent: false, value: 'obj' },
|
||||||
|
{ name: 'STL', isCurrent: false, value: 'stl' },
|
||||||
|
{ name: 'STEP', isCurrent: false, value: 'step' },
|
||||||
|
{ name: 'PLY', isCurrent: false, value: 'ply' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
inputType: 'options',
|
||||||
|
defaultValue: (c) => {
|
||||||
|
switch (c.argumentsToSubmit.type) {
|
||||||
|
case 'gltf':
|
||||||
|
return 'embedded'
|
||||||
|
case 'stl':
|
||||||
|
return 'ascii'
|
||||||
|
case 'ply':
|
||||||
|
return 'ascii'
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
skip: true,
|
||||||
|
required: (commandContext) =>
|
||||||
|
['gltf', 'stl', 'ply'].includes(
|
||||||
|
commandContext.argumentsToSubmit.type as string
|
||||||
|
),
|
||||||
|
options: (commandContext) => {
|
||||||
|
const type = commandContext.argumentsToSubmit.type as
|
||||||
|
| OutputTypeKey
|
||||||
|
| undefined
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'gltf':
|
||||||
|
return [
|
||||||
|
{ name: 'embedded', isCurrent: true, value: 'embedded' },
|
||||||
|
{ name: 'binary', isCurrent: false, value: 'binary' },
|
||||||
|
{ name: 'standard', isCurrent: false, value: 'standard' },
|
||||||
|
]
|
||||||
|
case 'stl':
|
||||||
|
return [
|
||||||
|
{ name: 'binary', isCurrent: false, value: 'binary' },
|
||||||
|
{ name: 'ascii', isCurrent: true, value: 'ascii' },
|
||||||
|
]
|
||||||
|
case 'ply':
|
||||||
|
return [
|
||||||
|
{ name: 'ascii', isCurrent: true, value: 'ascii' },
|
||||||
|
{
|
||||||
|
name: 'binary_big_endian',
|
||||||
|
isCurrent: false,
|
||||||
|
value: 'binary_big_endian',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'binary_little_endian',
|
||||||
|
isCurrent: false,
|
||||||
|
value: 'binary_little_endian',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
default:
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
Extrude: {
|
Extrude: {
|
||||||
description: 'Pull a sketch into 3D along its normal or perpendicular.',
|
description: 'Pull a sketch into 3D along its normal or perpendicular.',
|
||||||
icon: 'extrude',
|
icon: 'extrude',
|
||||||
|
@ -41,8 +41,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
|||||||
baseUnit: {
|
baseUnit: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.baseUnit,
|
defaultValueFromContext: (context) => context.baseUnit,
|
||||||
options: (context) =>
|
options: [],
|
||||||
|
optionsFromContext: (context) =>
|
||||||
Object.values(baseUnitsUnion).map((v) => ({
|
Object.values(baseUnitsUnion).map((v) => ({
|
||||||
name: v,
|
name: v,
|
||||||
value: v,
|
value: v,
|
||||||
@ -57,8 +58,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
|||||||
cameraControls: {
|
cameraControls: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.cameraControls,
|
defaultValueFromContext: (context) => context.cameraControls,
|
||||||
options: (context) =>
|
options: [],
|
||||||
|
optionsFromContext: (context) =>
|
||||||
Object.values(cameraSystems).map((v) => ({
|
Object.values(cameraSystems).map((v) => ({
|
||||||
name: v,
|
name: v,
|
||||||
value: v,
|
value: v,
|
||||||
@ -74,7 +76,7 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
|||||||
defaultProjectName: {
|
defaultProjectName: {
|
||||||
inputType: 'string',
|
inputType: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.defaultProjectName,
|
defaultValueFromContext: (context) => context.defaultProjectName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -84,8 +86,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
|||||||
textWrapping: {
|
textWrapping: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.textWrapping,
|
defaultValueFromContext: (context) => context.textWrapping,
|
||||||
options: (context) => [
|
options: [],
|
||||||
|
optionsFromContext: (context) => [
|
||||||
{
|
{
|
||||||
name: 'On',
|
name: 'On',
|
||||||
value: 'On' as Toggle,
|
value: 'On' as Toggle,
|
||||||
@ -106,8 +109,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
|||||||
theme: {
|
theme: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.theme,
|
defaultValueFromContext: (context) => context.theme,
|
||||||
options: (context) =>
|
options: [],
|
||||||
|
optionsFromContext: (context) =>
|
||||||
Object.values(Themes).map((v) => ({
|
Object.values(Themes).map((v) => ({
|
||||||
name: v,
|
name: v,
|
||||||
value: v,
|
value: v,
|
||||||
@ -122,8 +126,9 @@ export const settingsCommandBarConfig: CommandSetConfig<
|
|||||||
unitSystem: {
|
unitSystem: {
|
||||||
inputType: 'options',
|
inputType: 'options',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: (context) => context.unitSystem,
|
defaultValueFromContext: (context) => context.unitSystem,
|
||||||
options: (context) => [
|
options: [],
|
||||||
|
optionsFromContext: (context) => [
|
||||||
{
|
{
|
||||||
name: 'Imperial',
|
name: 'Imperial',
|
||||||
value: 'imperial' as UnitSystem,
|
value: 'imperial' as UnitSystem,
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
} from 'xstate'
|
} from 'xstate'
|
||||||
import { Selection } from './selections'
|
import { Selection } from './selections'
|
||||||
import { Identifier, Value, VariableDeclaration } from 'lang/wasm'
|
import { Identifier, Value, VariableDeclaration } from 'lang/wasm'
|
||||||
|
import { commandBarMachine } from 'machines/commandBarMachine'
|
||||||
|
|
||||||
type Icon = CustomIconName
|
type Icon = CustomIconName
|
||||||
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
const PLATFORMS = ['both', 'web', 'desktop'] as const
|
||||||
@ -93,15 +94,31 @@ export type CommandArgumentConfig<
|
|||||||
> =
|
> =
|
||||||
| {
|
| {
|
||||||
description?: string
|
description?: string
|
||||||
required: boolean
|
required:
|
||||||
skip?: true
|
| boolean
|
||||||
|
| ((
|
||||||
|
commandBarContext: { argumentsToSubmit: Record<string, unknown> } // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||||
|
) => boolean)
|
||||||
|
skip?: boolean
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'options'>
|
inputType: Extract<CommandInputType, 'options'>
|
||||||
options:
|
options:
|
||||||
| CommandArgumentOption<OutputType>[]
|
| CommandArgumentOption<OutputType>[]
|
||||||
| ((context: ContextFrom<T>) => CommandArgumentOption<OutputType>[])
|
| ((
|
||||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
commandBarContext: {
|
||||||
|
argumentsToSubmit: Record<string, unknown>
|
||||||
|
} // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||||
|
) => CommandArgumentOption<OutputType>[])
|
||||||
|
optionsFromContext?: (
|
||||||
|
context: ContextFrom<T>
|
||||||
|
) => CommandArgumentOption<OutputType>[]
|
||||||
|
defaultValue?:
|
||||||
|
| OutputType
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>
|
||||||
|
) => OutputType)
|
||||||
|
defaultValueFromContext?: (context: ContextFrom<T>) => OutputType
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'selection'>
|
inputType: Extract<CommandInputType, 'selection'>
|
||||||
@ -111,7 +128,12 @@ export type CommandArgumentConfig<
|
|||||||
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'string'>
|
inputType: Extract<CommandInputType, 'string'>
|
||||||
defaultValue?: OutputType | ((context: ContextFrom<T>) => OutputType)
|
defaultValue?:
|
||||||
|
| OutputType
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>
|
||||||
|
) => OutputType)
|
||||||
|
defaultValueFromContext?: (context: ContextFrom<T>) => OutputType
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -121,24 +143,42 @@ export type CommandArgument<
|
|||||||
> =
|
> =
|
||||||
| {
|
| {
|
||||||
description?: string
|
description?: string
|
||||||
required: boolean
|
required:
|
||||||
skip?: true
|
| boolean
|
||||||
|
| ((
|
||||||
|
commandBarContext: { argumentsToSubmit: Record<string, unknown> } // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||||
|
) => boolean)
|
||||||
|
skip?: boolean
|
||||||
|
machineActor: InterpreterFrom<T>
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'options'>
|
inputType: Extract<CommandInputType, 'options'>
|
||||||
options: CommandArgumentOption<OutputType>[]
|
options:
|
||||||
defaultValue?: OutputType
|
| CommandArgumentOption<OutputType>[]
|
||||||
|
| ((
|
||||||
|
commandBarContext: {
|
||||||
|
argumentsToSubmit: Record<string, unknown>
|
||||||
|
} // Should be the commandbarMachine's context, but it creates a circular dependency
|
||||||
|
) => CommandArgumentOption<OutputType>[])
|
||||||
|
defaultValue?:
|
||||||
|
| OutputType
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>
|
||||||
|
) => OutputType)
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'selection'>
|
inputType: Extract<CommandInputType, 'selection'>
|
||||||
selectionTypes: Selection['type'][]
|
selectionTypes: Selection['type'][]
|
||||||
actor: InterpreterFrom<T>
|
|
||||||
multiple: boolean
|
multiple: boolean
|
||||||
}
|
}
|
||||||
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
| { inputType: Extract<CommandInputType, 'kcl'>; defaultValue?: string } // KCL expression inputs have simple strings as default values
|
||||||
| {
|
| {
|
||||||
inputType: Extract<CommandInputType, 'string'>
|
inputType: Extract<CommandInputType, 'string'>
|
||||||
defaultValue?: OutputType
|
defaultValue?:
|
||||||
|
| OutputType
|
||||||
|
| ((
|
||||||
|
commandBarContext: ContextFrom<typeof commandBarMachine>
|
||||||
|
) => OutputType)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ interface CreateMachineCommandProps<
|
|||||||
ownerMachine: T['id']
|
ownerMachine: T['id']
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
send: Function
|
send: Function
|
||||||
actor?: InterpreterFrom<T>
|
actor: InterpreterFrom<T>
|
||||||
commandBarConfig?: CommandSetConfig<T, S>
|
commandBarConfig?: CommandSetConfig<T, S>
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
}
|
}
|
||||||
@ -91,13 +91,13 @@ function buildCommandArguments<
|
|||||||
>(
|
>(
|
||||||
state: StateFrom<T>,
|
state: StateFrom<T>,
|
||||||
args: CommandConfig<T, CommandName, S>['args'],
|
args: CommandConfig<T, CommandName, S>['args'],
|
||||||
actor?: InterpreterFrom<T>
|
machineActor: InterpreterFrom<T>
|
||||||
): NonNullable<Command<T, CommandName, S>['args']> {
|
): NonNullable<Command<T, CommandName, S>['args']> {
|
||||||
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
|
const newArgs = {} as NonNullable<Command<T, CommandName, S>['args']>
|
||||||
|
|
||||||
for (const arg in args) {
|
for (const arg in args) {
|
||||||
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
|
const argConfig = args[arg] as CommandArgumentConfig<S[typeof arg], T>
|
||||||
const newArg = buildCommandArgument(argConfig, arg, state, actor)
|
const newArg = buildCommandArgument(argConfig, arg, state, machineActor)
|
||||||
newArgs[arg] = newArg
|
newArgs[arg] = newArg
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,44 +111,36 @@ function buildCommandArgument<
|
|||||||
arg: CommandArgumentConfig<O, T>,
|
arg: CommandArgumentConfig<O, T>,
|
||||||
argName: string,
|
argName: string,
|
||||||
state: StateFrom<T>,
|
state: StateFrom<T>,
|
||||||
actor?: InterpreterFrom<T>
|
machineActor: InterpreterFrom<T>
|
||||||
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
|
||||||
const baseCommandArgument = {
|
const baseCommandArgument = {
|
||||||
description: arg.description,
|
description: arg.description,
|
||||||
required: arg.required,
|
required: arg.required,
|
||||||
skip: arg.skip,
|
skip: arg.skip,
|
||||||
|
machineActor,
|
||||||
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
} satisfies Omit<CommandArgument<O, T>, 'inputType'>
|
||||||
|
|
||||||
if (arg.inputType === 'options') {
|
if (arg.inputType === 'options') {
|
||||||
const options = arg.options
|
if (!arg.options) {
|
||||||
? arg.options instanceof Function
|
|
||||||
? arg.options(state.context)
|
|
||||||
: arg.options
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
if (!options) {
|
|
||||||
throw new Error('Options must be provided for options input type')
|
throw new Error('Options must be provided for options input type')
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
defaultValue:
|
defaultValue: arg.defaultValueFromContext
|
||||||
arg.defaultValue instanceof Function
|
? arg.defaultValueFromContext(state.context)
|
||||||
? arg.defaultValue(state.context)
|
: arg.defaultValue,
|
||||||
: arg.defaultValue,
|
options: arg.optionsFromContext
|
||||||
options,
|
? arg.optionsFromContext(state.context)
|
||||||
|
: arg.options,
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'options' }
|
} satisfies CommandArgument<O, T> & { inputType: 'options' }
|
||||||
} else if (arg.inputType === 'selection') {
|
} else if (arg.inputType === 'selection') {
|
||||||
if (!actor)
|
|
||||||
throw new Error('Actor must be provided for selection input type')
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
multiple: arg.multiple,
|
multiple: arg.multiple,
|
||||||
selectionTypes: arg.selectionTypes,
|
selectionTypes: arg.selectionTypes,
|
||||||
actor,
|
|
||||||
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
} satisfies CommandArgument<O, T> & { inputType: 'selection' }
|
||||||
} else if (arg.inputType === 'kcl') {
|
} else if (arg.inputType === 'kcl') {
|
||||||
return {
|
return {
|
||||||
@ -159,10 +151,7 @@ function buildCommandArgument<
|
|||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
inputType: arg.inputType,
|
inputType: arg.inputType,
|
||||||
defaultValue:
|
defaultValue: arg.defaultValue,
|
||||||
arg.defaultValue instanceof Function
|
|
||||||
? arg.defaultValue(state.context)
|
|
||||||
: arg.defaultValue,
|
|
||||||
...baseCommandArgument,
|
...baseCommandArgument,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
27
src/lib/exportFromEngine.ts
Normal file
27
src/lib/exportFromEngine.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { engineCommandManager } from 'lang/std/engineConnection'
|
||||||
|
import { type Models } from '@kittycad/lib'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
// Isolating a function to call the engine to export the current scene.
|
||||||
|
// Because it has given us trouble in automated testing environments.
|
||||||
|
export function exportFromEngine({
|
||||||
|
source_unit,
|
||||||
|
format,
|
||||||
|
}: {
|
||||||
|
source_unit: Models['UnitLength_type']
|
||||||
|
format: Models['OutputFormat_type']
|
||||||
|
}) {
|
||||||
|
return engineCommandManager.sendSceneCommand({
|
||||||
|
type: 'modeling_cmd_req',
|
||||||
|
cmd: {
|
||||||
|
type: 'export',
|
||||||
|
// By default let's leave this blank to export the whole scene.
|
||||||
|
// In the future we might want to let the user choose which entities
|
||||||
|
// in the scene to export. In that case, you'd pass the IDs thru here.
|
||||||
|
entity_ids: [],
|
||||||
|
format,
|
||||||
|
source_unit,
|
||||||
|
},
|
||||||
|
cmd_id: uuidv4(),
|
||||||
|
})
|
||||||
|
}
|
@ -8,21 +8,29 @@ import {
|
|||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
|
||||||
|
|
||||||
|
export type CommandBarContext = {
|
||||||
|
commands: Command[]
|
||||||
|
selectedCommand?: Command
|
||||||
|
currentArgument?: CommandArgument<unknown> & { name: string }
|
||||||
|
selectionRanges: Selections
|
||||||
|
argumentsToSubmit: { [x: string]: unknown }
|
||||||
|
}
|
||||||
|
|
||||||
export const commandBarMachine = createMachine(
|
export const commandBarMachine = createMachine(
|
||||||
{
|
{
|
||||||
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswdJNWcNbPFLNMr5AsRFWUtJcVSdRR2VVMUmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUG1klTsSi0awQDgYWhmqkWjnUslBWhOZyegU61GuZAAghACN8-HhYH92FxAXFQAlRA5FpJ5FjFPYtNDpIpLOkEW4GNs4eJxJoGvJxOVcT82gSrj0AEpgdCoABuYC+8ogNOYI3psQmYlkGUkU2slhc6mkmUUCIyKLc4i0lkU8iUyjUHLlVIuJEkAGUwK8Pg8oDr-WQQ2HPpTzrTIkagUzEDkLHZkQK1CUtKCHAiNlsdjkVAdFEc-ed2oG8W0Xu9uD0kwDjcCEJDplUPfn+Ut7CUhXJJFo6uYbBOMtJq-jLrrnqGmy2HOE6dEGSbESoRSUHOVqpV99Zh7JR+PXPu1G7zI1vKcFwSAOJYbgACzAJAj+FIUAAruggzcLAFBvrgMBfH+JAkEBP4kP+gE4NwrYpoywiIA4qjyDIyR2LmlTSHY8LGBhyjsrm-L2No0hpHys4Kh0L7vp+36-gBQEgQAInAS4fFGiYGv8qFbjkpSWLmswMNK-LOI6UmSKouZOBo8jOPu9EBpITEfl+OCRmxiHAZIJIAO5YDEen4A8bB-twMZ-gARugPBwQhQEoRu7ZpoiyRbLm1HiJC16bLIQo7FYUrVNkKhZJ4971pp2ksZZBkcZIABqWCUJwECvhGZAQPwYCSA8GqoAA1sVGpZTlr5gCS8HsUhHljGhCTODYVissoxaWPutQInyFhctKSL8jFmwabWWmvjprGNYZsAZTVuW8HpZCfiQqCBmwlCvgAZtt6CSNV2WrfVC3uYJ66tVuqQlNsaTpKkUlCrMClqNeuibHUolTQSqoapwYAmfZTkuQmvzXcmnmpuhCB9eyOieuk1o6C4DokQjZHqKjO66C6-0dIDwOg2SBCpc10NtnDCTiqU0ICjyciyG4+RYwKpQaBmSKpE4dpE4GJMg2QqrqlqrlNch1PCR2vNSLa4rZq4eaDVyo4+jymRqJWDQ4vFj7E2AQMiwAohALmU9La4w7dHaaNhNjaBKnqsqCatqBrdTirMNiQuIgudB+bzld+DVuUhIGFTgxWlRVVUrXV4dS-qNs021iDyO9TslMoTj1LJWN6BYOsyphOhmDkcXNP603IMHoeWcni0FUVJU4GVlUnYnzbNxxdCroasMZwgWKPay0qWNYLhZ+UCIuGeyR6O47o0ZsAcG7XioN2Hl2Rxt0HbZIu0HUd3dnUne-AS1m5y+UWz7DkY7pKpsKDRoMz1MK4olO4G-3jgVAEA4CCASrWIedtvKiAWBaBgmQ6g2g0PaR00xWYSjHFPHQNFCIzk3jWRURJIAQNvlA4oFo8zWlSPUT00pVhYw9NMHWrhKwbH2GaNQgdYxNm-JDPAxCvLw1mAzTY3opJ9TqKFehqlUTMMwiUdIrNA5gMbB8IhQlh5bkWFsVwyJshYmkNYQs9DNhI2vJhFQZp+SgkDklXS+kr7wHUZA+G-UzwygUPyLB+xApFh9BaCU6hpSLGqNKDheC5yBlsfNCORlTLmTWpGaytl+G0wwhoEU7ilDWnMN4zGhRPqO0Cn1XQthVKaBsbNZK9iYlLUyhfBJKSR7OH2FYAi1oDGzFSNIIUGwZiVD0BKTCSkFCB2FiZRpIlMjgk+v2VQch0jEUKIYz+HoOSaA0IeJRO8m4OImXLTQjDYRpAFIsfckillZAUjYGipceQ6zvF4IAA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QGED2BbdBDAdhABAEJYBOAxMgDaqxgDaADALqKgAONAlgC6eo6sQAD0QBaAJwA6AGwAmAKwBmBoukAWafIAcDcSoA0IAJ6JZDaZIDs8hgzV6AjA61a1DWQF8PhtJlwFiciowUkYWJBAOWB4+AQiRBFF5CwdpcVkHS1lpVyU5QxNEh1lFGTUsrUUtOQd5SwZLLx8MbDwiUkkqGkgyAHk2MBwwwSiY-kEEswZJbPltM0U3eXLZAsRFeQcrerUHRbTFvfkmkF9WgI6u2ggyADFONv98WkowAGNufDeW-2GI0d443iiHESkktUUilkskqdiUWjWCAcDC0kjUqnElkc6lkoK0JzOT0CnWo1zIAEEIARvn48LA-uwuIC4qAEqIHJjJPJcYtxFoYdJFFjVsZEG5pqCquJxJoGvJxOUCT82sSrj0AEpgdCoABuYC+yog9OYIyZsQmYmhWzMmTqLnU0kyikRGVRbj5lg2SmUam5StpFxIkgAymBXh8HlADQGyKHw58aecGZEzUDWYgchY7CisWoSlpQQ5EVDLJJxKkdIpyypUop-ed2kHCW0Xu9uD1kwDzcCEJCtlUNgWhZZ1OlnaKEBpZJItHVzDZ5xlpPWiZdDc8w22Ow5wozosyLUiVNMSg5ytVKmfrIipzO564z2otPVpI1vKd18SAOJYbgACzAEhI3wUgoAAV3QQZuFgCg-1wGAvjAkgSCgkCSHAyCcG4TtUxZYRED2TQZGSOw80qaQ7ARCc9mmZYSksextGkNJBRXFUOh-f9AOA0CIKgmCABE4E3D5oyTE1-lww8clKBjZF2Bh5SFZwXUUyRVDzJwNE2LQzzYwNJE4gCgJwKNeMw6DJHJAB3LAYlM-AHjYMDuFjMCACN0B4NCMKgnD927dMkWSUs8yY8RISfWQshvcsrDlapshULJPHfZsDKM7iHPM-jJAANSwShOAgX9IzICB+DASQHh1VAAGsqp1Qrit-MByXQvisP8sY8ISZwbCsDllBLSwz1qRFBQsRZ5WRIVkui-TG0M39jJ4jqLNgfLmpK3hTLIQCSFQIM2EoX8ADMjvQSQmqKna2vWvyJL3HrD1SEoyzSdJUkUm9dnUtQn10aK6hkxbiU1HVODAay3M87zE1+J6UwCtN8IQUauR0OZ0ksFwUUqRFPWmUdouPXR3TBjoIahmHKQIHKuqRrtUYSaVShhLF+TkeTzBFQosVKDRM2RVInEdSmg2p6GyE1bU9R8zrsKZqSexFqQHWlHNXHzCbFhnX1+UydFkVxiXJClmGAFEIG8hmld3ZGXp7TR5C5RSCxdjlQV1tR9bqaVdhsSFxDN5AALeOrgPa3ysJgiqcCqmr6sa7bWujxXjQd5nesQZYthsblIQYJx6hUic9AsdEFT2HQzByVLmgDJaw-eSOHPTjbysq6qcFqhrrtT9sO-4ugd1NFGc4QXEPo5eVLGsFxlnKREXGnZI9HcT1mOikO0s-S5w7bqNh9j-bkKOyQTvOy6B9utOHtj7qD1V8pSyrHJZ3STY4QmjQ0R2Ww0oSjuF3u+HAqAIBwEEOlRs48nZBVENkKQNprC42qBoJ0LothaXMJ9aElRXDLj3k3VUpJIBwOfgg4oMx8y41SPUOY8p5DFk2GiKoxsoRVmhGoM2cY2zAQRngChgU0a7HZtFH0ilRp1D5usVh6JXCKD2CUdI8lQ7rlbB8chkkJ6HkxFsBeulbQZAUCwrYCiqzIhyE+fqZtMomTMg-aCwiWYEV2NOBUCghQ6EFGzYsvpwQUT5MxawFECx2JWllRxMdLI2TsrtKMTkXIuMnmeCiZYwrePMFWCKv1XZKVGroWwmxNARK4g4hWG0tp3wSSk16lQpC41ULjV8uxUjSBvFCNEDTdCOkWCY44xCGzgzAJDaGdTnaZHBADYcqg5DpCovzeRno5izA0BeUOh8o5OPgDo+BaNvqV0xNFaE7gdYTnnvkmUChdKEMyF4LwQA */
|
||||||
|
predictableActionArguments: true,
|
||||||
|
tsTypes: {} as import('./commandBarMachine.typegen').Typegen0,
|
||||||
context: {
|
context: {
|
||||||
commands: [] as Command[],
|
commands: [],
|
||||||
selectedCommand: undefined as Command | undefined,
|
selectedCommand: undefined,
|
||||||
currentArgument: undefined as
|
currentArgument: undefined,
|
||||||
| (CommandArgument<unknown> & { name: string })
|
|
||||||
| undefined,
|
|
||||||
selectionRanges: {
|
selectionRanges: {
|
||||||
otherSelections: [],
|
otherSelections: [],
|
||||||
codeBasedSelections: [],
|
codeBasedSelections: [],
|
||||||
} as Selections,
|
},
|
||||||
argumentsToSubmit: {} as { [x: string]: unknown },
|
argumentsToSubmit: {},
|
||||||
},
|
} as CommandBarContext,
|
||||||
id: 'Command Bar',
|
id: 'Command Bar',
|
||||||
initial: 'Closed',
|
initial: 'Closed',
|
||||||
states: {
|
states: {
|
||||||
@ -267,7 +275,6 @@ export const commandBarMachine = createMachine(
|
|||||||
data: { [x: string]: CommandArgumentWithName<unknown> }
|
data: { [x: string]: CommandArgumentWithName<unknown> }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
predictableActionArguments: true,
|
|
||||||
preserveActionOrder: true,
|
preserveActionOrder: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -279,28 +286,45 @@ export const commandBarMachine = createMachine(
|
|||||||
(selectedCommand?.args && event.type === 'Submit command') ||
|
(selectedCommand?.args && event.type === 'Submit command') ||
|
||||||
event.type === 'done.invoke.validateArguments'
|
event.type === 'done.invoke.validateArguments'
|
||||||
) {
|
) {
|
||||||
selectedCommand?.onSubmit(getCommandArgumentKclValuesOnly(event.data))
|
const resolvedArgs = {} as { [x: string]: unknown }
|
||||||
|
for (const [argName, argValue] of Object.entries(
|
||||||
|
getCommandArgumentKclValuesOnly(event.data)
|
||||||
|
)) {
|
||||||
|
resolvedArgs[argName] =
|
||||||
|
typeof argValue === 'function' ? argValue(context) : argValue
|
||||||
|
}
|
||||||
|
selectedCommand?.onSubmit(resolvedArgs)
|
||||||
} else {
|
} else {
|
||||||
selectedCommand?.onSubmit()
|
selectedCommand?.onSubmit()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'Set current argument to first non-skippable': assign({
|
'Set current argument to first non-skippable': assign({
|
||||||
currentArgument: (context) => {
|
currentArgument: (context, event) => {
|
||||||
const { selectedCommand } = context
|
const { selectedCommand } = context
|
||||||
if (!(selectedCommand && selectedCommand.args)) return undefined
|
if (!(selectedCommand && selectedCommand.args)) return undefined
|
||||||
|
const rejectedArg = 'data' in event && event.data.arg
|
||||||
|
|
||||||
// Find the first argument that is not to be skipped:
|
// Find the first argument that is not to be skipped:
|
||||||
// that is, the first argument that is not already in the argumentsToSubmit
|
// that is, the first argument that is not already in the argumentsToSubmit
|
||||||
// or that is not undefined, or that is not marked as "skippable".
|
// or that is not undefined, or that is not marked as "skippable".
|
||||||
// TODO validate the type of the existing arguments
|
// TODO validate the type of the existing arguments
|
||||||
let argIndex = 0
|
let argIndex = 0
|
||||||
|
|
||||||
while (argIndex < Object.keys(selectedCommand.args).length) {
|
while (argIndex < Object.keys(selectedCommand.args).length) {
|
||||||
const argName = Object.keys(selectedCommand.args)[argIndex]
|
const [argName, argConfig] = Object.entries(selectedCommand.args)[
|
||||||
|
argIndex
|
||||||
|
]
|
||||||
|
const argIsRequired =
|
||||||
|
typeof argConfig.required === 'function'
|
||||||
|
? argConfig.required(context)
|
||||||
|
: argConfig.required
|
||||||
const mustNotSkipArg =
|
const mustNotSkipArg =
|
||||||
!context.argumentsToSubmit.hasOwnProperty(argName) ||
|
argIsRequired &&
|
||||||
context.argumentsToSubmit[argName] === undefined ||
|
(!context.argumentsToSubmit.hasOwnProperty(argName) ||
|
||||||
!selectedCommand.args[argName].skip
|
context.argumentsToSubmit[argName] === undefined ||
|
||||||
if (mustNotSkipArg) {
|
(rejectedArg && rejectedArg.name === argName))
|
||||||
|
|
||||||
|
if (mustNotSkipArg === true) {
|
||||||
return {
|
return {
|
||||||
...selectedCommand.args[argName],
|
...selectedCommand.args[argName],
|
||||||
name: argName,
|
name: argName,
|
||||||
@ -308,14 +332,10 @@ export const commandBarMachine = createMachine(
|
|||||||
}
|
}
|
||||||
argIndex++
|
argIndex++
|
||||||
}
|
}
|
||||||
// Just show the last argument if all are skippable
|
|
||||||
// TODO: use an XState service to continue onto review step
|
// TODO: use an XState service to continue onto review step
|
||||||
// if all arguments are skippable and contain values.
|
// if all arguments are skippable and contain values.
|
||||||
const argName = Object.keys(selectedCommand.args)[argIndex - 1]
|
return undefined
|
||||||
return {
|
|
||||||
...selectedCommand.args[argName],
|
|
||||||
name: argName,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
'Clear current argument': assign({
|
'Clear current argument': assign({
|
||||||
@ -333,8 +353,6 @@ export const commandBarMachine = createMachine(
|
|||||||
'Set current argument': assign({
|
'Set current argument': assign({
|
||||||
currentArgument: (context, event) => {
|
currentArgument: (context, event) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'error.platform.validateArguments':
|
|
||||||
return event.data.arg
|
|
||||||
case 'Edit argument':
|
case 'Edit argument':
|
||||||
return event.data.arg
|
return event.data.arg
|
||||||
default:
|
default:
|
||||||
@ -343,27 +361,22 @@ export const commandBarMachine = createMachine(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
'Remove current argument and set a new one': assign({
|
'Remove current argument and set a new one': assign({
|
||||||
currentArgument: (context, event) => {
|
|
||||||
if (event.type !== 'Change current argument')
|
|
||||||
return context.currentArgument
|
|
||||||
return Object.values(event.data)[0]
|
|
||||||
},
|
|
||||||
argumentsToSubmit: (context, event) => {
|
argumentsToSubmit: (context, event) => {
|
||||||
if (
|
if (
|
||||||
event.type !== 'Change current argument' ||
|
event.type !== 'Change current argument' ||
|
||||||
!context.currentArgument
|
!context.currentArgument
|
||||||
)
|
)
|
||||||
return context.argumentsToSubmit
|
return context.argumentsToSubmit
|
||||||
const { name, required } = context.currentArgument
|
const { name } = context.currentArgument
|
||||||
if (required)
|
|
||||||
return {
|
|
||||||
[name]: undefined,
|
|
||||||
...context.argumentsToSubmit,
|
|
||||||
}
|
|
||||||
|
|
||||||
const { [name]: _, ...rest } = context.argumentsToSubmit
|
const { [name]: _, ...rest } = context.argumentsToSubmit
|
||||||
return rest
|
return rest
|
||||||
},
|
},
|
||||||
|
currentArgument: (context, event) => {
|
||||||
|
if (event.type !== 'Change current argument')
|
||||||
|
return context.currentArgument
|
||||||
|
return Object.values(event.data)[0]
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
'Clear argument data': assign({
|
'Clear argument data': assign({
|
||||||
selectedCommand: undefined,
|
selectedCommand: undefined,
|
||||||
@ -388,11 +401,6 @@ export const commandBarMachine = createMachine(
|
|||||||
}),
|
}),
|
||||||
'Initialize arguments to submit': assign({
|
'Initialize arguments to submit': assign({
|
||||||
argumentsToSubmit: (c, e) => {
|
argumentsToSubmit: (c, e) => {
|
||||||
if (
|
|
||||||
e.type !== 'Select command' &&
|
|
||||||
e.type !== 'Find and select command'
|
|
||||||
)
|
|
||||||
return c.argumentsToSubmit
|
|
||||||
const command =
|
const command =
|
||||||
'command' in e.data ? e.data.command : c.selectedCommand!
|
'command' in e.data ? e.data.command : c.selectedCommand!
|
||||||
if (!command.args) return {}
|
if (!command.args) return {}
|
||||||
@ -421,38 +429,67 @@ export const commandBarMachine = createMachine(
|
|||||||
},
|
},
|
||||||
'Validate all arguments': (context, _) => {
|
'Validate all arguments': (context, _) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
for (const [argName, arg] of Object.entries(
|
for (const [argName, argConfig] of Object.entries(
|
||||||
context.argumentsToSubmit
|
context.selectedCommand!.args!
|
||||||
)) {
|
)) {
|
||||||
let argConfig = context.selectedCommand!.args![argName]
|
let arg = context.argumentsToSubmit[argName]
|
||||||
|
let argValue = typeof arg === 'function' ? arg(context) : arg
|
||||||
|
|
||||||
if (
|
try {
|
||||||
('defaultValue' in argConfig &&
|
const isRequired =
|
||||||
argConfig.defaultValue &&
|
typeof argConfig.required === 'function'
|
||||||
typeof arg !== typeof argConfig.defaultValue &&
|
? argConfig.required(context)
|
||||||
argConfig.inputType !== 'kcl') ||
|
: argConfig.required
|
||||||
(argConfig.inputType === 'kcl' &&
|
|
||||||
!(arg as Partial<KclCommandValue>).valueAst) ||
|
|
||||||
('options' in argConfig &&
|
|
||||||
typeof arg !== typeof argConfig.options[0].value)
|
|
||||||
) {
|
|
||||||
return reject({
|
|
||||||
message: 'Argument payload is of the wrong type',
|
|
||||||
arg: {
|
|
||||||
...argConfig,
|
|
||||||
name: argName,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!arg && argConfig.required) {
|
const resolvedDefaultValue =
|
||||||
return reject({
|
'defaultValue' in argConfig
|
||||||
message: 'Argument payload is falsy but is required',
|
? typeof argConfig.defaultValue === 'function'
|
||||||
arg: {
|
? argConfig.defaultValue(context)
|
||||||
...argConfig,
|
: argConfig.defaultValue
|
||||||
name: argName,
|
: undefined
|
||||||
},
|
|
||||||
})
|
const hasMismatchedDefaultValueType =
|
||||||
|
isRequired &&
|
||||||
|
typeof argValue !== typeof resolvedDefaultValue &&
|
||||||
|
!(argConfig.inputType === 'kcl' || argConfig.skip)
|
||||||
|
const hasInvalidKclValue =
|
||||||
|
argConfig.inputType === 'kcl' &&
|
||||||
|
!(argValue as Partial<KclCommandValue> | undefined)?.valueAst
|
||||||
|
const hasInvalidOptionsValue =
|
||||||
|
isRequired &&
|
||||||
|
'options' in argConfig &&
|
||||||
|
!(
|
||||||
|
typeof argConfig.options === 'function'
|
||||||
|
? argConfig.options(context)
|
||||||
|
: argConfig.options
|
||||||
|
).some((o) => o.value === argValue)
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasMismatchedDefaultValueType ||
|
||||||
|
hasInvalidKclValue ||
|
||||||
|
hasInvalidOptionsValue
|
||||||
|
) {
|
||||||
|
return reject({
|
||||||
|
message: 'Argument payload is of the wrong type',
|
||||||
|
arg: {
|
||||||
|
...argConfig,
|
||||||
|
name: argName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!argValue && isRequired) {
|
||||||
|
return reject({
|
||||||
|
message: 'Argument payload is falsy but is required',
|
||||||
|
arg: {
|
||||||
|
...argConfig,
|
||||||
|
name: argName,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error validating argument', context, e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +104,7 @@ export type ModelingMachineEvent =
|
|||||||
| { type: 'Constrain parallel' }
|
| { type: 'Constrain parallel' }
|
||||||
| { type: 'Constrain remove constraints' }
|
| { type: 'Constrain remove constraints' }
|
||||||
| { type: 'Re-execute' }
|
| { type: 'Re-execute' }
|
||||||
|
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
|
||||||
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
||||||
| { type: 'Equip Line tool' }
|
| { type: 'Equip Line tool' }
|
||||||
| { type: 'Equip tangential arc to' }
|
| { type: 'Equip tangential arc to' }
|
||||||
@ -119,7 +120,7 @@ export type MoveDesc = { line: number; snippet: string }
|
|||||||
|
|
||||||
export const modelingMachine = createMachine(
|
export const modelingMachine = createMachine(
|
||||||
{
|
{
|
||||||
/** @xstate-layout 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-McBNlhGQjynZHZSmibF5HsGoKFmKgKi6KcCAA */
|
/** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8KElsCEwwHz9A4NCuXAjeGI5B3kTBLWFJDWV5hVSZZTrlBUKjS1FZiUrDeWVDasaQdxb8ds7ugFFcdjAAJ0CAaz9yAAthqNG4iaF07bCNLiRYlGiZUyZDbFYQzfIaLSQ5Sw0TGZQnM6eNodLoEW73J6wV7sD5UQyRZisMbcP4IQRrGiSdRmfIqUxrCrQlbKcoQ4RHDTWUx5DHNLGXXHXPjsB4AVwwX0psXGCSEFThkNEymFGlECkM0MM+0ZMgUa1MwuypgRhlFHlaEpufBYD3YiuiVN+qrpdg0klMwkFaU0ermwuhwlUkiBhxUyKUazt5za3mJH2IZEomFTb0+9BGnpVoESRrNkmMga0ahMplkhqOZVL+sM4ksCj1SfFOZJ70k3Y+AEkrj0AkEugNwvnvoWad7DIGyuqOe2rPNDRCLIKaKJa6JBXrRJ2Hf3eyeh7jkCRXn0oABbMB3fwAN0enHIJEw7p+Rf4RhkpkbapNEjTRUgKfQjA0FsUnBOQJHyJQNCPC4Tz7NN3nPbpL2vII7wfAJ3lQB5sAAL24dgPy-Gd4mLP9A0kVQzW3I18mRGRDXEI1yl1LJDBWYwKhkZCU3QtDc0w4huFgGUSDwfxCOIsi7g-fwIGwaTMzAKjlVnWiECsPdNzSGgqnSAD1GhNJ2QsCpRHBXUAJbYSxJ7FzB2HIgpJkuSX1dbB30wVT1IoigtKnJVqRo399OWBR-TyFdlCgtRBUs1lymsAVYRjPdnNQs8PK8h5ZNwfwAEEACFvH8AANbTItpKx21mcQxEMGxOP-NJLJ1eL41gndagaVxTjFY9RIK3FPNwaTirkyrqoATXqr09Ka7ZrDsZQ5H2ZQtXYiDijUOKgVsvi1ErPKJvQiTptmkr-DIKAuhWn8S3SKQQRBU15H1YRTEsuy4QPbKtX+gMrtzNyMMKmbvNKrp8HYPMKQ9HSove1rpD21QANkTjREsw4Tu1KD-oRBNhEh1zJu6O74f8JhHiZ3A1PIWVMBIJ41I00LXt06KrFNWYLU6jIjlUZRLP1P1+SqAMgQRDQxGpj5oduoqHoU0jyI-TA9EenAoCGcK0YaudlhkJlQ3bAEMhWQ1UR5f9Q1hfItRKVXTxu2H7p819-L1g2P2wY3+YxujzByU1-qdsxUUNbbtj1AMqjWf9HGGpp7RQ67xN9hnYFwEgmH8dhUFq8PGs4qRlhBNQA2qMxLOyK2ZDkeZ5m3Rx2699WC7m0qi5LsuK+W03vwF97oMyfjYVa4wNEs9qZj3Hud3nOYcj72nJLhwf-DAABHWUVMRqBkari3a3KFf-qyPIJHAop51bFqTHBbccdMHefamzW5JMC5nragE9qLV3yBYdu-EdzpE3oaWsMwTCcUOBaTIahDwjUxONKGu96YHweGAW8qAXz+HIAAu4sAr5rWsHFQ41h9qaH2DoQ6RxOIWD4rYbIYIzRCSwWNXOuC-7dAAEpgEEGAPgIRZT3GoYLOoUgWyGFrKkdIYgE6HXUFbdIqRkSdwTLafhOcRJCPzpKE+2BS4ABk8BgFHqgT8YD0aNQUNZCQKgbDCnSBZTRdReSK3bnuPaRpf5mJuBY0uIUYB3GwCpLm5BR5yMSFtOKIIPZ8V+go+slhoxrFUJkhESU+5lQAO6yQIkRHWylAo8xCpQfweAABmqACAQG4GAdouAnyoFeJIGA7BBDayUhRTAggmmoCSYgYyfpCkZADGYBW4hLJQXMO2U0aROEdypkY5M0NJClPKfJSpwyVK1M0g03AzSCCPAeERSQTBObsGaQ8W8fS-CDOObrUZ4zJkIGMtsI0NoWy6lNAGSy31Nw0FUCYPIkZDHZ12ahA5HBnwBwCkFXm9TxmtPaZ07pvT+mCF8m+D8YzLkTKcebPSGyrYtkUPYZEyj8hpSUEyMQcg7ClCBMUspKLiWBxqcFc52Kbl3IeSQJ5RFXmEv5QFMlzTfk0qZNuMQAExCaDSoKcsONVFaOZDyw5C1aoXKuW03AHS8D4o6YSkgAAjWAgg+DyopajSeEc-koNyTIWCcg6jrEOv+ba5YMhQSSrIbatQDUoqNTVE1LTRUPHuY8550r3l2odU6n5lLVrRQ2TMHIIIlA7m3FoHqfFpB6j2pxbIgYf47K7KJZFAQjWLTjTi81eKenWrTfawQehnWKs9XUeQFRbCIN1NCVsyIRauLsnqVsuV604Nck28qVV-CtpFQ8W5ibxWSpeW8gZ6a+0DuzW9KZnEkGsVassFYobJ18T9FC9k20do3qjQEJ6XQ21motV0rth7BBfvEVm114DvR5pSEaOocgLQaKKHBHkKi7JGgqGaWEH7Hr4G-VundSaJUpsA8B09YHnEQc4idbITUrAN3nIDec5YWzyFSHO5Y2yEUNqhqu8+yMf24stQBwlPH3gkYLGR6lBMWpWTJluZlAaOQWBWJelQ84rCYeE22hN+H92poGcJ0T05xO5s9cYGF+o1jKwOghpKVt5i0OtGIFE6Il2CJXbygITMHgszZhzLmGK6l2Oxb+ztBL3mee8-5XzDxBBnNCgZiKObkmW3ikoawr706EwDdUbYC85h7Tg76zD4WHw+c5tzIVoVNPbrFcmqVgHius0i2VmLFXKDxbNolqZyXhSpbmNA7LllsrSH5HlncwpCsuZMa5Gx5r7GYBHH0cccRFXeqjjRo4mQjh5CWQGgM5YesmVoYxPhHHl1qxm3Y8uDi8QRLLk9fCsTArxMSWeqeUzYR+n2GYcE6h5ZS0OgrFqrVHBZHZH3C7c3JADlwBwAgK2VBMjSRt60sGuT6kkMGWskJWr-TSOD2xkPoew7JKRqlub8hWzsqaR9NHtQRn+rMFYEhbBakhNufHs2ruYEkLgKVH4FtjhCMt177rrC1GkKafUvCMhan+0UJKMwFzKz1O1KCnEOeXdQA4yQAA5CuAAFVAeB2CwAIGVCAEBAgUVdIzI3dxFXzD9JqfLgpFhJS5NqFqqcsiKCUAoDXkO9f+EN8b03pBQqONJ51-SmhzAmGDKtrQep-VFF4iLVBpRTQrD7kT9gcOReNUsDyJPWQVDbkT4aYEjPIzLChboqCOeYd55J2JsnJZmJMkhJCOYshwTtUrwBaMts5irg6s507rm1YABV7sxLiQ8BJ5cBf9GF1H89fztrmG1EoLxh3-yV6gkyR93rLC6g7JNvZM-8APfn4vlp1xbsQ65w7hE79FA7jsttaE1oeRuzSMxqFOYdjUaYxPZWUVmCuDSV0E8AAeVwHbT-StX2W8Cn0EHALaUECgPYFgJNjXze30nxkZxMF1BMkyEWGfkQB3DihXDQzpU8UwQnymw+H8F538EaRIEoB6CW1UjAHYI5g805nNV+UEHanMDSRhSNEDAkGUS5DkH9BsEODmEsDgmAOwQuDIGwFvAlVaFHkZkEO6GCwE16Q0K0PuEEHLkEHYMoF+QqEAjIOyB9ShEOm2lSSsDBFBUDHUGchMO0PwF0PFXNWXyW0GEVSBEYw5R3EYkDGhDXnLFWDsHBBWBqG8Jh1MJ0PLkPj4GCh0KJFzAQJCw6R8LMIsNyJ7BsL2gYisFqAtHbjSFhGhCghywSkAN9BcKQhOF5wwHgCiDUKgFb2j0EEWDKFyEUDL3UC0B2yKEGJNC1EsFqCFGMDNGchxDAH6PX3pC0HKGRHmRDGAmhHUEXFbH1GkOMmUS9jWPwPZDihLxMHyQr0OiQQWGtEGlsHmAwwv3ymEQuPdRbAtAxySn1GB19UyxfiZTiNalSGqAAmhMwyGS+X82FXJW+JcTWFmDmUUH5BqFSHBWSABFbChW1HyHhRAMRUbXc1RT8nRViyxSRMMzbyMC0W1T7wUU3wNADTkEZFSCghlnmFsMwxjTjWRLnFcSkDUE6lrG2lMwBl23YV1HjFUHOj1BOxJM4zc0NXXU3VpIS3XyOGSAShyA21SkOinQ2jghHSqChMw2A0FLpOj11O2BWHshKCqDyFrEBjUCZGDAqFUBrzqHUwfAvneBtO1PwN1MZAyFWBVwlLXHkwR3+mUUcG2LlKzhVLO17FXQa1Kz82pMCy1I6x1PbF-3ZDslAgUJKEG3RzlnbAAnVDBw+NEify10wCFOpXsAsCGnbBsHUDWBiKjG5MEmQVtnH1TMn17EbO11zxbNzX2DKEyBKE7ItJ7OcOSD4jvR72721ADy5x5z52bNtPXyFHzXnn-GrFkCs0QDTwUOUUzzNFkC3KbN1wNztxNynOSV1FriUHan1HjxUEmIvKDWp2gyOEjC0GJN6L2UnP3NDO3C311EFAjJ3371YQbEYwOJbDbHP0YMv1n04Fv1HlfKmVDGjBjBUB+nBnXDyH+LNA5AfgMj7nQMgOt2wPQjgIIv0nNHKDqCTyZ1S2lKKCCUZyZTP3jC1C9hYIrisNWKgvdSmDkIkBWVUDqH+i-2cLkKgnsDnQmNNBSM0N8KgH8P0LYr4ikADG1CtHkFp1kJ5BMHUEjDXiONUIETaCKPSIrkkWyL8NKI+CMscAx1hCYlanZE5UnUjF5HsHIMswAnaKwpcr8IyICLsQAApyEmA9AABKNikQCQCXTJRiTQFhIoHhRTF9QJOlZU8C2K-S+K-Q-wZK1AVKtK2qlK9KzKsQT6OQUYvKiYydBESnHcawA0muYUFwFwIAA */
|
||||||
id: 'Modeling',
|
id: 'Modeling',
|
||||||
|
|
||||||
tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
|
tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
|
||||||
@ -170,6 +171,13 @@ export const modelingMachine = createMachine(
|
|||||||
actions: ['AST extrude'],
|
actions: ['AST extrude'],
|
||||||
internal: true,
|
internal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Export: {
|
||||||
|
target: 'idle',
|
||||||
|
internal: true,
|
||||||
|
cond: 'Has exportable geometry',
|
||||||
|
actions: 'Engine export',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
entry: 'reset client scene mouse handlers',
|
entry: 'reset client scene mouse handlers',
|
||||||
@ -530,6 +538,9 @@ export const modelingMachine = createMachine(
|
|||||||
|
|
||||||
entry: 'clientToEngine cam sync direction',
|
entry: 'clientToEngine cam sync direction',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'animating to plane (copy)': {},
|
||||||
|
'animating to plane (copy) (copy)': {},
|
||||||
},
|
},
|
||||||
|
|
||||||
initial: 'idle',
|
initial: 'idle',
|
||||||
|
@ -72,7 +72,7 @@ const Home = () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const [state, send] = useMachine(homeMachine, {
|
const [state, send, actor] = useMachine(homeMachine, {
|
||||||
context: {
|
context: {
|
||||||
projects: loadedProjects,
|
projects: loadedProjects,
|
||||||
defaultProjectName,
|
defaultProjectName,
|
||||||
@ -176,6 +176,7 @@ const Home = () => {
|
|||||||
send,
|
send,
|
||||||
state,
|
state,
|
||||||
commandBarConfig: homeCommandBarConfig,
|
commandBarConfig: homeCommandBarConfig,
|
||||||
|
actor,
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -21,7 +21,7 @@ export default function Export() {
|
|||||||
<section className="flex-1">
|
<section className="flex-1">
|
||||||
<h2 className="text-2xl font-bold">Export</h2>
|
<h2 className="text-2xl font-bold">Export</h2>
|
||||||
<p className="my-4">
|
<p className="my-4">
|
||||||
Try opening the project menu and clicking "Export Model".
|
Try opening the project menu and clicking "Export Part".
|
||||||
</p>
|
</p>
|
||||||
<p className="my-4">
|
<p className="my-4">
|
||||||
{APP_NAME} uses{' '}
|
{APP_NAME} uses{' '}
|
||||||
|
Reference in New Issue
Block a user