Remove Create with Text-to-CAD from toolbar, make commands in command palette more distinct (#7048)
* Remove Create with Text-to-CAD from the toolbar * Remove "prompt-to-edit" wording, call commands "create" and "edit" * Use sparkles for the ML feature, not chat * lints * Start fixing up tests, there are probably more though * Fix up a few more tests * Fix up prompt-to-edit tests (yay using fixtures!) * Fix native file menu tests * Update snapshots * Fix menu test * Fix snaps --------- Co-authored-by: Pierre Jacquier <pierre@zoo.dev> Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
@ -146,9 +146,7 @@ export class CmdBarFixture {
|
||||
await this.cmdBarOpenBtn.click()
|
||||
await expect(this.page.getByPlaceholder('Search commands')).toBeVisible()
|
||||
if (selectCmd === 'promptToEdit') {
|
||||
const promptEditCommand = this.page.getByText(
|
||||
'Use Zoo AI to edit your parts and code.'
|
||||
)
|
||||
const promptEditCommand = this.selectOption({ name: 'Text-to-CAD Edit' })
|
||||
await expect(promptEditCommand.first()).toBeVisible()
|
||||
await promptEditCommand.first().scrollIntoViewIfNeeded()
|
||||
await promptEditCommand.first().click()
|
||||
|
@ -121,11 +121,13 @@ export class HomePageFixture {
|
||||
await projectCard.click()
|
||||
}
|
||||
|
||||
goToModelingScene = async (name: string = 'testDefault') => {
|
||||
/** Returns the project name in case caller has used the default and needs it */
|
||||
goToModelingScene = async (name = 'testDefault') => {
|
||||
// On web this is a no-op. There is no project view.
|
||||
if (process.env.PLATFORM === 'web') return
|
||||
if (process.env.PLATFORM === 'web') return ''
|
||||
|
||||
await this.createAndGoToProject(name)
|
||||
return name
|
||||
}
|
||||
|
||||
isNativeFileMenuCreated = async () => {
|
||||
|
@ -252,7 +252,7 @@ test.describe(
|
||||
tronApp,
|
||||
'Edit.Modify with Zoo Text-To-CAD'
|
||||
)
|
||||
await cmdBar.expectCommandName('Prompt-to-edit')
|
||||
await cmdBar.expectCommandName('Text-to-CAD Edit')
|
||||
})
|
||||
await test.step('Modeling.Edit.Edit parameter', async () => {
|
||||
await page.waitForTimeout(250)
|
||||
@ -518,7 +518,7 @@ test.describe(
|
||||
'Design.Create with Zoo Text-To-CAD'
|
||||
)
|
||||
await cmdBar.toBeOpened()
|
||||
await cmdBar.expectCommandName('Text to CAD')
|
||||
await cmdBar.expectCommandName('Text-to-CAD Create')
|
||||
})
|
||||
|
||||
await test.step('Modeling.Design.Modify with Zoo Text-To-CAD', async () => {
|
||||
@ -528,7 +528,7 @@ test.describe(
|
||||
'Design.Modify with Zoo Text-To-CAD'
|
||||
)
|
||||
await cmdBar.toBeOpened()
|
||||
await cmdBar.expectCommandName('Prompt-to-edit')
|
||||
await cmdBar.expectCommandName('Text-to-CAD Edit')
|
||||
})
|
||||
|
||||
await test.step('Modeling.Help.KCL code samples', async () => {
|
||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 135 KiB |
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 134 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
@ -4,9 +4,10 @@ import type { Page } from '@playwright/test'
|
||||
|
||||
import { createProject, getUtils } from '@e2e/playwright/test-utils'
|
||||
import { expect, test } from '@e2e/playwright/zoo-test'
|
||||
import type { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
|
||||
|
||||
test.describe('Text-to-CAD tests', () => {
|
||||
test('basic lego happy case', async ({ page, homePage }) => {
|
||||
test('basic lego happy case', async ({ page, homePage, cmdBar }) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
await test.step('Set up', async () => {
|
||||
@ -15,7 +16,11 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await u.waitForPageLoad()
|
||||
})
|
||||
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
|
||||
await sendPromptFromCommandBarAndSetExistingProject(
|
||||
page,
|
||||
'a 2x4 lego',
|
||||
cmdBar
|
||||
)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -56,6 +61,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
test('success model, then ignore success toast, user can create new prompt from command bar', async ({
|
||||
page,
|
||||
homePage,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
@ -64,7 +70,11 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await homePage.goToModelingScene()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x6 lego')
|
||||
await sendPromptFromCommandBarAndSetExistingProject(
|
||||
page,
|
||||
'a 2x6 lego',
|
||||
cmdBar
|
||||
)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -82,7 +92,11 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await expect(successToastMessage).toBeVisible({ timeout: 15000 })
|
||||
|
||||
// Can send a new prompt from the command bar.
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
|
||||
await sendPromptFromCommandBarAndSetExistingProject(
|
||||
page,
|
||||
'a 2x4 lego',
|
||||
cmdBar
|
||||
)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -100,6 +114,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
test('you can reject text-to-cad output and it does nothing', async ({
|
||||
page,
|
||||
homePage,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
@ -108,7 +123,11 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await homePage.goToModelingScene()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
|
||||
await sendPromptFromCommandBarAndSetExistingProject(
|
||||
page,
|
||||
'a 2x4 lego',
|
||||
cmdBar
|
||||
)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -141,6 +160,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
test('sending a bad prompt fails, can dismiss', async ({
|
||||
page,
|
||||
homePage,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
@ -150,7 +170,11 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await u.waitForPageLoad()
|
||||
|
||||
const randomPrompt = `aslkdfja;` + Date.now() + `FFFFEIWJF`
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, randomPrompt)
|
||||
await sendPromptFromCommandBarAndSetExistingProject(
|
||||
page,
|
||||
randomPrompt,
|
||||
cmdBar
|
||||
)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -188,6 +212,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
test('sending a bad prompt fails, can start over from toast', async ({
|
||||
page,
|
||||
homePage,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
@ -197,7 +222,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await u.waitForPageLoad()
|
||||
|
||||
const badPrompt = 'akjsndladf lajbhflauweyfaaaljhr472iouafyvsssssss'
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, badPrompt)
|
||||
await sendPromptFromCommandBarAndSetExistingProject(page, badPrompt, cmdBar)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -256,6 +281,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
test('sending a bad prompt fails, can ignore toast, can start over from command bar', async ({
|
||||
page,
|
||||
homePage,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
@ -265,7 +291,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await u.waitForPageLoad()
|
||||
|
||||
const badPrompt = 'akjsndladflajbhflauweyf15;'
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, badPrompt)
|
||||
await sendPromptFromCommandBarAndSetExistingProject(page, badPrompt, cmdBar)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -292,7 +318,11 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await expect(page.getByText(`Text-to-CAD failed`)).toBeVisible()
|
||||
|
||||
// They should be able to try again from the command bar.
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
|
||||
await sendPromptFromCommandBarAndSetExistingProject(
|
||||
page,
|
||||
'a 2x4 lego',
|
||||
cmdBar
|
||||
)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -310,17 +340,40 @@ test.describe('Text-to-CAD tests', () => {
|
||||
test('ensure you can shift+enter in the prompt box', async ({
|
||||
page,
|
||||
homePage,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
const projectName = await homePage.goToModelingScene()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
const promptWithNewline = `a 2x4\nlego`
|
||||
|
||||
await page.getByTestId('text-to-cad').click()
|
||||
await test.step('Get to the prompt step to test', async () => {
|
||||
await cmdBar.openCmdBar()
|
||||
await cmdBar.selectOption({ name: 'Text-to-CAD Create' }).click()
|
||||
|
||||
await cmdBar.currentArgumentInput.fill('existing')
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
await cmdBar.currentArgumentInput.fill(projectName)
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Text-to-CAD Create',
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'prompt',
|
||||
currentArgValue: '',
|
||||
highlightedHeaderArg: 'prompt',
|
||||
headerArguments: {
|
||||
Method: 'Existing project',
|
||||
ProjectName: projectName,
|
||||
Prompt: '',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Type the prompt.
|
||||
await page.keyboard.type('a 2x4')
|
||||
@ -354,6 +407,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
test('can do many at once and get many prompts back, and interact with many', async ({
|
||||
page,
|
||||
homePage,
|
||||
cmdBar,
|
||||
}) => {
|
||||
// Let this test run longer since we've seen it timeout.
|
||||
test.setTimeout(180_000)
|
||||
@ -365,11 +419,23 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await homePage.goToModelingScene()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
|
||||
await sendPromptFromCommandBarAndSetExistingProject(
|
||||
page,
|
||||
'a 2x4 lego',
|
||||
cmdBar
|
||||
)
|
||||
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x8 lego')
|
||||
await sendPromptFromCommandBarAndSetExistingProject(
|
||||
page,
|
||||
'a 2x8 lego',
|
||||
cmdBar
|
||||
)
|
||||
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x10 lego')
|
||||
await sendPromptFromCommandBarAndSetExistingProject(
|
||||
page,
|
||||
'a 2x10 lego',
|
||||
cmdBar
|
||||
)
|
||||
|
||||
// Find the toast.
|
||||
// Look out for the toast message
|
||||
@ -440,6 +506,7 @@ test.describe('Text-to-CAD tests', () => {
|
||||
test('can do many at once with errors, clicking dismiss error does not dismiss all', async ({
|
||||
page,
|
||||
homePage,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
@ -448,11 +515,16 @@ test.describe('Text-to-CAD tests', () => {
|
||||
await homePage.goToModelingScene()
|
||||
await u.waitForPageLoad()
|
||||
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, 'a 2x4 lego')
|
||||
|
||||
await sendPromptFromCommandBarTriggeredByButton(
|
||||
await sendPromptFromCommandBarAndSetExistingProject(
|
||||
page,
|
||||
'alkjsdnlajshdbfjlhsbdf a;askjdnf'
|
||||
'a 2x4 lego',
|
||||
cmdBar
|
||||
)
|
||||
|
||||
await sendPromptFromCommandBarAndSetExistingProject(
|
||||
page,
|
||||
'alkjsdnlajshdbfjlhsbdf a;askjdnf',
|
||||
cmdBar
|
||||
)
|
||||
|
||||
// Find the toast.
|
||||
@ -526,7 +598,9 @@ async function _sendPromptFromCommandBar(page: Page, promptStr: string) {
|
||||
const cmdSearchBar = page.getByPlaceholder('Search commands')
|
||||
await expect(cmdSearchBar).toBeVisible()
|
||||
|
||||
const textToCadCommand = page.getByText('Use the Zoo Text-to-CAD API')
|
||||
const textToCadCommand = page.getByRole('option', {
|
||||
name: 'Text-to-CAD Create',
|
||||
})
|
||||
await expect(textToCadCommand.first()).toBeVisible()
|
||||
// Click the Text-to-CAD command
|
||||
await textToCadCommand.first().scrollIntoViewIfNeeded()
|
||||
@ -544,29 +618,67 @@ async function _sendPromptFromCommandBar(page: Page, promptStr: string) {
|
||||
})
|
||||
}
|
||||
|
||||
async function sendPromptFromCommandBarTriggeredByButton(
|
||||
async function sendPromptFromCommandBarAndSetExistingProject(
|
||||
page: Page,
|
||||
promptStr: string
|
||||
promptStr: string,
|
||||
cmdBar: CmdBarFixture,
|
||||
projectName = 'testDefault'
|
||||
) {
|
||||
await page.waitForTimeout(1000)
|
||||
await test.step(`Send prompt from command bar: ${promptStr}`, async () => {
|
||||
await page.getByTestId('text-to-cad').click()
|
||||
await cmdBar.openCmdBar()
|
||||
await cmdBar.selectOption({ name: 'Text-to-CAD Create' }).click()
|
||||
|
||||
// Enter the prompt.
|
||||
const prompt = page.getByRole('textbox', { name: 'Prompt' })
|
||||
await expect(prompt.first()).toBeVisible()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Text-to-CAD Create',
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'method',
|
||||
currentArgValue: '',
|
||||
highlightedHeaderArg: 'method',
|
||||
headerArguments: {
|
||||
Method: '',
|
||||
Prompt: '',
|
||||
},
|
||||
})
|
||||
await cmdBar.currentArgumentInput.fill('existing')
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
// Type the prompt.
|
||||
await page.keyboard.type(promptStr)
|
||||
await page.waitForTimeout(200)
|
||||
await page.keyboard.press('Enter')
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Text-to-CAD Create',
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'projectName',
|
||||
currentArgValue: '',
|
||||
highlightedHeaderArg: 'projectName',
|
||||
headerArguments: {
|
||||
Method: 'Existing project',
|
||||
ProjectName: '',
|
||||
Prompt: '',
|
||||
},
|
||||
})
|
||||
await cmdBar.currentArgumentInput.fill(projectName)
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Text-to-CAD Create',
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'prompt',
|
||||
currentArgValue: '',
|
||||
highlightedHeaderArg: 'prompt',
|
||||
headerArguments: {
|
||||
Method: 'Existing project',
|
||||
ProjectName: projectName,
|
||||
Prompt: '',
|
||||
},
|
||||
})
|
||||
await cmdBar.currentArgumentInput.fill(promptStr)
|
||||
await cmdBar.progressCmdBar()
|
||||
})
|
||||
}
|
||||
|
||||
test(
|
||||
'Text-to-CAD functionality',
|
||||
{ tag: '@electron' },
|
||||
async ({ context, page }, testInfo) => {
|
||||
async ({ context, page, cmdBar }, testInfo) => {
|
||||
const projectName = 'project-000'
|
||||
const prompt = 'lego 2x4'
|
||||
const textToCadFileName = 'lego-2x4.kcl'
|
||||
@ -603,7 +715,12 @@ test(
|
||||
await openKclCodePanel()
|
||||
|
||||
await test.step(`Test file creation`, async () => {
|
||||
await sendPromptFromCommandBarTriggeredByButton(page, prompt)
|
||||
await sendPromptFromCommandBarAndSetExistingProject(
|
||||
page,
|
||||
prompt,
|
||||
cmdBar,
|
||||
projectName
|
||||
)
|
||||
// File is considered created if it shows up in the Project Files pane
|
||||
await expect(textToCadFileButton).toBeVisible({ timeout: 20_000 })
|
||||
expect(fileExists()).toBeTruthy()
|
||||
|
@ -325,7 +325,7 @@ export function Toolbar({
|
||||
// A single button
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
className={`relative ${itemConfig.alwaysDark ? ' dark bg-chalkboard-90 ' : ''}`}
|
||||
key={itemConfig.id}
|
||||
// Mouse events do not fire on disabled buttons
|
||||
onMouseEnter={handleMouseEnter}
|
||||
|
@ -110,7 +110,7 @@ export function createApplicationCommands({
|
||||
const textToCADCommand: Command = {
|
||||
name: 'Text-to-CAD',
|
||||
description: 'Generate parts from text prompts.',
|
||||
displayName: 'Text to CAD',
|
||||
displayName: 'Text-to-CAD Create',
|
||||
groupId: 'application',
|
||||
needsReview: false,
|
||||
status: IS_ML_EXPERIMENTAL ? 'experimental' : 'active',
|
||||
|
@ -939,8 +939,10 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
},
|
||||
},
|
||||
'Prompt-to-edit': {
|
||||
description: 'Use Zoo AI to edit your parts and code.',
|
||||
icon: 'chat',
|
||||
displayName: 'Text-to-CAD Edit',
|
||||
description:
|
||||
'Use machine learning to edit your parts and code from a text prompt.',
|
||||
icon: 'sparkles',
|
||||
status: IS_ML_EXPERIMENTAL ? 'experimental' : 'active',
|
||||
args: {
|
||||
selection: {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { EventFrom, StateFrom } from 'xstate'
|
||||
import { settingsActor } from '@src/lib/singletons'
|
||||
|
||||
import type { CustomIconName } from '@src/components/CustomIcon'
|
||||
import { createLiteral } from '@src/lang/create'
|
||||
@ -35,6 +34,7 @@ export type ToolbarItem = {
|
||||
id: string
|
||||
onClick: (props: ToolbarItemCallbackProps) => void
|
||||
icon?: CustomIconName
|
||||
className?: string
|
||||
iconColor?: string
|
||||
alwaysDark?: true
|
||||
status: 'available' | 'unavailable' | 'kcl-only' | 'experimental'
|
||||
@ -424,39 +424,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
],
|
||||
},
|
||||
'break',
|
||||
{
|
||||
id: 'ai',
|
||||
array: [
|
||||
{
|
||||
id: 'text-to-cad',
|
||||
onClick: () => {
|
||||
const currentProject =
|
||||
settingsActor.getSnapshot().context.currentProject
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
name: 'Text-to-CAD',
|
||||
groupId: 'application',
|
||||
argDefaultValues: {
|
||||
method: 'existingProject',
|
||||
projectName: currentProject?.name,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
icon: 'sparkles',
|
||||
iconColor: '#29FFA4',
|
||||
alwaysDark: true,
|
||||
status: IS_ML_EXPERIMENTAL ? 'experimental' : 'available',
|
||||
title: 'Create with Zoo Text-to-CAD',
|
||||
description: 'Create geometry with AI / ML.',
|
||||
links: [
|
||||
{
|
||||
label: 'API docs',
|
||||
url: 'https://zoo.dev/docs/api/ml/generate-a-cad-model-from-text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'prompt-to-edit',
|
||||
onClick: () =>
|
||||
@ -468,14 +435,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
iconColor: '#29FFA4',
|
||||
alwaysDark: true,
|
||||
status: IS_ML_EXPERIMENTAL ? 'experimental' : 'available',
|
||||
title: 'Modify with Zoo Text-to-CAD',
|
||||
description: 'Edit geometry with AI / ML.',
|
||||
title: 'Text-to-CAD',
|
||||
description:
|
||||
'Edit or create geometry from a text prompt and selection.',
|
||||
links: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
sketching: {
|
||||
check: (state) =>
|
||||
state.matches('Sketch') ||
|
||||
|