Point-and-click Sweep (first PR) (#4989)
* Refactor 'Delete selection' as actor
Will fix #4662
* WIP logging
* WIP: working Solid3dGetExtrusionFaceInfo for loft
* Working wall deletion of loft
* Add offset plane deletion
* Add feature tree deletion of shell
* Clean up
* Revert "Clean up"
This reverts commit 214763cc2b.
* Clean up rust changes, taking the sketch with the most paths
* Working cap selection and deletion
* Clean up
* Add test for loft and offset plane deletion via selection
* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-macos-8-cores)
* Set reenter: false as it was originally
* Passing test
* Add shell deletion via feature tree test
* Revert the migration to promise actor
* 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
* 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
* Use cmd.id as solid_id after latest engine merge
* Add feature tree deletion of offset plane and fix lint
* Add feature tree deletion of loft
* Clean up
* Better comment
* Lint fix
* Remove sketch sorting
* WIP: sweep point-and-click
* Working sweep
* Add test
* Make sweep a development command
* Fix tsc error
* Clean up for review
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
			
			
This commit is contained in:
		
				
					committed by
					
						
						Frank Noirot
					
				
			
			
				
	
			
			
			
						parent
						
							4f02e45da3
						
					
				
				
					commit
					339de00e68
				
			@ -14,6 +14,7 @@ export class ToolbarFixture {
 | 
			
		||||
 | 
			
		||||
  extrudeButton!: Locator
 | 
			
		||||
  loftButton!: Locator
 | 
			
		||||
  sweepButton!: Locator
 | 
			
		||||
  shellButton!: Locator
 | 
			
		||||
  offsetPlaneButton!: Locator
 | 
			
		||||
  startSketchBtn!: Locator
 | 
			
		||||
@ -40,6 +41,7 @@ export class ToolbarFixture {
 | 
			
		||||
    this.page = page
 | 
			
		||||
    this.extrudeButton = page.getByTestId('extrude')
 | 
			
		||||
    this.loftButton = page.getByTestId('loft')
 | 
			
		||||
    this.sweepButton = page.getByTestId('sweep')
 | 
			
		||||
    this.shellButton = page.getByTestId('shell')
 | 
			
		||||
    this.offsetPlaneButton = page.getByTestId('plane-offset')
 | 
			
		||||
    this.startSketchBtn = page.getByTestId('sketch')
 | 
			
		||||
 | 
			
		||||
@ -934,6 +934,104 @@ loft001 = loft([sketch001, sketch002])
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test(`Sweep point-and-click`, 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(-500, %)
 | 
			
		||||
  |> tangentialArcTo([-2000, 500], %)
 | 
			
		||||
`
 | 
			
		||||
  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: 700, y: 250 }
 | 
			
		||||
  const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
 | 
			
		||||
  const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
 | 
			
		||||
  const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
 | 
			
		||||
 | 
			
		||||
  await test.step(`Look for sketch001`, async () => {
 | 
			
		||||
    await toolbar.closePane('code')
 | 
			
		||||
    await scene.expectPixelColor([53, 53, 53], testPoint, 15)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step(`Go through the command bar flow`, async () => {
 | 
			
		||||
    await toolbar.sweepButton.click()
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      commandName: 'Sweep',
 | 
			
		||||
      currentArgKey: 'profile',
 | 
			
		||||
      currentArgValue: '',
 | 
			
		||||
      headerArguments: {
 | 
			
		||||
        Path: '',
 | 
			
		||||
        Profile: '',
 | 
			
		||||
      },
 | 
			
		||||
      highlightedHeaderArg: 'profile',
 | 
			
		||||
      stage: 'arguments',
 | 
			
		||||
    })
 | 
			
		||||
    await clickOnSketch1()
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      commandName: 'Sweep',
 | 
			
		||||
      currentArgKey: 'path',
 | 
			
		||||
      currentArgValue: '',
 | 
			
		||||
      headerArguments: {
 | 
			
		||||
        Path: '',
 | 
			
		||||
        Profile: '1 face',
 | 
			
		||||
      },
 | 
			
		||||
      highlightedHeaderArg: 'path',
 | 
			
		||||
      stage: 'arguments',
 | 
			
		||||
    })
 | 
			
		||||
    await clickOnSketch2()
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      commandName: 'Sweep',
 | 
			
		||||
      headerArguments: {
 | 
			
		||||
        Path: '1 face',
 | 
			
		||||
        Profile: '1 face',
 | 
			
		||||
      },
 | 
			
		||||
      stage: 'review',
 | 
			
		||||
    })
 | 
			
		||||
    await cmdBar.progressCmdBar()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
 | 
			
		||||
    await scene.expectPixelColor([135, 64, 73], testPoint, 15)
 | 
			
		||||
    await toolbar.openPane('code')
 | 
			
		||||
    await editor.expectEditor.toContain(sweepDeclaration)
 | 
			
		||||
    await editor.expectState({
 | 
			
		||||
      diagnostics: [],
 | 
			
		||||
      activeLines: [sweepDeclaration],
 | 
			
		||||
      highlightedCode: '',
 | 
			
		||||
    })
 | 
			
		||||
    await toolbar.closePane('code')
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step('Delete sweep via feature tree selection', async () => {
 | 
			
		||||
    await toolbar.openPane('feature-tree')
 | 
			
		||||
    await page.waitForTimeout(500)
 | 
			
		||||
    const operationButton = await toolbar.getFeatureTreeOperation('Sweep', 0)
 | 
			
		||||
    await operationButton.click({ button: 'left' })
 | 
			
		||||
    await page.keyboard.press('Backspace')
 | 
			
		||||
    await page.waitForTimeout(500)
 | 
			
		||||
    await toolbar.closePane('feature-tree')
 | 
			
		||||
    await scene.expectPixelColor([53, 53, 53], testPoint, 15)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const shellPointAndClickCapCases = [
 | 
			
		||||
  { shouldPreselect: true },
 | 
			
		||||
  { shouldPreselect: false },
 | 
			
		||||
 | 
			
		||||
@ -374,6 +374,37 @@ export function loftSketches(
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function addSweep(
 | 
			
		||||
  node: Node<Program>,
 | 
			
		||||
  profileDeclarator: VariableDeclarator,
 | 
			
		||||
  pathDeclarator: VariableDeclarator
 | 
			
		||||
): {
 | 
			
		||||
  modifiedAst: Node<Program>
 | 
			
		||||
  pathToNode: PathToNode
 | 
			
		||||
} {
 | 
			
		||||
  const modifiedAst = structuredClone(node)
 | 
			
		||||
  const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP)
 | 
			
		||||
  const sweep = createCallExpressionStdLib('sweep', [
 | 
			
		||||
    createObjectExpression({ path: createIdentifier(pathDeclarator.id.name) }),
 | 
			
		||||
    createIdentifier(profileDeclarator.id.name),
 | 
			
		||||
  ])
 | 
			
		||||
  const declaration = createVariableDeclaration(name, sweep)
 | 
			
		||||
  modifiedAst.body.push(declaration)
 | 
			
		||||
  const pathToNode: PathToNode = [
 | 
			
		||||
    ['body', ''],
 | 
			
		||||
    [modifiedAst.body.length - 1, 'index'],
 | 
			
		||||
    ['declaration', 'VariableDeclaration'],
 | 
			
		||||
    ['init', 'VariableDeclarator'],
 | 
			
		||||
    ['arguments', 'CallExpression'],
 | 
			
		||||
    [0, 'index'],
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    modifiedAst,
 | 
			
		||||
    pathToNode,
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function revolveSketch(
 | 
			
		||||
  node: Node<Program>,
 | 
			
		||||
  pathToNode: PathToNode,
 | 
			
		||||
 | 
			
		||||
@ -77,7 +77,7 @@ interface SegmentArtifactRich extends BaseArtifact {
 | 
			
		||||
/** A Sweep is a more generic term for extrude, revolve, loft and sweep*/
 | 
			
		||||
interface SweepArtifact extends BaseArtifact {
 | 
			
		||||
  type: 'sweep'
 | 
			
		||||
  subType: 'extrusion' | 'revolve' | 'loft'
 | 
			
		||||
  subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
 | 
			
		||||
  pathId: string
 | 
			
		||||
  surfaceIds: Array<string>
 | 
			
		||||
  edgeIds: Array<string>
 | 
			
		||||
@ -85,7 +85,7 @@ interface SweepArtifact extends BaseArtifact {
 | 
			
		||||
}
 | 
			
		||||
interface SweepArtifactRich extends BaseArtifact {
 | 
			
		||||
  type: 'sweep'
 | 
			
		||||
  subType: 'extrusion' | 'revolve' | 'loft'
 | 
			
		||||
  subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
 | 
			
		||||
  path: PathArtifact
 | 
			
		||||
  surfaces: Array<WallArtifact | CapArtifact>
 | 
			
		||||
  edges: Array<SweepEdge>
 | 
			
		||||
@ -377,7 +377,11 @@ export function getArtifactsToUpdate({
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
    return returnArr
 | 
			
		||||
  } else if (cmd.type === 'extrude' || cmd.type === 'revolve') {
 | 
			
		||||
  } else if (
 | 
			
		||||
    cmd.type === 'extrude' ||
 | 
			
		||||
    cmd.type === 'revolve' ||
 | 
			
		||||
    cmd.type === 'sweep'
 | 
			
		||||
  ) {
 | 
			
		||||
    const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type
 | 
			
		||||
    returnArr.push({
 | 
			
		||||
      id,
 | 
			
		||||
 | 
			
		||||
@ -37,6 +37,10 @@ export type ModelingCommandSchema = {
 | 
			
		||||
    // result: (typeof EXTRUSION_RESULTS)[number]
 | 
			
		||||
    distance: KclCommandValue
 | 
			
		||||
  }
 | 
			
		||||
  Sweep: {
 | 
			
		||||
    path: Selections
 | 
			
		||||
    profile: Selections
 | 
			
		||||
  }
 | 
			
		||||
  Loft: {
 | 
			
		||||
    selection: Selections
 | 
			
		||||
  }
 | 
			
		||||
@ -292,6 +296,33 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  Sweep: {
 | 
			
		||||
    description:
 | 
			
		||||
      'Create a 3D body by moving a sketch region along an arbitrary path.',
 | 
			
		||||
    icon: 'sweep',
 | 
			
		||||
    status: 'development',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    args: {
 | 
			
		||||
      profile: {
 | 
			
		||||
        inputType: 'selection',
 | 
			
		||||
        selectionTypes: ['solid2D'],
 | 
			
		||||
        required: true,
 | 
			
		||||
        skip: true,
 | 
			
		||||
        multiple: false,
 | 
			
		||||
        // TODO: add dry-run validation
 | 
			
		||||
        warningMessage:
 | 
			
		||||
          'The sweep workflow is new and under tested. Please break it and report issues.',
 | 
			
		||||
      },
 | 
			
		||||
      path: {
 | 
			
		||||
        inputType: 'selection',
 | 
			
		||||
        selectionTypes: ['segment', 'path'],
 | 
			
		||||
        required: true,
 | 
			
		||||
        skip: true,
 | 
			
		||||
        multiple: false,
 | 
			
		||||
        // TODO: add dry-run validation
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  Loft: {
 | 
			
		||||
    description: 'Create a 3D body by blending between two or more sketches',
 | 
			
		||||
    icon: 'loft',
 | 
			
		||||
 | 
			
		||||
@ -53,6 +53,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
 | 
			
		||||
  SKETCH: 'sketch',
 | 
			
		||||
  EXTRUDE: 'extrude',
 | 
			
		||||
  LOFT: 'loft',
 | 
			
		||||
  SWEEP: 'sweep',
 | 
			
		||||
  SHELL: 'shell',
 | 
			
		||||
  SEGMENT: 'seg',
 | 
			
		||||
  REVOLVE: 'revolve',
 | 
			
		||||
 | 
			
		||||
@ -119,17 +119,21 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: 'sweep',
 | 
			
		||||
        onClick: () => console.error('Sweep not yet implemented'),
 | 
			
		||||
        onClick: ({ commandBarSend }) =>
 | 
			
		||||
          commandBarSend({
 | 
			
		||||
            type: 'Find and select command',
 | 
			
		||||
            data: { name: 'Sweep', groupId: 'modeling' },
 | 
			
		||||
          }),
 | 
			
		||||
        icon: 'sweep',
 | 
			
		||||
        status: 'unavailable',
 | 
			
		||||
        status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
 | 
			
		||||
        title: 'Sweep',
 | 
			
		||||
        hotkey: 'W',
 | 
			
		||||
        description:
 | 
			
		||||
          'Create a 3D body by moving a sketch region along an arbitrary path.',
 | 
			
		||||
        links: [
 | 
			
		||||
          {
 | 
			
		||||
            label: 'GitHub discussion',
 | 
			
		||||
            url: 'https://github.com/KittyCAD/modeling-app/discussions/498',
 | 
			
		||||
            label: 'KCL docs',
 | 
			
		||||
            url: 'https://zoo.dev/docs/kcl/sweep',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@ -45,6 +45,7 @@ import {
 | 
			
		||||
import { revolveSketch } from 'lang/modifyAst/addRevolve'
 | 
			
		||||
import {
 | 
			
		||||
  addOffsetPlane,
 | 
			
		||||
  addSweep,
 | 
			
		||||
  deleteFromSelection,
 | 
			
		||||
  extrudeSketch,
 | 
			
		||||
  loftSketches,
 | 
			
		||||
@ -266,6 +267,7 @@ export type ModelingMachineEvent =
 | 
			
		||||
  | { type: 'Export'; data: ModelingCommandSchema['Export'] }
 | 
			
		||||
  | { type: 'Make'; data: ModelingCommandSchema['Make'] }
 | 
			
		||||
  | { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
 | 
			
		||||
  | { type: 'Sweep'; data?: ModelingCommandSchema['Sweep'] }
 | 
			
		||||
  | { type: 'Loft'; data?: ModelingCommandSchema['Loft'] }
 | 
			
		||||
  | { type: 'Shell'; data?: ModelingCommandSchema['Shell'] }
 | 
			
		||||
  | { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
 | 
			
		||||
@ -1544,6 +1546,66 @@ export const modelingMachine = setup({
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ),
 | 
			
		||||
    sweepAstMod: fromPromise(
 | 
			
		||||
      async ({
 | 
			
		||||
        input,
 | 
			
		||||
      }: {
 | 
			
		||||
        input: ModelingCommandSchema['Sweep'] | undefined
 | 
			
		||||
      }) => {
 | 
			
		||||
        if (!input) return new Error('No input provided')
 | 
			
		||||
        // Extract inputs
 | 
			
		||||
        const ast = kclManager.ast
 | 
			
		||||
        const { profile, path } = input
 | 
			
		||||
 | 
			
		||||
        // Find the profile declaration
 | 
			
		||||
        const profileNodePath = getNodePathFromSourceRange(
 | 
			
		||||
          ast,
 | 
			
		||||
          profile.graphSelections[0].codeRef.range
 | 
			
		||||
        )
 | 
			
		||||
        const profileNode = getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
          ast,
 | 
			
		||||
          profileNodePath,
 | 
			
		||||
          'VariableDeclarator'
 | 
			
		||||
        )
 | 
			
		||||
        if (err(profileNode)) {
 | 
			
		||||
          return new Error("Couldn't parse profile selection")
 | 
			
		||||
        }
 | 
			
		||||
        const profileDeclarator = profileNode.node
 | 
			
		||||
 | 
			
		||||
        // Find the path declaration
 | 
			
		||||
        const pathNodePath = getNodePathFromSourceRange(
 | 
			
		||||
          ast,
 | 
			
		||||
          path.graphSelections[0].codeRef.range
 | 
			
		||||
        )
 | 
			
		||||
        const pathNode = getNodeFromPath<VariableDeclarator>(
 | 
			
		||||
          ast,
 | 
			
		||||
          pathNodePath,
 | 
			
		||||
          'VariableDeclarator'
 | 
			
		||||
        )
 | 
			
		||||
        if (err(pathNode)) {
 | 
			
		||||
          return new Error("Couldn't parse path selection")
 | 
			
		||||
        }
 | 
			
		||||
        const pathDeclarator = pathNode.node
 | 
			
		||||
 | 
			
		||||
        // Perform the sweep
 | 
			
		||||
        const sweepRes = addSweep(ast, profileDeclarator, pathDeclarator)
 | 
			
		||||
        const updateAstResult = await kclManager.updateAst(
 | 
			
		||||
          sweepRes.modifiedAst,
 | 
			
		||||
          true,
 | 
			
		||||
          {
 | 
			
		||||
            focusPath: [sweepRes.pathToNode],
 | 
			
		||||
          }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        await codeManager.updateEditorWithAstAndWriteToFile(
 | 
			
		||||
          updateAstResult.newAst
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if (updateAstResult?.selections) {
 | 
			
		||||
          editorManager.selectRange(updateAstResult?.selections)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ),
 | 
			
		||||
    loftAstMod: fromPromise(
 | 
			
		||||
      async ({
 | 
			
		||||
        input,
 | 
			
		||||
@ -1739,6 +1801,11 @@ export const modelingMachine = setup({
 | 
			
		||||
          reenter: false,
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        Sweep: {
 | 
			
		||||
          target: 'Applying sweep',
 | 
			
		||||
          reenter: true,
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        Loft: {
 | 
			
		||||
          target: 'Applying loft',
 | 
			
		||||
          reenter: true,
 | 
			
		||||
@ -2531,6 +2598,19 @@ export const modelingMachine = setup({
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Applying sweep': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        src: 'sweepAstMod',
 | 
			
		||||
        id: 'sweepAstMod',
 | 
			
		||||
        input: ({ event }) => {
 | 
			
		||||
          if (event.type !== 'Sweep') return undefined
 | 
			
		||||
          return event.data
 | 
			
		||||
        },
 | 
			
		||||
        onDone: ['idle'],
 | 
			
		||||
        onError: ['idle'],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Applying loft': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        src: 'loftAstMod',
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user