diff --git a/e2e/playwright/fixtures/toolbarFixture.ts b/e2e/playwright/fixtures/toolbarFixture.ts index c2f26f3de..87cda41b6 100644 --- a/e2e/playwright/fixtures/toolbarFixture.ts +++ b/e2e/playwright/fixtures/toolbarFixture.ts @@ -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') diff --git a/e2e/playwright/point-click.spec.ts b/e2e/playwright/point-click.spec.ts index 51fba0428..9f7c2381b 100644 --- a/e2e/playwright/point-click.spec.ts +++ b/e2e/playwright/point-click.spec.ts @@ -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 }, diff --git a/src/lib/commandBarConfigs/modelingCommandConfig.ts b/src/lib/commandBarConfigs/modelingCommandConfig.ts index d2aa753bb..51c8dd0c6 100644 --- a/src/lib/commandBarConfigs/modelingCommandConfig.ts +++ b/src/lib/commandBarConfigs/modelingCommandConfig.ts @@ -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', diff --git a/src/lib/toolbar.ts b/src/lib/toolbar.ts index 7736cc838..16ba8d779 100644 --- a/src/lib/toolbar.ts +++ b/src/lib/toolbar.ts @@ -173,10 +173,14 @@ export const toolbarConfig: Record = { 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.', diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index cfde08e9c..14a6bee12 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -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',