diff --git a/e2e/playwright/boolean.spec.ts b/e2e/playwright/boolean.spec.ts new file mode 100644 index 000000000..67127ef68 --- /dev/null +++ b/e2e/playwright/boolean.spec.ts @@ -0,0 +1,117 @@ +import { test, expect } from './zoo-test' +import fs from 'node:fs/promises' +import path from 'node:path' + +test.describe('Point and click for boolean workflows', () => { + // Boolean operations to test + const booleanOperations = [ + { + name: 'union', + code: 'union([extrude001, extrude006])', + }, + { + name: 'subtract', + code: 'subtract([extrude001], tools = [extrude006])', + }, + { + name: 'intersect', + code: 'intersect([extrude001, extrude006])', + }, + ] as const + for (let i = 0; i < booleanOperations.length; i++) { + const operation = booleanOperations[i] + const operationName = operation.name + const commandName = `Boolean ${ + operationName.charAt(0).toUpperCase() + operationName.slice(1) + }` + test(`Create boolean operation -- ${operationName}`, async ({ + context, + homePage, + cmdBar, + editor, + toolbar, + scene, + page, + }) => { + const file = await fs.readFile( + path.resolve( + __dirname, + '../../', + './rust/kcl-lib/e2e/executor/inputs/boolean-setup-with' + ), + 'utf-8' + ) + await context.addInitScript((file) => { + localStorage.setItem('persistCode', file) + }, file) + await homePage.goToModelingScene() + await scene.waitForExecutionDone() + + await scene.settled(cmdBar) + + // Test coordinates for selection - these might need adjustment based on actual scene layout + const cylinderPoint = { x: 592, y: 174 } + const secondObjectPoint = { x: 683, y: 273 } + + // Create mouse helpers for selecting objects + const [clickFirstObject] = scene.makeMouseHelpers( + cylinderPoint.x, + cylinderPoint.y, + { steps: 10 } + ) + const [clickSecondObject] = scene.makeMouseHelpers( + secondObjectPoint.x, + secondObjectPoint.y, + { steps: 10 } + ) + + await test.step(`Test ${operationName} operation`, async () => { + // Click the boolean operation button in the toolbar + await toolbar.selectBoolean(operationName) + + // Verify command bar is showing the right command + await expect(cmdBar.page.getByTestId('command-name')).toContainText( + commandName + ) + + // Select first object in the scene, expect there to be a pixel diff from the selection color change + await clickFirstObject({ pixelDiff: 50 }) + + // For subtract, we need to proceed to the next step before selecting the second object + if (operationName !== 'subtract') { + // should down shift key to select multiple objects + await page.keyboard.down('Shift') + } + + // Select second object + await clickSecondObject({ pixelDiff: 50 }) + + // Confirm the operation in the command bar + await cmdBar.progressCmdBar() + + if (operationName === 'union' || operationName === 'intersect') { + await cmdBar.expectState({ + stage: 'review', + headerArguments: { + Solids: '2 paths', + }, + commandName, + }) + } else if (operationName === 'subtract') { + await cmdBar.expectState({ + stage: 'review', + headerArguments: { + Tool: '1 path', + Target: '1 path', + }, + commandName, + }) + } + + await cmdBar.submit() + + await editor.expectEditor.toContain(operation.code) + }) + }) + } +}) diff --git a/e2e/playwright/fixtures/toolbarFixture.ts b/e2e/playwright/fixtures/toolbarFixture.ts index 468140b39..09dd54d96 100644 --- a/e2e/playwright/fixtures/toolbarFixture.ts +++ b/e2e/playwright/fixtures/toolbarFixture.ts @@ -181,6 +181,14 @@ export class ToolbarFixture { ).toBeVisible() await this.page.getByTestId('dropdown-center-rectangle').click() } + selectBoolean = async (operation: 'union' | 'subtract' | 'intersect') => { + await this.page + .getByRole('button', { name: 'caret down Union: open menu' }) + .click() + const operationTestId = `dropdown-boolean-${operation}` + await expect(this.page.getByTestId(operationTestId)).toBeVisible() + await this.page.getByTestId(operationTestId).click() + } selectCircleThreePoint = async () => { await this.page diff --git a/rust/kcl-lib/e2e/executor/inputs/boolean-setup-with b/rust/kcl-lib/e2e/executor/inputs/boolean-setup-with new file mode 100644 index 000000000..81e7139fe --- /dev/null +++ b/rust/kcl-lib/e2e/executor/inputs/boolean-setup-with @@ -0,0 +1,77 @@ +@settings(defaultLengthUnit = mm) + +sketch001 = startSketchOn(XZ) +profile001 = circle(sketch001, center = [154.36, 113.92], radius = 41.09) +extrude001 = extrude(profile001, length = 200) +sketch002 = startSketchOn(XY) +profile002 = startProfileAt([72.24, -52.05], sketch002) + |> angledLine([0, 181.26], %, $rectangleSegmentA001) + |> angledLine([ + segAng(rectangleSegmentA001) - 90, + 21.54 + ], %) + |> angledLine([ + segAng(rectangleSegmentA001), + -segLen(rectangleSegmentA001) + ], %, $mySeg) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude002 = extrude(profile002, length = 150) + |> chamfer( + %, + length = 15, + tags = [mySeg], + tag = $seg02, + ) + +sketch003 = startSketchOn(extrude002, mySeg) +profile003 = startProfileAt([207.36, 126.19], sketch003) + |> angledLine([0, 33.57], %, $rectangleSegmentA002) + |> angledLine([ + segAng(rectangleSegmentA002) - 90, + 99.11 + ], %) + |> angledLine([ + segAng(rectangleSegmentA002), + -segLen(rectangleSegmentA002) + ], %, $seg01) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude003 = extrude(profile003, length = -20) +sketch004 = startSketchOn(extrude003, seg01) +profile004 = startProfileAt([-235.38, 66.16], sketch004) + |> angledLine([0, 24.21], %, $rectangleSegmentA003) + |> angledLine([ + segAng(rectangleSegmentA003) - 90, + 3.72 + ], %) + |> angledLine([ + segAng(rectangleSegmentA003), + -segLen(rectangleSegmentA003) + ], %) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude004 = extrude(profile004, length = 30) + +sketch005 = startSketchOn(extrude002, seg02) +profile005 = startProfileAt([-129.93, -59.19], sketch005) + |> xLine(length = 48.79) + |> line(end = [1.33, 11.03]) + |> xLine(length = -60.56, tag = $seg03) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude005 = extrude(profile005, length = -10) +sketch006 = startSketchOn(extrude005, seg03) +profile006 = startProfileAt([-95.86, 38.73], sketch006) + |> angledLine([0, 3.48], %, $rectangleSegmentA004) + |> angledLine([ + segAng(rectangleSegmentA004) - 90, + 3.36 + ], %) + |> angledLine([ + segAng(rectangleSegmentA004), + -segLen(rectangleSegmentA004) + ], %) + |> line(endAbsolute = [profileStartX(%), profileStartY(%)]) + |> close() +extrude006 = extrude(profile006, length = 13) \ No newline at end of file diff --git a/src/components/CommandBar/CommandBarSelectionInput.tsx b/src/components/CommandBar/CommandBarSelectionInput.tsx index f812e183f..454c22cf5 100644 --- a/src/components/CommandBar/CommandBarSelectionInput.tsx +++ b/src/components/CommandBar/CommandBarSelectionInput.tsx @@ -6,7 +6,7 @@ import { getSelectionCountByType, getSelectionTypeDisplayText, } from 'lib/selections' -import { kclManager } from 'lib/singletons' +import { engineCommandManager, kclManager } from 'lib/singletons' import { reportRejection } from 'lib/trap' import { toSync } from 'lib/utils' import { commandBarActor, useCommandBarState } from 'machines/commandBarMachine' @@ -112,6 +112,23 @@ function CommandBarSelectionInput({ onSubmit(selection) } + // Clear selection if needed + useEffect(() => { + arg.clearSelectionFirst && + engineCommandManager.modelingSend({ + type: 'Set selection', + data: { + selectionType: 'singleCodeCursor', + }, + }) + }, [arg.clearSelectionFirst]) + + // Set selection filter if needed, and reset it when the component unmounts + useEffect(() => { + arg.selectionFilter && kclManager.setSelectionFilter(arg.selectionFilter) + return () => kclManager.defaultSelectionFilter(selection) + }, [arg.selectionFilter]) + return (