diff --git a/e2e/playwright/snapshot-tests.spec.ts b/e2e/playwright/snapshot-tests.spec.ts index 6c0805f8b..191edecc2 100644 --- a/e2e/playwright/snapshot-tests.spec.ts +++ b/e2e/playwright/snapshot-tests.spec.ts @@ -521,7 +521,6 @@ test( const startXPx = 600 // Equip the rectangle tool - await page.getByRole('button', { name: 'line Line', exact: true }).click() await page .getByRole('button', { name: 'rectangle Corner rectangle', exact: true }) .click() diff --git a/e2e/playwright/testing-selections.spec.ts b/e2e/playwright/testing-selections.spec.ts index eb3aba829..c162f910f 100644 --- a/e2e/playwright/testing-selections.spec.ts +++ b/e2e/playwright/testing-selections.spec.ts @@ -1208,6 +1208,12 @@ extrude001 = extrude(50, sketch001) test('Deselecting line tool should mean nothing happens on click', async ({ page, }) => { + /** + * If the line tool is clicked when the state is 'No Points' it will exit Sketch mode. + * This is the same exact workflow as pressing ESC. + * + * To continue to test this workflow, we now enter sketch mode and place a single point before exiting the line tool. + */ const u = await getUtils(page) await page.setViewportSize({ width: 1200, height: 500 }) @@ -1228,6 +1234,7 @@ extrude001 = extrude(50, sketch001) 200 ) + // Clicks the XZ Plane in the page await page.mouse.click(700, 200) await expect(page.locator('.cm-content')).toHaveText( @@ -1236,6 +1243,11 @@ extrude001 = extrude(50, sketch001) await page.waitForTimeout(600) + // Place a point because the line tool will exit if no points are pressed + await page.mouse.click(650, 200) + await page.waitForTimeout(600) + + // Code before exiting the tool let previousCodeContent = await page.locator('.cm-content').innerText() // deselect the line tool by clicking it diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index 0b0d73dbf..dfe76e2c5 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -149,6 +149,13 @@ export const ModelingMachineProvider = ({ }, 'sketch exit execute': ({ context: { store } }) => { ;(async () => { + // When cancelling the sketch mode we should disable sketch mode within the engine. + await engineCommandManager.sendSceneCommand({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { type: 'sketch_mode_disable' }, + }) + sceneInfra.camControls.syncDirection = 'clientToEngine' if (cameraProjection.current === 'perspective') { diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index d29d953bf..f0ebb915c 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -295,6 +295,12 @@ export class KclManager { this._cancelTokens.delete(currentExecutionId) return } + + // Exit sketch mode if the AST is empty + if (this._isAstEmpty(ast)) { + await this.disableSketchMode() + } + this.logs = logs // Do not add the errors since the program was interrupted and the error is not a real KCL error this.addKclErrors(isInterrupted ? [] : errors) @@ -553,6 +559,24 @@ export class KclManager { defaultSelectionFilter() { defaultSelectionFilter(this.programMemory, this.engineCommandManager) } + + /** + * We can send a single command of 'enable_sketch_mode' or send this in a batched request. + * When there is no code in the KCL editor we should be sending 'sketch_mode_disable' since any previous half finished + * code could leave the state of the application in sketch mode on the engine side. + */ + async disableSketchMode() { + await this.engineCommandManager.sendSceneCommand({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { type: 'sketch_mode_disable' }, + }) + } + + // Determines if there is no KCL code which means it is executing a blank KCL file + _isAstEmpty(ast: Program) { + return ast.start === 0 && ast.end === 0 && ast.body.length === 0 + } } function defaultSelectionFilter( diff --git a/src/lib/toolbar.ts b/src/lib/toolbar.ts index 6917de76c..988a29d47 100644 --- a/src/lib/toolbar.ts +++ b/src/lib/toolbar.ts @@ -295,15 +295,24 @@ export const toolbarConfig: Record = { 'break', { id: 'line', - onClick: ({ modelingState, modelingSend }) => - modelingSend({ - type: 'change tool', - data: { - tool: !modelingState.matches({ Sketch: 'Line tool' }) - ? 'line' - : 'none', - }, - }), + onClick: ({ modelingState, modelingSend }) => { + if (modelingState.matches({ Sketch: { 'Line tool': 'No Points' } })) { + // Exit the sketch state if there are no points and they press ESC + modelingSend({ + type: 'Cancel', + }) + } else { + // Exit the tool if there are points and they press ESC + modelingSend({ + type: 'change tool', + data: { + tool: !modelingState.matches({ Sketch: 'Line tool' }) + ? 'line' + : 'none', + }, + }) + } + }, icon: 'line', status: 'available', disabled: (state) =>