Selection dry-run validation for Shell (#4775)
* WIP: mess with shell selection validation Will eventually fix #4711 * Update from main * WIP: not working yet * Working loft dry run validator * Clean up shell (still not working) * Bump kittycad-modeling-cmds * Clean up * Add logging * Add proper object_id and face_id mapping, still not working for shell * Fix faceId * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * Trigger CI * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * Working validation after engine merge; Clean up * Fix codespell * Add pw test * More clean up * Back to basics * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * Trigger CI * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * Trigger CI * Clean up * Fix tests * A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores) * Trigger CI * Remove kcl-samples --------- Co-authored-by: Adam Chalmers <adam.chalmers@zoo.dev> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
		@ -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)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -153,3 +153,57 @@ export const loftValidator = async ({
 | 
			
		||||
    return 'Unable to loft with selected sketches'
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const shellValidator = async ({
 | 
			
		||||
  data,
 | 
			
		||||
}: {
 | 
			
		||||
  data: { selection: Selections }
 | 
			
		||||
}): Promise<boolean | string> => {
 | 
			
		||||
  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'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user