diff --git a/e2e/playwright/point-click.spec.ts b/e2e/playwright/point-click.spec.ts index daf5d83a4..3afce1112 100644 --- a/e2e/playwright/point-click.spec.ts +++ b/e2e/playwright/point-click.spec.ts @@ -1503,6 +1503,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { await clickOnCap() await page.waitForTimeout(500) await cmdBar.progressCmdBar() + await page.waitForTimeout(500) await cmdBar.progressCmdBar() await cmdBar.expectState({ stage: 'review', @@ -1523,6 +1524,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => { await toolbar.shellButton.click() await cmdBar.progressCmdBar() + await page.waitForTimeout(500) await cmdBar.progressCmdBar() await cmdBar.expectState({ stage: 'review', @@ -1604,6 +1606,7 @@ extrude001 = extrude(40, sketch001) await page.waitForTimeout(500) await page.keyboard.up('Shift') await cmdBar.progressCmdBar() + await page.waitForTimeout(500) await cmdBar.progressCmdBar() await cmdBar.expectState({ stage: 'review', @@ -1727,3 +1730,61 @@ shellSketchOnFacesCases.forEach((initialCode, index) => { }) }) }) + +test(`Shell dry-run validation rejects sweeps`, async ({ + context, + page, + homePage, + scene, + editor, + toolbar, + cmdBar, +}) => { + const initialCode = `sketch001 = startSketchOn('YZ') + |> circle({ + center = [0, 0], + radius = 500 + }, %) +sketch002 = startSketchOn('XZ') + |> startProfileAt([0, 0], %) + |> xLine(-2000, %) +sweep001 = sweep({ path = sketch002 }, sketch001) +` + await context.addInitScript((initialCode) => { + localStorage.setItem('persistCode', initialCode) + }, initialCode) + await page.setBodyDimensions({ width: 1000, height: 500 }) + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + // One dumb hardcoded screen pixel value + const testPoint = { x: 500, y: 250 } + const [clickOnSweep] = scene.makeMouseHelpers(testPoint.x, testPoint.y) + + await test.step(`Confirm sweep exists`, async () => { + await toolbar.closePane('code') + await scene.expectPixelColor([231, 231, 231], testPoint, 15) + }) + + await test.step(`Go through the Shell flow and fail validation with a toast`, async () => { + await toolbar.shellButton.click() + await cmdBar.expectState({ + stage: 'arguments', + currentArgKey: 'selection', + currentArgValue: '', + headerArguments: { + Selection: '', + Thickness: '', + }, + highlightedHeaderArg: 'selection', + commandName: 'Shell', + }) + await clickOnSweep() + await page.waitForTimeout(500) + await cmdBar.progressCmdBar() + await expect( + page.getByText('Unable to shell with the provided selection') + ).toBeVisible() + await page.waitForTimeout(1000) + }) +}) diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts index ffbbda3b9..8b7c9a760 100644 --- a/src/lib/commandBarConfigs/modelingCommandConfig.ts +++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts @@ -9,7 +9,11 @@ import { Selections } from 'lib/selections' import { kclManager } from 'lib/singletons' import { err } from 'lib/trap' import { modelingMachine, SketchTool } from 'machines/modelingMachine' -import { loftValidator, revolveAxisValidator } from './validators' +import { + loftValidator, + revolveAxisValidator, + shellValidator, +} from './validators' type OutputFormat = Models['OutputFormat_type'] type OutputTypeKey = OutputFormat['type'] @@ -351,12 +355,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig< selectionTypes: ['cap', 'wall'], multiple: true, required: true, - skip: false, + validation: shellValidator, }, thickness: { inputType: 'kcl', defaultValue: KCL_DEFAULT_LENGTH, required: true, + // TODO: add dry-run validation on thickness param }, }, }, diff --git a/src/lib/commandBarConfigs/validators.ts b/src/lib/commandBarConfigs/validators.ts index dd161aa85..003218771 100644 --- a/src/lib/commandBarConfigs/validators.ts +++ b/src/lib/commandBarConfigs/validators.ts @@ -153,3 +153,57 @@ export const loftValidator = async ({ return 'Unable to loft with selected sketches' } } + +export const shellValidator = async ({ + data, +}: { + data: { selection: Selections } +}): Promise => { + if (!isSelections(data.selection)) { + return 'Unable to shell, selections are missing' + } + + // No validation on the faces, filtering is done upstream and we have the dry run validation just below + const face_ids = data.selection.graphSelections.flatMap((s) => + s.artifact ? s.artifact.id : [] + ) + + // We don't have the concept of solid3ds in TS yet. + // So we're listing out the sweeps as if they were solids and taking the first one, just like in Rust for Shell: + // https://github.com/KittyCAD/modeling-app/blob/e61fff115b9fa94aaace6307b1842cc15d41655e/src/wasm-lib/kcl/src/std/shell.rs#L237-L238 + // TODO: This is one cheap way to make sketch-on-face supported now but will likely fail multiple solids + const object_id = engineCommandManager.artifactGraph + .values() + .find((v) => v.type === 'sweep')?.pathId + + if (!object_id) { + return "Unable to shell, couldn't find the solid" + } + + const shellCommand = async () => { + // TODO: figure out something better than an arbitrarily small value + const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9 + const DEFAULT_HOLLOW = false + const cmdArgs = { + face_ids, + object_id, + hollow: DEFAULT_HOLLOW, + shell_thickness: DEFAULT_THICKNESS, + } + return await engineCommandManager.sendSceneCommand({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'solid3d_shell_face', + ...cmdArgs, + }, + }) + } + + const attemptShell = await dryRunWrapper(shellCommand) + if (attemptShell?.success) { + return true + } + + return 'Unable to shell with the provided selection' +}