Hook up chamfer UI with AST-mod (#4694)
* button * config * hook up with ast * cmd bar test * button states fix and test * little naming fix * xState action to actor * remove button state test updates * fixture-based approach * nightly Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com> * Update src/lib/toolbar.ts Co-authored-by: Frank Noirot <frank@zoo.dev> --------- Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com> Co-authored-by: Frank Noirot <frank@zoo.dev>
This commit is contained in:
		@ -15,6 +15,7 @@ export class ToolbarFixture {
 | 
			
		||||
  extrudeButton!: Locator
 | 
			
		||||
  loftButton!: Locator
 | 
			
		||||
  sweepButton!: Locator
 | 
			
		||||
  chamferButton!: Locator
 | 
			
		||||
  shellButton!: Locator
 | 
			
		||||
  offsetPlaneButton!: Locator
 | 
			
		||||
  startSketchBtn!: Locator
 | 
			
		||||
@ -42,6 +43,7 @@ export class ToolbarFixture {
 | 
			
		||||
    this.extrudeButton = page.getByTestId('extrude')
 | 
			
		||||
    this.loftButton = page.getByTestId('loft')
 | 
			
		||||
    this.sweepButton = page.getByTestId('sweep')
 | 
			
		||||
    this.chamferButton = page.getByTestId('chamfer3d')
 | 
			
		||||
    this.shellButton = page.getByTestId('shell')
 | 
			
		||||
    this.offsetPlaneButton = page.getByTestId('plane-offset')
 | 
			
		||||
    this.startSketchBtn = page.getByTestId('sketch')
 | 
			
		||||
 | 
			
		||||
@ -1032,6 +1032,222 @@ sketch002 = startSketchOn('XZ')
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test(`Chamfer point-and-click`, async ({
 | 
			
		||||
  context,
 | 
			
		||||
  page,
 | 
			
		||||
  homePage,
 | 
			
		||||
  scene,
 | 
			
		||||
  editor,
 | 
			
		||||
  toolbar,
 | 
			
		||||
  cmdBar,
 | 
			
		||||
}) => {
 | 
			
		||||
  // TODO: fix this test on windows after the electron migration
 | 
			
		||||
  test.skip(process.platform === 'win32', 'Skip on windows')
 | 
			
		||||
 | 
			
		||||
  // Code samples
 | 
			
		||||
  const initialCode = `sketch001 = startSketchOn('XY')
 | 
			
		||||
  |> startProfileAt([-12, -6], %)
 | 
			
		||||
  |> line([0, 12], %)
 | 
			
		||||
  |> line([24, 0], %)
 | 
			
		||||
  |> line([0, -12], %)
 | 
			
		||||
  |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
extrude001 = extrude(-12, sketch001)
 | 
			
		||||
`
 | 
			
		||||
  const firstChamferDeclaration = 'chamfer({ length = 5, tags = [seg01] }, %)'
 | 
			
		||||
  const secondChamferDeclaration =
 | 
			
		||||
    'chamfer({       length = 5,       tags = [getOppositeEdge(seg01)]     }, %)'
 | 
			
		||||
 | 
			
		||||
  // Locators
 | 
			
		||||
  const firstEdgeLocation = { x: 600, y: 193 }
 | 
			
		||||
  const secondEdgeLocation = { x: 600, y: 383 }
 | 
			
		||||
  const bodyLocation = { x: 630, y: 290 }
 | 
			
		||||
  const [clickOnFirstEdge] = scene.makeMouseHelpers(
 | 
			
		||||
    firstEdgeLocation.x,
 | 
			
		||||
    firstEdgeLocation.y
 | 
			
		||||
  )
 | 
			
		||||
  const [clickOnSecondEdge] = scene.makeMouseHelpers(
 | 
			
		||||
    secondEdgeLocation.x,
 | 
			
		||||
    secondEdgeLocation.y
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  // Colors
 | 
			
		||||
  const edgeColorWhite: [number, number, number] = [248, 248, 248]
 | 
			
		||||
  const edgeColorYellow: [number, number, number] = [251, 251, 40] // Mac:B=67 Ubuntu:B=12
 | 
			
		||||
  const bodyColor: [number, number, number] = [155, 155, 155]
 | 
			
		||||
  const chamferColor: [number, number, number] = [168, 168, 168]
 | 
			
		||||
  const backgroundColor: [number, number, number] = [30, 30, 30]
 | 
			
		||||
  const lowTolerance = 20
 | 
			
		||||
  const highTolerance = 40
 | 
			
		||||
 | 
			
		||||
  // Setup
 | 
			
		||||
  await context.addInitScript((initialCode) => {
 | 
			
		||||
    localStorage.setItem('persistCode', initialCode)
 | 
			
		||||
  }, initialCode)
 | 
			
		||||
  await page.setBodyDimensions({ width: 1000, height: 500 })
 | 
			
		||||
  await homePage.goToModelingScene()
 | 
			
		||||
 | 
			
		||||
  await test.step(`Verify scene is loaded`, async () => {
 | 
			
		||||
    // verify modeling scene is loaded
 | 
			
		||||
    await scene.expectPixelColor(
 | 
			
		||||
      backgroundColor,
 | 
			
		||||
      secondEdgeLocation,
 | 
			
		||||
      lowTolerance
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    // wait for stream to load
 | 
			
		||||
    await scene.expectPixelColor(bodyColor, bodyLocation, highTolerance)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Test 1: Command bar flow with preselected edges
 | 
			
		||||
  await test.step(`Select first edge`, async () => {
 | 
			
		||||
    await scene.expectPixelColor(
 | 
			
		||||
      edgeColorWhite,
 | 
			
		||||
      firstEdgeLocation,
 | 
			
		||||
      lowTolerance
 | 
			
		||||
    )
 | 
			
		||||
    await clickOnFirstEdge()
 | 
			
		||||
    await scene.expectPixelColor(
 | 
			
		||||
      edgeColorYellow,
 | 
			
		||||
      firstEdgeLocation,
 | 
			
		||||
      highTolerance // Ubuntu color mismatch can require high tolerance
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step(`Apply chamfer to the preselected edge`, async () => {
 | 
			
		||||
    await toolbar.chamferButton.click()
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      commandName: 'Chamfer',
 | 
			
		||||
      highlightedHeaderArg: 'selection',
 | 
			
		||||
      currentArgKey: 'selection',
 | 
			
		||||
      currentArgValue: '',
 | 
			
		||||
      headerArguments: {
 | 
			
		||||
        Selection: '',
 | 
			
		||||
        Length: '',
 | 
			
		||||
      },
 | 
			
		||||
      stage: 'arguments',
 | 
			
		||||
    })
 | 
			
		||||
    await cmdBar.progressCmdBar()
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      commandName: 'Chamfer',
 | 
			
		||||
      highlightedHeaderArg: 'length',
 | 
			
		||||
      currentArgKey: 'length',
 | 
			
		||||
      currentArgValue: '5',
 | 
			
		||||
      headerArguments: {
 | 
			
		||||
        Selection: '1 face',
 | 
			
		||||
        Length: '',
 | 
			
		||||
      },
 | 
			
		||||
      stage: 'arguments',
 | 
			
		||||
    })
 | 
			
		||||
    await cmdBar.progressCmdBar()
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      commandName: 'Chamfer',
 | 
			
		||||
      headerArguments: {
 | 
			
		||||
        Selection: '1 face',
 | 
			
		||||
        Length: '5',
 | 
			
		||||
      },
 | 
			
		||||
      stage: 'review',
 | 
			
		||||
    })
 | 
			
		||||
    await cmdBar.progressCmdBar()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step(`Confirm code is added to the editor`, async () => {
 | 
			
		||||
    await editor.expectEditor.toContain(firstChamferDeclaration)
 | 
			
		||||
    await editor.expectState({
 | 
			
		||||
      diagnostics: [],
 | 
			
		||||
      activeLines: ['|>chamfer({length=5,tags=[seg01]},%)'],
 | 
			
		||||
      highlightedCode: '',
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step(`Confirm scene has changed`, async () => {
 | 
			
		||||
    await scene.expectPixelColor(chamferColor, firstEdgeLocation, lowTolerance)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Test 2: Command bar flow without preselected edges
 | 
			
		||||
  await test.step(`Open chamfer UI without selecting edges`, async () => {
 | 
			
		||||
    await toolbar.chamferButton.click()
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      stage: 'arguments',
 | 
			
		||||
      currentArgKey: 'selection',
 | 
			
		||||
      currentArgValue: '',
 | 
			
		||||
      headerArguments: {
 | 
			
		||||
        Selection: '',
 | 
			
		||||
        Length: '',
 | 
			
		||||
      },
 | 
			
		||||
      highlightedHeaderArg: 'selection',
 | 
			
		||||
      commandName: 'Chamfer',
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step(`Select second edge`, async () => {
 | 
			
		||||
    await scene.expectPixelColor(
 | 
			
		||||
      edgeColorWhite,
 | 
			
		||||
      secondEdgeLocation,
 | 
			
		||||
      lowTolerance
 | 
			
		||||
    )
 | 
			
		||||
    await clickOnSecondEdge()
 | 
			
		||||
    await scene.expectPixelColor(
 | 
			
		||||
      edgeColorYellow,
 | 
			
		||||
      secondEdgeLocation,
 | 
			
		||||
      highTolerance // Ubuntu color mismatch can require high tolerance
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step(`Apply chamfer to the second edge`, async () => {
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      commandName: 'Chamfer',
 | 
			
		||||
      highlightedHeaderArg: 'selection',
 | 
			
		||||
      currentArgKey: 'selection',
 | 
			
		||||
      currentArgValue: '',
 | 
			
		||||
      headerArguments: {
 | 
			
		||||
        Selection: '',
 | 
			
		||||
        Length: '',
 | 
			
		||||
      },
 | 
			
		||||
      stage: 'arguments',
 | 
			
		||||
    })
 | 
			
		||||
    await cmdBar.progressCmdBar()
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      commandName: 'Chamfer',
 | 
			
		||||
      highlightedHeaderArg: 'length',
 | 
			
		||||
      currentArgKey: 'length',
 | 
			
		||||
      currentArgValue: '5',
 | 
			
		||||
      headerArguments: {
 | 
			
		||||
        Selection: '1 sweepEdge',
 | 
			
		||||
        Length: '',
 | 
			
		||||
      },
 | 
			
		||||
      stage: 'arguments',
 | 
			
		||||
    })
 | 
			
		||||
    await cmdBar.progressCmdBar()
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      commandName: 'Chamfer',
 | 
			
		||||
      headerArguments: {
 | 
			
		||||
        Selection: '1 sweepEdge',
 | 
			
		||||
        Length: '5',
 | 
			
		||||
      },
 | 
			
		||||
      stage: 'review',
 | 
			
		||||
    })
 | 
			
		||||
    await cmdBar.progressCmdBar()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step(`Confirm code is added to the editor`, async () => {
 | 
			
		||||
    await editor.expectEditor.toContain(secondChamferDeclaration)
 | 
			
		||||
    await editor.expectState({
 | 
			
		||||
      diagnostics: [],
 | 
			
		||||
      activeLines: ['length=5,'],
 | 
			
		||||
      highlightedCode: '',
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step(`Confirm scene has changed`, async () => {
 | 
			
		||||
    await scene.expectPixelColor(
 | 
			
		||||
      backgroundColor,
 | 
			
		||||
      secondEdgeLocation,
 | 
			
		||||
      lowTolerance
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const shellPointAndClickCapCases = [
 | 
			
		||||
  { shouldPreselect: true },
 | 
			
		||||
  { shouldPreselect: false },
 | 
			
		||||
 | 
			
		||||
@ -56,10 +56,13 @@ export type ModelingCommandSchema = {
 | 
			
		||||
    edge: Selections
 | 
			
		||||
  }
 | 
			
		||||
  Fillet: {
 | 
			
		||||
    // todo
 | 
			
		||||
    selection: Selections
 | 
			
		||||
    radius: KclCommandValue
 | 
			
		||||
  }
 | 
			
		||||
  Chamfer: {
 | 
			
		||||
    selection: Selections
 | 
			
		||||
    length: KclCommandValue
 | 
			
		||||
  }
 | 
			
		||||
  'Offset plane': {
 | 
			
		||||
    plane: Selections
 | 
			
		||||
    distance: KclCommandValue
 | 
			
		||||
@ -429,7 +432,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
 | 
			
		||||
  },
 | 
			
		||||
  Fillet: {
 | 
			
		||||
    description: 'Fillet edge',
 | 
			
		||||
    icon: 'fillet',
 | 
			
		||||
    icon: 'fillet3d',
 | 
			
		||||
    status: 'development',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    args: {
 | 
			
		||||
@ -449,6 +452,28 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  Chamfer: {
 | 
			
		||||
    description: 'Chamfer edge',
 | 
			
		||||
    icon: 'chamfer3d',
 | 
			
		||||
    status: 'development',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    args: {
 | 
			
		||||
      selection: {
 | 
			
		||||
        inputType: 'selection',
 | 
			
		||||
        selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
 | 
			
		||||
        multiple: true,
 | 
			
		||||
        required: true,
 | 
			
		||||
        skip: false,
 | 
			
		||||
        warningMessage:
 | 
			
		||||
          'Chamfers cannot touch other chamfers yet. This is under development.',
 | 
			
		||||
      },
 | 
			
		||||
      length: {
 | 
			
		||||
        inputType: 'kcl',
 | 
			
		||||
        defaultValue: KCL_DEFAULT_LENGTH,
 | 
			
		||||
        required: true,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  'Constrain length': {
 | 
			
		||||
    description: 'Constrain the length of one or more segments.',
 | 
			
		||||
    icon: 'dimension',
 | 
			
		||||
 | 
			
		||||
@ -173,10 +173,14 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
        links: [{ label: 'KCL docs', url: 'https://zoo.dev/docs/kcl/fillet' }],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: 'chamfer',
 | 
			
		||||
        onClick: () => console.error('Chamfer not yet implemented'),
 | 
			
		||||
        id: 'chamfer3d',
 | 
			
		||||
        onClick: ({ commandBarSend }) =>
 | 
			
		||||
          commandBarSend({
 | 
			
		||||
            type: 'Find and select command',
 | 
			
		||||
            data: { name: 'Chamfer', groupId: 'modeling' },
 | 
			
		||||
          }),
 | 
			
		||||
        icon: 'chamfer3d',
 | 
			
		||||
        status: 'kcl-only',
 | 
			
		||||
        status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
 | 
			
		||||
        title: 'Chamfer',
 | 
			
		||||
        hotkey: 'C',
 | 
			
		||||
        description: 'Bevel the edges of a 3D solid.',
 | 
			
		||||
 | 
			
		||||
@ -52,6 +52,7 @@ import {
 | 
			
		||||
} from 'lang/modifyAst'
 | 
			
		||||
import {
 | 
			
		||||
  applyEdgeTreatmentToSelection,
 | 
			
		||||
  ChamferParameters,
 | 
			
		||||
  EdgeTreatmentType,
 | 
			
		||||
  FilletParameters,
 | 
			
		||||
} from 'lang/modifyAst/addEdgeTreatment'
 | 
			
		||||
@ -272,6 +273,7 @@ export type ModelingMachineEvent =
 | 
			
		||||
  | { type: 'Shell'; data?: ModelingCommandSchema['Shell'] }
 | 
			
		||||
  | { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
 | 
			
		||||
  | { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
 | 
			
		||||
  | { type: 'Chamfer'; data?: ModelingCommandSchema['Chamfer'] }
 | 
			
		||||
  | { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] }
 | 
			
		||||
  | { type: 'Text-to-CAD'; data: ModelingCommandSchema['Text-to-CAD'] }
 | 
			
		||||
  | { type: 'Prompt-to-edit'; data: ModelingCommandSchema['Prompt-to-edit'] }
 | 
			
		||||
@ -1737,6 +1739,33 @@ export const modelingMachine = setup({
 | 
			
		||||
        if (err(filletResult)) return filletResult
 | 
			
		||||
      }
 | 
			
		||||
    ),
 | 
			
		||||
    chamferAstMod: fromPromise(
 | 
			
		||||
      async ({
 | 
			
		||||
        input,
 | 
			
		||||
      }: {
 | 
			
		||||
        input: ModelingCommandSchema['Chamfer'] | undefined
 | 
			
		||||
      }) => {
 | 
			
		||||
        if (!input) {
 | 
			
		||||
          return new Error('No input provided')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Extract inputs
 | 
			
		||||
        const ast = kclManager.ast
 | 
			
		||||
        const { selection, length } = input
 | 
			
		||||
        const parameters: ChamferParameters = {
 | 
			
		||||
          type: EdgeTreatmentType.Chamfer,
 | 
			
		||||
          length,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Apply chamfer to selection
 | 
			
		||||
        const chamferResult = await applyEdgeTreatmentToSelection(
 | 
			
		||||
          ast,
 | 
			
		||||
          selection,
 | 
			
		||||
          parameters
 | 
			
		||||
        )
 | 
			
		||||
        if (err(chamferResult)) return chamferResult
 | 
			
		||||
      }
 | 
			
		||||
    ),
 | 
			
		||||
    'submit-prompt-edit': fromPromise(
 | 
			
		||||
      async ({ input }: { input: ModelingCommandSchema['Prompt-to-edit'] }) => {
 | 
			
		||||
        console.log('doing thing', input)
 | 
			
		||||
@ -1821,6 +1850,11 @@ export const modelingMachine = setup({
 | 
			
		||||
          reenter: true,
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        Chamfer: {
 | 
			
		||||
          target: 'Applying chamfer',
 | 
			
		||||
          reenter: true,
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        Export: {
 | 
			
		||||
          target: 'idle',
 | 
			
		||||
          reenter: false,
 | 
			
		||||
@ -2650,6 +2684,19 @@ export const modelingMachine = setup({
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Applying chamfer': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        src: 'chamferAstMod',
 | 
			
		||||
        id: 'chamferAstMod',
 | 
			
		||||
        input: ({ event }) => {
 | 
			
		||||
          if (event.type !== 'Chamfer') return undefined
 | 
			
		||||
          return event.data
 | 
			
		||||
        },
 | 
			
		||||
        onDone: ['idle'],
 | 
			
		||||
        onError: ['idle'],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    'Applying Prompt-to-edit': {
 | 
			
		||||
      invoke: {
 | 
			
		||||
        src: 'submit-prompt-edit',
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user