make delete key work for solids (#2752)

* failing test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* failing test

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* push up progress

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* improve travers

* basic deleteFromSelection

* remove .only

* delete depended on extrude

* fix

* fix selection override

* add selection test

* Revert "add selection test"

This reverts commit 40a414b612.

* Revert "fix selection override"

This reverts commit 68e66e2980.

* more progress

* add toast message when we're not able to delet

* add e2e tests

* tweak test timeout

* more test tweaks

* fix back space cmd bar conflic

* clean up

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
This commit is contained in:
Jess Frazelle
2024-06-29 10:36:04 -07:00
committed by GitHub
parent f86473d13b
commit 9daf2d7794
8 changed files with 677 additions and 9 deletions

View File

@ -1894,6 +1894,239 @@ test.describe('Testing selections', () => {
await selectionSequence() 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 }) => { test('Hovering over 3d features highlights code', async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async (KCL_DEFAULT_LENGTH) => { await page.addInitScript(async (KCL_DEFAULT_LENGTH) => {

View File

@ -45,8 +45,8 @@ async function clearCommandLogs(page: Page) {
await page.getByTestId('clear-commands').click() await page.getByTestId('clear-commands').click()
} }
async function expectCmdLog(page: Page, locatorStr: string) { async function expectCmdLog(page: Page, locatorStr: string, timeout = 5000) {
await expect(page.locator(locatorStr).last()).toBeVisible() await expect(page.locator(locatorStr).last()).toBeVisible({ timeout })
} }
async function waitForDefaultPlanesToBeVisible(page: Page) { async function waitForDefaultPlanesToBeVisible(page: Page) {
@ -228,7 +228,8 @@ export async function getUtils(page: Page) {
await fillInput('z', xyz[2]) await fillInput('z', xyz[2])
}, },
clearCommandLogs: () => clearCommandLogs(page), clearCommandLogs: () => clearCommandLogs(page),
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr), expectCmdLog: (locatorStr: string, timeout = 5000) =>
expectCmdLog(page, locatorStr, timeout),
openKclCodePanel: () => openKclCodePanel(page), openKclCodePanel: () => openKclCodePanel(page),
closeKclCodePanel: () => closeKclCodePanel(page), closeKclCodePanel: () => closeKclCodePanel(page),
openDebugPanel: () => openDebugPanel(page), openDebugPanel: () => openDebugPanel(page),

View File

@ -1967,9 +1967,9 @@ export async function getSketchOrientationDetails(
* @param entityId - The ID of the entity for which orientation details are being fetched. * @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. * @returns A promise that resolves with the orientation details of the face.
*/ */
async function getFaceDetails( export async function getFaceDetails(
entityId: string entityId: string
): Promise<Models['FaceIsPlanar_type']> { ): Promise<Models['GetSketchModePlane_type']> {
// TODO mode engine connection to allow batching returns and batch the following // TODO mode engine connection to allow batching returns and batch the following
await engineCommandManager.sendSceneCommand({ await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
@ -1982,8 +1982,7 @@ async function getFaceDetails(
entity_id: entityId, entity_id: entityId,
}, },
}) })
// TODO change typing to get_sketch_mode_plane once lib is updated const faceInfo: Models['GetSketchModePlane_type'] = (
const faceInfo: Models['FaceIsPlanar_type'] = (
await engineCommandManager.sendSceneCommand({ await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req', type: 'modeling_cmd_req',
cmd_id: uuidv4(), cmd_id: uuidv4(),

View File

@ -23,6 +23,7 @@ import {
editorManager, editorManager,
sceneEntitiesManager, sceneEntitiesManager,
} from 'lib/singletons' } from 'lib/singletons'
import { useHotkeys } from 'react-hotkeys-hook'
import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance' import { applyConstraintHorzVertDistance } from './Toolbar/SetHorzVertDistance'
import { import {
angleBetweenInfo, angleBetweenInfo,
@ -78,6 +79,7 @@ import { getVarNameModal } from 'hooks/useToolbarGuards'
import useHotkeyWrapper from 'lib/hotkeyWrapper' import useHotkeyWrapper from 'lib/hotkeyWrapper'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -140,6 +142,7 @@ export const ModelingMachineProvider = ({
} }
) )
}) })
const { commandBarState } = useCommandsContext()
// Settings machine setup // Settings machine setup
// const retrievedSettings = useRef( // const retrievedSettings = useRef(
@ -464,6 +467,11 @@ export const ModelingMachineProvider = ({
return canExtrudeSelection(selectionRanges) 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 }) => { 'Sketch is empty': ({ sketchDetails }) => {
const node = getNodeFromPath<VariableDeclaration>( const node = getNodeFromPath<VariableDeclaration>(
kclManager.ast, kclManager.ast,
@ -927,6 +935,11 @@ export const ModelingMachineProvider = ({
} }
}, [modelingSend]) }, [modelingSend])
// Allow using the delete key to delete solids
useHotkeys(['backspace', 'delete', 'del'], () => {
modelingSend({ type: 'Delete selection' })
})
useStateMachineCommands({ useStateMachineCommands({
machineId: 'modeling', machineId: 'modeling',
state: modelingState, state: modelingState,

View File

@ -83,6 +83,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
if (!videoRef.current) return if (!videoRef.current) return
if (state.matches('Sketch')) return if (state.matches('Sketch')) return
if (state.matches('Sketch no face')) return if (state.matches('Sketch no face')) return
const { x, y } = getNormalisedCoordinates({ const { x, y } = getNormalisedCoordinates({
clientX: e.clientX, clientX: e.clientX,
clientY: e.clientY, clientY: e.clientY,

View File

@ -15,6 +15,7 @@ import {
sketchOnExtrudedFace, sketchOnExtrudedFace,
deleteSegmentFromPipeExpression, deleteSegmentFromPipeExpression,
removeSingleConstraintInfo, removeSingleConstraintInfo,
deleteFromSelection,
} from './modifyAst' } from './modifyAst'
import { enginelessExecutor } from '../lib/testHelpers' import { enginelessExecutor } from '../lib/testHelpers'
import { findUsesOfTagInPipe, getNodePathFromSourceRange } from './queryAst' 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)
}
)
})

View File

@ -17,6 +17,7 @@ import {
PathToNode, PathToNode,
ProgramMemory, ProgramMemory,
SourceRange, SourceRange,
SketchGroup,
} from './wasm' } from './wasm'
import { import {
isNodeSafeToReplacePath, isNodeSafeToReplacePath,
@ -25,6 +26,7 @@ import {
getNodeFromPath, getNodeFromPath,
getNodePathFromSourceRange, getNodePathFromSourceRange,
isNodeSafeToReplace, isNodeSafeToReplace,
traverse,
} from './queryAst' } from './queryAst'
import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch' import { addTagForSketchOnFace, getConstraintInfo } from './std/sketch'
import { import {
@ -38,6 +40,7 @@ import { isOverlap, roundOff } from 'lib/utils'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants' import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
import { ConstrainInfo } from './std/stdTypes' import { ConstrainInfo } from './std/stdTypes'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator' import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { Models } from '@kittycad/lib'
export function startSketchOnDefault( export function startSketchOnDefault(
node: Program, node: Program,
@ -873,3 +876,175 @@ export function removeSingleConstraintInfo(
if (err(retval)) return false if (err(retval)) return false
return retval return retval
} }
export async function deleteFromSelection(
ast: Program,
selection: Selection,
programMemory: ProgramMemory,
getFaceDetails: (id: string) => Promise<Models['FaceIsPlanar_type']> = () =>
({} as any)
): Promise<Program | Error> {
const astClone = JSON.parse(JSON.stringify(ast))
const range = selection.range
const path = getNodePathFromSourceRange(ast, range)
const varDec = getNodeFromPath<VariableDeclarator>(
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<PipeExpression['body']>(
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')
}

View File

@ -26,7 +26,11 @@ import {
applyConstraintEqualLength, applyConstraintEqualLength,
setEqualLengthInfo, setEqualLengthInfo,
} from 'components/Toolbar/EqualLength' } from 'components/Toolbar/EqualLength'
import { addStartProfileAt, extrudeSketch } from 'lang/modifyAst' import {
addStartProfileAt,
deleteFromSelection,
extrudeSketch,
} from 'lang/modifyAst'
import { getNodeFromPath } from '../lang/queryAst' import { getNodeFromPath } from '../lang/queryAst'
import { import {
applyConstraintEqualAngle, applyConstraintEqualAngle,
@ -44,12 +48,14 @@ import {
import { Models } from '@kittycad/lib/dist/types/src' import { Models } from '@kittycad/lib/dist/types/src'
import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig' import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig'
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { DefaultPlaneStr } from 'clientSideScene/sceneEntities' import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
import { Vector3 } from 'three' import { Vector3 } from 'three'
import { quaternionFromUpNForward } from 'clientSideScene/helpers' import { quaternionFromUpNForward } from 'clientSideScene/helpers'
import { uuidv4 } from 'lib/utils' import { uuidv4 } from 'lib/utils'
import { Coords2d } from 'lang/std/sketch' import { Coords2d } from 'lang/std/sketch'
import { deleteSegment } from 'clientSideScene/ClientSideSceneComp' import { deleteSegment } from 'clientSideScene/ClientSideSceneComp'
import { executeAst } from 'useStore'
import toast from 'react-hot-toast'
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY' export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
@ -157,6 +163,9 @@ export type ModelingMachineEvent =
type: 'Set selection' type: 'Set selection'
data: SetSelections data: SetSelections
} }
| {
type: 'Delete selection'
}
| { type: 'Sketch no face' } | { type: 'Sketch no face' }
| { type: 'Toggle gui mode' } | { type: 'Toggle gui mode' }
| { type: 'Cancel' } | { type: 'Cancel' }
@ -273,6 +282,13 @@ export const modelingMachine = createMachine(
cond: 'Has exportable geometry', cond: 'Has exportable geometry',
actions: 'Engine export', 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', entry: 'reset client scene mouse handlers',
@ -963,6 +979,42 @@ export const modelingMachine = createMachine(
editorManager.selectRange(updatedAst?.selections) 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 }) => { 'conditionally equip line tool': (_, { type }) => {
if (type === 'done.invoke.animate-to-face') { if (type === 'done.invoke.animate-to-face') {
sceneInfra.modelingSend('Equip Line tool') sceneInfra.modelingSend('Equip Line tool')