diff --git a/e2e/playwright/flow-tests.spec.ts b/e2e/playwright/flow-tests.spec.ts index ed13aa6e6..54eba877c 100644 --- a/e2e/playwright/flow-tests.spec.ts +++ b/e2e/playwright/flow-tests.spec.ts @@ -1894,6 +1894,239 @@ test.describe('Testing selections', () => { await selectionSequence() }) + test('Solids should be select and deletable', async ({ page }) => { + test.setTimeout(90_000) + const u = await getUtils(page) + await page.addInitScript(async () => { + localStorage.setItem( + 'persistCode', + `const sketch001 = startSketchOn('XZ') + |> startProfileAt([-79.26, 95.04], %) + |> line([112.54, 127.64], %, $seg02) + |> line([170.36, -121.61], %, $seg01) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const extrude001 = extrude(50, sketch001) +const sketch005 = startSketchOn(extrude001, 'END') + |> startProfileAt([23.24, 136.52], %) + |> line([-8.44, 36.61], %) + |> line([49.4, 2.05], %) + |> line([29.69, -46.95], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const sketch003 = startSketchOn(extrude001, seg01) + |> startProfileAt([21.23, 17.81], %) + |> line([51.97, 21.32], %) + |> line([4.07, -22.75], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const sketch002 = startSketchOn(extrude001, seg02) + |> startProfileAt([-100.54, 16.99], %) + |> line([0, 20.03], %) + |> line([62.61, 0], %, $seg03) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const extrude002 = extrude(50, sketch002) +const sketch004 = startSketchOn(extrude002, seg03) + |> startProfileAt([57.07, 134.77], %) + |> line([-4.72, 22.84], %) + |> line([28.8, 6.71], %) + |> line([9.19, -25.33], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const extrude003 = extrude(20, sketch004) +const pipeLength = 40 +const pipeSmallDia = 10 +const pipeLargeDia = 20 +const thickness = 0.5 +const part009 = startSketchOn('XY') + |> startProfileAt([pipeLargeDia - (thickness / 2), 38], %) + |> line([thickness, 0], %) + |> line([0, -1], %) + |> angledLineToX({ + angle: 60, + to: pipeSmallDia + thickness + }, %) + |> line([0, -pipeLength], %) + |> angledLineToX({ + angle: -60, + to: pipeLargeDia + thickness + }, %) + |> line([0, -1], %) + |> line([-thickness, 0], %) + |> line([0, 1], %) + |> angledLineToX({ angle: 120, to: pipeSmallDia }, %) + |> line([0, pipeLength], %) + |> angledLineToX({ angle: 60, to: pipeLargeDia }, %) + |> close(%) +const rev = revolve({ axis: 'y' }, part009) +` + ) + }, KCL_DEFAULT_LENGTH) + await page.setViewportSize({ width: 1000, height: 500 }) + await page.goto('/') + await u.waitForAuthSkipAppStart() + + await u.openDebugPanel() + await u.expectCmdLog('[data-message-type="execution-done"]') + await u.closeDebugPanel() + + await u.openAndClearDebugPanel() + await u.sendCustomCmd({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'default_camera_look_at', + vantage: { x: 1139.49, y: -7053, z: 8597.31 }, + center: { x: -2206.68, y: -1298.36, z: 60 }, + up: { x: 0, y: 0, z: 1 }, + }, + }) + await page.waitForTimeout(100) + await u.sendCustomCmd({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'default_camera_get_settings', + }, + }) + await page.waitForTimeout(100) + + const revolve = { x: 646, y: 248 } + const parentExtrude = { x: 915, y: 133 } + const solid2d = { x: 770, y: 167 } + + // DELETE REVOLVE + await page.mouse.click(revolve.x, revolve.y) + await page.waitForTimeout(100) + await expect(page.locator('.cm-activeLine')).toHaveText( + '|> line([0, -pipeLength], %)' + ) + await u.clearCommandLogs() + await page.keyboard.press('Backspace') + await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) + await page.waitForTimeout(200) + + await expect(u.codeLocator).not.toContainText( + `const rev = revolve({ axis: 'y' }, part009)` + ) + + // DELETE PARENT EXTRUDE + await page.mouse.click(parentExtrude.x, parentExtrude.y) + await page.waitForTimeout(100) + await expect(page.locator('.cm-activeLine')).toHaveText( + '|> line([170.36, -121.61], %, $seg01)' + ) + await u.clearCommandLogs() + await page.keyboard.press('Backspace') + await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) + await page.waitForTimeout(200) + await expect(u.codeLocator).not.toContainText( + `const extrude001 = extrude(50, sketch001)` + ) + await expect(u.codeLocator).toContainText(`const sketch005 = startSketchOn({ + plane: { + origin: { x: 0, y: -50, z: 0 }, + x_axis: { x: 1, y: 0, z: 0 }, + y_axis: { x: 0, y: 0, z: 1 }, + z_axis: { x: 0, y: -1, z: 0 } + } + })`) + await expect(u.codeLocator).toContainText(`const sketch003 = startSketchOn({ + plane: { + origin: { x: 116.53, y: 0, z: 163.25 }, + x_axis: { x: -0.81, y: 0, z: 0.58 }, + y_axis: { x: 0, y: -1, z: 0 }, + z_axis: { x: 0.58, y: 0, z: 0.81 } + } + })`) + await expect(u.codeLocator).toContainText(`const sketch002 = startSketchOn({ + plane: { + origin: { x: -91.74, y: 0, z: 80.89 }, + x_axis: { x: -0.66, y: 0, z: -0.75 }, + y_axis: { x: 0, y: -1, z: 0 }, + z_axis: { x: -0.75, y: 0, z: 0.66 } + } + })`) + + // DELETE SOLID 2D + await page.mouse.click(solid2d.x, solid2d.y) + await page.waitForTimeout(100) + await expect(page.locator('.cm-activeLine')).toHaveText( + '|> startProfileAt([23.24, 136.52], %)' + ) + await u.clearCommandLogs() + await page.keyboard.press('Backspace') + await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) + await page.waitForTimeout(200) + await expect(u.codeLocator).not.toContainText( + `const sketch005 = startSketchOn({` + ) + }) + test("Deleting solid that the AST mod can't handle results in a toast message", async ({ + page, + }) => { + const u = await getUtils(page) + await page.addInitScript(async () => { + localStorage.setItem( + 'persistCode', + `const sketch001 = startSketchOn('XZ') + |> startProfileAt([-79.26, 95.04], %) + |> line([112.54, 127.64], %, $seg02) + |> line([170.36, -121.61], %, $seg01) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const extrude001 = extrude(50, sketch001) +const launderExtrudeThroughVar = extrude001 +const sketch002 = startSketchOn(launderExtrudeThroughVar, seg02) + |> startProfileAt([-100.54, 16.99], %) + |> line([0, 20.03], %) + |> line([62.61, 0], %, $seg03) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +` + ) + }, KCL_DEFAULT_LENGTH) + await page.setViewportSize({ width: 1000, height: 500 }) + await page.goto('/') + await u.waitForAuthSkipAppStart() + + await u.openDebugPanel() + await u.expectCmdLog('[data-message-type="execution-done"]', 10_000) + await u.closeDebugPanel() + + await u.openAndClearDebugPanel() + await u.sendCustomCmd({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'default_camera_look_at', + vantage: { x: 1139.49, y: -7053, z: 8597.31 }, + center: { x: -2206.68, y: -1298.36, z: 60 }, + up: { x: 0, y: 0, z: 1 }, + }, + }) + await page.waitForTimeout(100) + await u.sendCustomCmd({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'default_camera_get_settings', + }, + }) + await page.waitForTimeout(100) + + // attempt delete + await page.mouse.click(930, 139) + await page.waitForTimeout(100) + await expect(page.locator('.cm-activeLine')).toHaveText( + '|> line([170.36, -121.61], %, $seg01)' + ) + await u.clearCommandLogs() + await page.keyboard.press('Backspace') + + await expect(page.getByText('Unable to delete part')).toBeVisible() + }) test('Hovering over 3d features highlights code', async ({ page }) => { const u = await getUtils(page) await page.addInitScript(async (KCL_DEFAULT_LENGTH) => { diff --git a/e2e/playwright/test-utils.ts b/e2e/playwright/test-utils.ts index e4806aae4..c72585288 100644 --- a/e2e/playwright/test-utils.ts +++ b/e2e/playwright/test-utils.ts @@ -45,8 +45,8 @@ async function clearCommandLogs(page: Page) { await page.getByTestId('clear-commands').click() } -async function expectCmdLog(page: Page, locatorStr: string) { - await expect(page.locator(locatorStr).last()).toBeVisible() +async function expectCmdLog(page: Page, locatorStr: string, timeout = 5000) { + await expect(page.locator(locatorStr).last()).toBeVisible({ timeout }) } async function waitForDefaultPlanesToBeVisible(page: Page) { @@ -228,7 +228,8 @@ export async function getUtils(page: Page) { await fillInput('z', xyz[2]) }, clearCommandLogs: () => clearCommandLogs(page), - expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr), + expectCmdLog: (locatorStr: string, timeout = 5000) => + expectCmdLog(page, locatorStr, timeout), openKclCodePanel: () => openKclCodePanel(page), closeKclCodePanel: () => closeKclCodePanel(page), openDebugPanel: () => openDebugPanel(page), diff --git a/src/clientSideScene/sceneEntities.ts b/src/clientSideScene/sceneEntities.ts index f8e133c25..89aaa329d 100644 --- a/src/clientSideScene/sceneEntities.ts +++ b/src/clientSideScene/sceneEntities.ts @@ -1967,9 +1967,9 @@ export async function getSketchOrientationDetails( * @param entityId - The ID of the entity for which orientation details are being fetched. * @returns A promise that resolves with the orientation details of the face. */ -async function getFaceDetails( +export async function getFaceDetails( entityId: string -): Promise { +): Promise { // TODO mode engine connection to allow batching returns and batch the following await engineCommandManager.sendSceneCommand({ type: 'modeling_cmd_req', @@ -1982,8 +1982,7 @@ async function getFaceDetails( entity_id: entityId, }, }) - // TODO change typing to get_sketch_mode_plane once lib is updated - const faceInfo: Models['FaceIsPlanar_type'] = ( + const faceInfo: Models['GetSketchModePlane_type'] = ( await engineCommandManager.sendSceneCommand({ type: 'modeling_cmd_req', cmd_id: uuidv4(), diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index a57d70486..637f0fd70 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -23,6 +23,7 @@ import { editorManager, sceneEntitiesManager, } from 'lib/singletons' +import { useHotkeys } from 'react-hotkeys-hook' import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance' import { angleBetweenInfo, @@ -78,6 +79,7 @@ import { getVarNameModal } from 'hooks/useToolbarGuards' import useHotkeyWrapper from 'lib/hotkeyWrapper' import { uuidv4 } from 'lib/utils' import { err, trap } from 'lib/trap' +import { useCommandsContext } from 'hooks/useCommandsContext' type MachineContext = { state: StateFrom @@ -140,6 +142,7 @@ export const ModelingMachineProvider = ({ } ) }) + const { commandBarState } = useCommandsContext() // Settings machine setup // const retrievedSettings = useRef( @@ -464,6 +467,11 @@ export const ModelingMachineProvider = ({ return canExtrudeSelection(selectionRanges) }, + 'has valid selection for deletion': ({ selectionRanges }) => { + if (!commandBarState.matches('Closed')) return false + if (selectionRanges.codeBasedSelections.length <= 0) return false + return true + }, 'Sketch is empty': ({ sketchDetails }) => { const node = getNodeFromPath( kclManager.ast, @@ -927,6 +935,11 @@ export const ModelingMachineProvider = ({ } }, [modelingSend]) + // Allow using the delete key to delete solids + useHotkeys(['backspace', 'delete', 'del'], () => { + modelingSend({ type: 'Delete selection' }) + }) + useStateMachineCommands({ machineId: 'modeling', state: modelingState, diff --git a/src/components/Stream.tsx b/src/components/Stream.tsx index 0009e1ff2..63ea78764 100644 --- a/src/components/Stream.tsx +++ b/src/components/Stream.tsx @@ -83,6 +83,7 @@ export const Stream = ({ className = '' }: { className?: string }) => { if (!videoRef.current) return if (state.matches('Sketch')) return if (state.matches('Sketch no face')) return + const { x, y } = getNormalisedCoordinates({ clientX: e.clientX, clientY: e.clientY, diff --git a/src/lang/modifyAst.test.ts b/src/lang/modifyAst.test.ts index fb2591db5..9cf202189 100644 --- a/src/lang/modifyAst.test.ts +++ b/src/lang/modifyAst.test.ts @@ -15,6 +15,7 @@ import { sketchOnExtrudedFace, deleteSegmentFromPipeExpression, removeSingleConstraintInfo, + deleteFromSelection, } from './modifyAst' import { enginelessExecutor } from '../lib/testHelpers' import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst' @@ -696,3 +697,196 @@ describe('Testing removeSingleConstraintInfo', () => { }) }) }) + +describe('Testing deleteFromSelection', () => { + const cases = [ + [ + 'basicCase', + { + codeBefore: `const myVar = 5 +const sketch003 = startSketchOn('XZ') + |> startProfileAt([3.82, 13.6], %) + |> line([-2.94, 2.7], %) + |> line([7.7, 0.16], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%)`, + codeAfter: `const myVar = 5\n`, + lineOfInterest: 'line([-2.94, 2.7], %)', + type: 'default', + }, + ], + [ + 'delete extrude', + { + codeBefore: `const sketch001 = startSketchOn('XZ') + |> startProfileAt([3.29, 7.86], %) + |> line([2.48, 2.44], %) + |> line([2.66, 1.17], %) + |> line([3.75, 0.46], %) + |> line([4.99, -0.46], %, $seg01) + |> line([-3.86, -2.73], %) + |> line([-17.67, 0.85], %) + |> close(%) +const extrude001 = extrude(10, sketch001)`, + codeAfter: `const sketch001 = startSketchOn('XZ') + |> startProfileAt([3.29, 7.86], %) + |> line([2.48, 2.44], %) + |> line([2.66, 1.17], %) + |> line([3.75, 0.46], %) + |> line([4.99, -0.46], %, $seg01) + |> line([-3.86, -2.73], %) + |> line([-17.67, 0.85], %) + |> close(%)\n`, + lineOfInterest: 'line([2.66, 1.17], %)', + type: 'extrude-wall', + }, + ], + [ + 'delete extrude with sketch on it', + { + codeBefore: `const myVar = 5 +const sketch001 = startSketchOn('XZ') + |> startProfileAt([4.46, 5.12], %, $tag) + |> line([0.08, myVar], %) + |> line([13.03, 2.02], %, $seg01) + |> line([3.9, -7.6], %) + |> line([-11.18, -2.15], %) + |> line([5.41, -9.61], %) + |> line([-8.54, -2.51], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const extrude001 = extrude(5, sketch001) +const sketch002 = startSketchOn(extrude001, seg01) + |> startProfileAt([-12.55, 2.89], %) + |> line([3.02, 1.9], %) + |> line([1.82, -1.49], %, $seg02) + |> angledLine([-86, segLen(seg02, %)], %) + |> line([-3.97, -0.53], %) + |> line([0.3, 0.84], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%)`, + codeAfter: `const myVar = 5 +const sketch001 = startSketchOn('XZ') + |> startProfileAt([4.46, 5.12], %, $tag) + |> line([0.08, myVar], %) + |> line([13.03, 2.02], %, $seg01) + |> line([3.9, -7.6], %) + |> line([-11.18, -2.15], %) + |> line([5.41, -9.61], %) + |> line([-8.54, -2.51], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const sketch002 = startSketchOn({ + plane: { + origin: { x: 1, y: 2, z: 3 }, + x_axis: { x: 4, y: 5, z: 6 }, + y_axis: { x: 7, y: 8, z: 9 }, + z_axis: { x: 10, y: 11, z: 12 } + } + }) + |> startProfileAt([-12.55, 2.89], %) + |> line([3.02, 1.9], %) + |> line([1.82, -1.49], %, $seg02) + |> angledLine([-86, segLen(seg02, %)], %) + |> line([-3.97, -0.53], %) + |> line([0.3, 0.84], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +`, + lineOfInterest: 'line([-11.18, -2.15], %)', + type: 'extrude-wall', + }, + ], + [ + 'delete extrude with sketch on it', + { + codeBefore: `const myVar = 5 +const sketch001 = startSketchOn('XZ') + |> startProfileAt([4.46, 5.12], %, $tag) + |> line([0.08, myVar], %) + |> line([13.03, 2.02], %, $seg01) + |> line([3.9, -7.6], %) + |> line([-11.18, -2.15], %) + |> line([5.41, -9.61], %) + |> line([-8.54, -2.51], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const extrude001 = extrude(5, sketch001) +const sketch002 = startSketchOn(extrude001, seg01) + |> startProfileAt([-12.55, 2.89], %) + |> line([3.02, 1.9], %) + |> line([1.82, -1.49], %, $seg02) + |> angledLine([-86, segLen(seg02, %)], %) + |> line([-3.97, -0.53], %) + |> line([0.3, 0.84], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%)`, + codeAfter: `const myVar = 5 +const sketch001 = startSketchOn('XZ') + |> startProfileAt([4.46, 5.12], %, $tag) + |> line([0.08, myVar], %) + |> line([13.03, 2.02], %, $seg01) + |> line([3.9, -7.6], %) + |> line([-11.18, -2.15], %) + |> line([5.41, -9.61], %) + |> line([-8.54, -2.51], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +const sketch002 = startSketchOn({ + plane: { + origin: { x: 1, y: 2, z: 3 }, + x_axis: { x: 4, y: 5, z: 6 }, + y_axis: { x: 7, y: 8, z: 9 }, + z_axis: { x: 10, y: 11, z: 12 } + } + }) + |> startProfileAt([-12.55, 2.89], %) + |> line([3.02, 1.9], %) + |> line([1.82, -1.49], %, $seg02) + |> angledLine([-86, segLen(seg02, %)], %) + |> line([-3.97, -0.53], %) + |> line([0.3, 0.84], %) + |> lineTo([profileStartX(%), profileStartY(%)], %) + |> close(%) +`, + lineOfInterest: 'startProfileAt([4.46, 5.12], %, $tag)', + type: 'end-cap', + }, + ], + ] as const + test.each(cases)( + '%s', + async (name, { codeBefore, codeAfter, lineOfInterest, type }) => { + // const lineOfInterest = 'line([-2.94, 2.7], %)' + const ast = parse(codeBefore) + if (err(ast)) throw ast + const programMemory = await enginelessExecutor(ast) + + // deleteFromSelection + const range: [number, number] = [ + codeBefore.indexOf(lineOfInterest), + codeBefore.indexOf(lineOfInterest) + lineOfInterest.length, + ] + const newAst = await deleteFromSelection( + ast, + { + range, + type, + }, + programMemory, + async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + return { + origin: { x: 1, y: 2, z: 3 }, + x_axis: { x: 4, y: 5, z: 6 }, + y_axis: { x: 7, y: 8, z: 9 }, + z_axis: { x: 10, y: 11, z: 12 }, + } + } + ) + if (err(newAst)) throw newAst + const newCode = recast(newAst) + expect(newCode).toBe(codeAfter) + } + ) +}) diff --git a/src/lang/modifyAst.ts b/src/lang/modifyAst.ts index 55f21d4f4..e2f047a2e 100644 --- a/src/lang/modifyAst.ts +++ b/src/lang/modifyAst.ts @@ -17,6 +17,7 @@ import { PathToNode, ProgramMemory, SourceRange, + SketchGroup, } from './wasm' import { isNodeSafeToReplacePath, @@ -25,6 +26,7 @@ import { getNodeFromPath, getNodePathFromSourceRange, isNodeSafeToReplace, + traverse, } from './queryAst' import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch' import { @@ -38,6 +40,7 @@ import { isOverlap, roundOff } from 'lib/utils' import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants' import { ConstrainInfo } from './std/stdTypes' import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator' +import { Models } from '@kittycad/lib' export function startSketchOnDefault( node: Program, @@ -873,3 +876,175 @@ export function removeSingleConstraintInfo( if (err(retval)) return false return retval } + +export async function deleteFromSelection( + ast: Program, + selection: Selection, + programMemory: ProgramMemory, + getFaceDetails: (id: string) => Promise = () => + ({} as any) +): Promise { + const astClone = JSON.parse(JSON.stringify(ast)) + const range = selection.range + const path = getNodePathFromSourceRange(ast, range) + const varDec = getNodeFromPath( + ast, + path, + 'VariableDeclarator' + ) + if (err(varDec)) return varDec + if ( + (selection.type === 'extrude-wall' || + selection.type === 'end-cap' || + selection.type === 'start-cap') && + varDec.node.init.type === 'PipeExpression' + ) { + const varDecName = varDec.node.id.name + let pathToNode: PathToNode | null = null + let extrudeNameToDelete = '' + traverse(astClone, { + enter: (node, path) => { + if (node.type === 'VariableDeclaration') { + const dec = node.declarations[0] + if ( + dec.init.type === 'CallExpression' && + (dec.init.callee.name === 'extrude' || + dec.init.callee.name === 'revolve') && + dec.init.arguments?.[1].type === 'Identifier' && + dec.init.arguments?.[1].name === varDecName + ) { + pathToNode = path + extrudeNameToDelete = dec.id.name + } + } + }, + }) + if (!pathToNode) return new Error('Could not find extrude variable') + + const expressionIndex = pathToNode[1][0] as number + astClone.body.splice(expressionIndex, 1) + if (extrudeNameToDelete) { + await new Promise(async (resolve) => { + let currentVariableName = '' + const pathsDependingOnExtrude: Array<{ + path: PathToNode + sketchName: string + }> = [] + traverse(astClone, { + leave: (node) => { + if (node.type === 'VariableDeclaration') { + currentVariableName = '' + } + }, + enter: async (node, path) => { + if (node.type === 'VariableDeclaration') { + currentVariableName = node.declarations[0].id.name + } + if ( + // match startSketchOn(${extrudeNameToDelete}) + node.type === 'CallExpression' && + node.callee.name === 'startSketchOn' && + node.arguments[0].type === 'Identifier' && + node.arguments[0].name === extrudeNameToDelete + ) { + pathsDependingOnExtrude.push({ + path, + sketchName: currentVariableName, + }) + } + }, + }) + const roundLiteral = (x: number) => createLiteral(roundOff(x)) + const modificationDetails: { + parent: PipeExpression['body'] + faceDetails: Models['FaceIsPlanar_type'] + lastKey: number + }[] = [] + for (const { path, sketchName } of pathsDependingOnExtrude) { + const parent = getNodeFromPath( + astClone, + path.slice(0, -1) + ) + if (err(parent)) { + return + } + const sketchToPreserve = programMemory.root[sketchName] as SketchGroup + console.log('sketchName', sketchName) + // Can't kick off multiple requests at once as getFaceDetails + // is three engine calls in one and they conflict + const faceDetails = await getFaceDetails(sketchToPreserve.on.id) + if ( + !( + faceDetails.origin && + faceDetails.x_axis && + faceDetails.y_axis && + faceDetails.z_axis + ) + ) { + return + } + const lastKey = Number(path.slice(-1)[0][0]) + modificationDetails.push({ + parent: parent.node, + faceDetails, + lastKey, + }) + } + for (const { parent, faceDetails, lastKey } of modificationDetails) { + if ( + !( + faceDetails.origin && + faceDetails.x_axis && + faceDetails.y_axis && + faceDetails.z_axis + ) + ) { + continue + } + parent[lastKey] = createCallExpressionStdLib('startSketchOn', [ + createObjectExpression({ + plane: createObjectExpression({ + origin: createObjectExpression({ + x: roundLiteral(faceDetails.origin.x), + y: roundLiteral(faceDetails.origin.y), + z: roundLiteral(faceDetails.origin.z), + }), + x_axis: createObjectExpression({ + x: roundLiteral(faceDetails.x_axis.x), + y: roundLiteral(faceDetails.x_axis.y), + z: roundLiteral(faceDetails.x_axis.z), + }), + y_axis: createObjectExpression({ + x: roundLiteral(faceDetails.y_axis.x), + y: roundLiteral(faceDetails.y_axis.y), + z: roundLiteral(faceDetails.y_axis.z), + }), + z_axis: createObjectExpression({ + x: roundLiteral(faceDetails.z_axis.x), + y: roundLiteral(faceDetails.z_axis.y), + z: roundLiteral(faceDetails.z_axis.z), + }), + }), + }), + ]) + } + resolve(true) + }) + } + // await prom + return astClone + } else if (varDec.node.init.type === 'PipeExpression') { + const pipeBody = varDec.node.init.body + if ( + pipeBody[0].type === 'CallExpression' && + pipeBody[0].callee.name === 'startSketchOn' + ) { + // remove varDec + const varDecIndex = varDec.shallowPath[1][0] as number + astClone.body.splice(varDecIndex, 1) + return astClone + } + } + + return new Error('Selection not recognised, could not delete') +} diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index da1076f6d..64af27ea1 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -26,7 +26,11 @@ import { applyConstraintEqualLength, setEqualLengthInfo, } from 'components/Toolbar/EqualLength' -import { addStartProfileAt, extrudeSketch } from 'lang/modifyAst' +import { + addStartProfileAt, + deleteFromSelection, + extrudeSketch, +} from 'lang/modifyAst' import { getNodeFromPath } from '../lang/queryAst' import { applyConstraintEqualAngle, @@ -44,12 +48,14 @@ import { import { Models } from '@kittycad/lib/dist/types/src' import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig' import { err, trap } from 'lib/trap' -import { DefaultPlaneStr } from 'clientSideScene/sceneEntities' +import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities' import { Vector3 } from 'three' import { quaternionFromUpNForward } from 'clientSideScene/helpers' import { uuidv4 } from 'lib/utils' import { Coords2d } from 'lang/std/sketch' import { deleteSegment } from 'clientSideScene/ClientSideSceneComp' +import { executeAst } from 'useStore' +import toast from 'react-hot-toast' export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY' @@ -157,6 +163,9 @@ export type ModelingMachineEvent = type: 'Set selection' data: SetSelections } + | { + type: 'Delete selection' + } | { type: 'Sketch no face' } | { type: 'Toggle gui mode' } | { type: 'Cancel' } @@ -273,6 +282,13 @@ export const modelingMachine = createMachine( cond: 'Has exportable geometry', actions: 'Engine export', }, + + 'Delete selection': { + target: 'idle', + cond: 'has valid selection for deletion', + actions: ['AST delete selection'], + internal: true, + }, }, entry: 'reset client scene mouse handlers', @@ -963,6 +979,42 @@ export const modelingMachine = createMachine( editorManager.selectRange(updatedAst?.selections) } }, + 'AST delete selection': async ({ sketchDetails, selectionRanges }) => { + let ast = kclManager.ast + + const getScaledFaceDetails = async (entityId: string) => { + const faceDetails = await getFaceDetails(entityId) + if (err(faceDetails)) return {} + return { + ...faceDetails, + origin: { + x: faceDetails.origin.x / sceneInfra._baseUnitMultiplier, + y: faceDetails.origin.y / sceneInfra._baseUnitMultiplier, + z: faceDetails.origin.z / sceneInfra._baseUnitMultiplier, + }, + } + } + + const modifiedAst = await deleteFromSelection( + ast, + selectionRanges.codeBasedSelections[0], + kclManager.programMemory, + getScaledFaceDetails + ) + if (err(modifiedAst)) return + + const testExecute = await executeAst({ + ast: modifiedAst, + useFakeExecutor: true, + engineCommandManager, + }) + if (testExecute.errors.length) { + toast.error('Unable to delete part') + return + } + + await kclManager.updateAst(modifiedAst, true) + }, 'conditionally equip line tool': (_, { type }) => { if (type === 'done.invoke.animate-to-face') { sceneInfra.modelingSend('Equip Line tool')