Add Delete to right-click context menu of feature tree operations (#5302)

* Revert "Revert multi-profile (#4812)"

This reverts commit efe8089b08.

* fix poor 1000ms wait UX

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores)

* trigger CI

* Add Rust side artifacts for startSketchOn face or plane (#4834)

* Add Rust side artifacts for startSketchOn face or plane

* move ast digging

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>

* lint

* lint

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-macos-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores)

* trigger CI

* chore: disabled file watcher which prevents faster file write (#4835)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* partial fixes

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* Fix up all the tests

* Fix partial execution

* wip

* WIP

* wip

* rust changes to make three point confrom to same as others since we're not ready with name params yet

* most of the fix for 3 point circle

* get overlays working for circle three point

* fmt

* fix types

* cargo fmt

* add face codef ref for walls and caps

* fix sketch on face after updates to rust side artifact graph

* some things needed for multi-profile tests

* bad attempts at fixing rust

* more

* more

* fix rust

* more rust fixes

* overlay fix

* remove duplicate test

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* lint and typing

* maybe fix a unit test

* small thing

* WIP: Add Delete right click menu item to Feature Tree
Copying code around
Fixes #5090

* I don't know why it works

* WIP

* fix circ dep

* fix unit test

* fix some tests

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Working deletion machine loo

* Working helix deletion

* Extend deletion to more things

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* fix sweep point-and-click test

* fix more tests and add a fix me

* fix more tests

* fix electron specific test

* tsc

* more test tweaks

* update docs

* commint snaps?

* is clippy happy now?

* clippy again

* test works now without me changing anything big-fixed-itself

* small bug

* make three point have cross hair to make it consistent with othe rtools

* fix up state diagram

* fmt

* add draft point for first click of three point circ

* 1 test for three point circle

* 2 test for three point circle

* clean up

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* remove bad doc comment

* remove test skip

* remove onboarding test changes

* Update src/lang/modifyAst.ts

Co-authored-by: Jonathan Tran <jonnytran@gmail.com>

* Update output from simulation tests

* Fix to use correct source ranges

This also reduces cloning.

* Change back to skipping face cap none and both

* Update output after changing back to skipping none and both

* Fix clippy warning

* fix profile start snap bug

* WIP: migrate to actor

* add path ids to cap

* fix going into edit sketch

* make other startSketchOn's work

* fix snapshot test

* explain function name

* Update src/lib/rectangleTool.ts

Co-authored-by: Frank Noirot <frank@zoo.dev>

* rename error

* remove file tree from diff

* Update src/clientSideScene/segments.ts

Co-authored-by: Frank Noirot <frank@zoo.dev>

* nit

* Continue actor migration

* Prevent double write to KCL code on revolve

* Clean up

* Update output after adding cap-to-path graph edge

* Clean up

* Update machine diag

* Update context menu hotkey class

* Fix edit/select sketch-on-cap via feature tree

* clean up for face codeRef

* fix changing tools part way through circle/rect tools

* fix delete of circle profile

* fix close profiles

* fix closing profile bug (tangentArcTo being ignored)

* remove stale comment

* Delete paths associated with sketch when the sketch plane is deleted

* Add support for deleting sketches on caps (not walls)

* get delet working for walls

* make delet of extrusions work for multi profile

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Delete the sketch statement too on the cap and wall cases

* Don't write to file in `split-sketch-pipe-if-needed` unless necessary

* Don't wait for file write to complete within `updateEditorWithAstAndWriteToFile`
It is already debounced internally. If we await it, we will have to wait for a debounced timeout

* Fix bad conflict resolution

* Fix a few things post merge

* Add guard back, fixing tests

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Add e2e test

* Working tests on ubuntu

* Another one

* Update src/machines/featureTreeMachine.ts

Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>

* Fix sketch test
@Irev-Dev's suggestion

---------

Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
Co-authored-by: Kevin Nadro <nadr0@users.noreply.github.com>
Co-authored-by: 49lf <ircsurfer33@gmail.com>
Co-authored-by: Frank Noirot <frank@zoo.dev>
Co-authored-by: Frank Noirot <frankjohnson1993@gmail.com>
This commit is contained in:
Pierre Jacquier
2025-02-19 13:43:27 -05:00
committed by GitHub
parent b98f5605b6
commit b2e1d21d45
8 changed files with 332 additions and 53 deletions

View File

@ -5,6 +5,7 @@ import { ToolbarFixture } from './fixtures/toolbarFixture'
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
import { getUtils } from './test-utils' import { getUtils } from './test-utils'
import { Locator } from '@playwright/test'
// test file is for testing point an click code gen functionality that's not sketch mode related // test file is for testing point an click code gen functionality that's not sketch mode related
@ -2506,6 +2507,94 @@ extrude002 = extrude(sketch002, length = 50)
}) })
}) })
const shellPointAndClickDeletionCases = [
{ shouldUseKeyboard: true },
{ shouldUseKeyboard: false },
]
shellPointAndClickDeletionCases.forEach(({ shouldUseKeyboard }) => {
test(`Shell point-and-click deletion (shouldUseKeyboard: ${shouldUseKeyboard})`, async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
const sketchCode = `sketch001 = startSketchOn('XY')
profile001 = startProfileAt([-20, 20], sketch001)
|> xLine(40, %)
|> yLine(-60, %)
|> xLine(-40, %)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
`
const extrudeCode = `extrude001 = extrude(profile001, length = 40)
`
const shellCode = `shell001 = shell(extrude001, faces = ['end'], thickness = 5)
`
const initialCode = sketchCode + extrudeCode + shellCode
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
await toolbar.openPane('feature-tree')
// One dumb hardcoded screen pixel value
const testPoint = { x: 590, y: 400 }
const extrudeColor: [number, number, number] = [100, 100, 100]
const sketchColor: [number, number, number] = [140, 140, 140]
const defaultPlaneColor: [number, number, number] = [50, 50, 100]
const deleteOperation = async (operationButton: Locator) => {
if (shouldUseKeyboard) {
await operationButton.click({ button: 'left' })
await page.keyboard.press('Backspace')
} else {
await operationButton.click({ button: 'right' })
const editButton = page.getByTestId('context-menu-delete')
await editButton.click()
}
}
await test.step(`Look for the grey of the extrude shape`, async () => {
await scene.expectPixelColor(extrudeColor, testPoint, 20)
})
await test.step('Delete shell and confirm deletion', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Shell',
0
)
await deleteOperation(operationButton)
await scene.expectPixelColor(extrudeColor, testPoint, 20)
await editor.expectEditor.not.toContain(shellCode)
})
await test.step('Delete extrude and confirm deletion', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Extrude',
0
)
await deleteOperation(operationButton)
await editor.expectEditor.not.toContain(extrudeCode)
await scene.expectPixelColor(sketchColor, testPoint, 20)
})
await test.step('Delete sketch and confirm empty scene', async () => {
const operationButton = await toolbar.getFeatureTreeOperation(
'Sketch',
0
)
await deleteOperation(operationButton)
await editor.expectEditor.toContain('')
await scene.expectPixelColor(defaultPlaneColor, testPoint, 20)
})
})
})
test(`Shell dry-run validation rejects sweeps`, async ({ test(`Shell dry-run validation rejects sweeps`, async ({
context, context,
page, page,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -1,7 +1,6 @@
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { ActionIcon, ActionIconProps } from './ActionIcon' import { ActionIcon, ActionIconProps } from './ActionIcon'
import { import {
MouseEvent,
RefObject, RefObject,
useCallback, useCallback,
useEffect, useEffect,
@ -148,24 +147,22 @@ interface ContextMenuItemProps {
onClick?: () => void onClick?: () => void
hotkey?: string hotkey?: string
'data-testid'?: string 'data-testid'?: string
disabled?: boolean
} }
export function ContextMenuItem(props: ContextMenuItemProps) { export function ContextMenuItem(props: ContextMenuItemProps) {
const { children, icon, onClick, hotkey } = props const { children, icon, onClick, hotkey, disabled } = props
return ( return (
<button <button
disabled={disabled}
data-testid={props['data-testid']} data-testid={props['data-testid']}
className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left" className="flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-chalkboard-20 dark:hover:bg-chalkboard-80 border-none text-left"
onClick={onClick} onClick={disabled ? undefined : onClick}
> >
{icon && <ActionIcon icon={icon} bgClassName="!bg-transparent" />} {icon && <ActionIcon icon={icon} bgClassName="!bg-transparent" />}
<div className="flex-1">{children}</div> <div className="flex-1">{children}</div>
{hotkey && ( {hotkey && <kbd className="hotkey">{hotkey}</kbd>}
<kbd className="px-1.5 py-0.5 rounded bg-primary/10 text-primary dark:bg-chalkboard-80 dark:text-chalkboard-40">
{hotkey}
</kbd>
)}
</button> </button>
) )
} }

View File

@ -324,6 +324,20 @@ const OperationItem = (props: {
} }
} }
function deleteOperation() {
if (
props.item.type === 'StdLibCall' ||
props.item.type === 'UserDefinedFunctionCall'
) {
props.send({
type: 'deleteOperation',
data: {
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
},
})
}
}
const menuItems = useMemo( const menuItems = useMemo(
() => [ () => [
<ContextMenuItem <ContextMenuItem
@ -364,14 +378,24 @@ const OperationItem = (props: {
</ContextMenuItem>, </ContextMenuItem>,
] ]
: []), : []),
...(props.item.type === 'StdLibCall' && ...(props.item.type === 'StdLibCall'
stdLibMap[props.item.name]?.prepareToEdit
? [ ? [
<ContextMenuItem onClick={enterEditFlow}> <ContextMenuItem
Edit {name} disabled={!stdLibMap[props.item.name]?.prepareToEdit}
onClick={enterEditFlow}
hotkey="Double click"
>
Edit
</ContextMenuItem>, </ContextMenuItem>,
] ]
: []), : []),
<ContextMenuItem
onClick={deleteOperation}
hotkey="Delete"
data-testid="context-menu-delete"
>
Delete
</ContextMenuItem>,
], ],
[props.item, props.send] [props.item, props.send]
) )

View File

@ -0,0 +1,38 @@
import { Selection } from 'lib/selections'
import { getFaceDetails } from 'clientSideScene/sceneEntities'
import { deleteFromSelection } from 'lang/modifyAst'
import { codeManager, engineCommandManager, kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { executeAst } from 'lang/langHelpers'
export const deletionErrorMessage =
'Unable to delete selection. Please edit manually in code pane.'
export async function deleteSelectionPromise(
selection: Selection
): Promise<Error | void> {
let ast = kclManager.ast
const modifiedAst = await deleteFromSelection(
ast,
selection,
kclManager.variables,
engineCommandManager.artifactGraph,
getFaceDetails
)
if (err(modifiedAst)) {
return new Error(deletionErrorMessage)
}
const testExecute = await executeAst({
ast: modifiedAst,
engineCommandManager,
isMock: true,
})
if (testExecute.errors.length) {
return new Error(deletionErrorMessage)
}
await kclManager.updateAst(modifiedAst, true)
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
}

View File

@ -106,6 +106,7 @@ export type ModelingCommandSchema = {
prompt: string prompt: string
selection: Selections selection: Selections
} }
'Delete selection': {}
} }
export const modelingMachineCommandConfig: StateMachineCommandSetConfig< export const modelingMachineCommandConfig: StateMachineCommandSetConfig<

File diff suppressed because one or more lines are too long

View File

@ -44,7 +44,6 @@ import {
addHelix, addHelix,
addOffsetPlane, addOffsetPlane,
addSweep, addSweep,
deleteFromSelection,
extrudeSketch, extrudeSketch,
loftSketches, loftSketches,
} from 'lang/modifyAst' } from 'lang/modifyAst'
@ -70,12 +69,10 @@ import {
} from 'components/Toolbar/SetAbsDistance' } from 'components/Toolbar/SetAbsDistance'
import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig' import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig'
import { err, reportRejection, trap } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
import { getFaceDetails } from 'clientSideScene/sceneEntities'
import { DefaultPlaneStr } from 'lib/planes' import { DefaultPlaneStr } from 'lib/planes'
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 'lang/langHelpers'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { ToolbarModeName } from 'lib/toolbar' import { ToolbarModeName } from 'lib/toolbar'
import { quaternionFromUpNForward } from 'clientSideScene/helpers' import { quaternionFromUpNForward } from 'clientSideScene/helpers'
@ -83,6 +80,11 @@ import { Mesh, Vector3 } from 'three'
import { MachineManager } from 'components/MachineManagerProvider' import { MachineManager } from 'components/MachineManagerProvider'
import { addShell } from 'lang/modifyAst/addShell' import { addShell } from 'lang/modifyAst/addShell'
import { KclCommandValue } from 'lib/commandTypes' import { KclCommandValue } from 'lib/commandTypes'
import { ModelingMachineContext } from 'components/ModelingMachineProvider'
import {
deleteSelectionPromise,
deletionErrorMessage,
} from 'lang/modifyAst/deleteSelection'
import { getPathsFromPlaneArtifact } from 'lang/std/artifactGraph' import { getPathsFromPlaneArtifact } from 'lang/std/artifactGraph'
import { createProfileStartHandle } from 'clientSideScene/segments' import { createProfileStartHandle } from 'clientSideScene/segments'
import { DRAFT_POINT } from 'clientSideScene/sceneInfra' import { DRAFT_POINT } from 'clientSideScene/sceneInfra'
@ -308,6 +310,10 @@ export type ModelingMachineEvent =
| { type: 'Helix'; data: ModelingCommandSchema['Helix'] } | { type: 'Helix'; data: ModelingCommandSchema['Helix'] }
| { type: 'Text-to-CAD'; data: ModelingCommandSchema['Text-to-CAD'] } | { type: 'Text-to-CAD'; data: ModelingCommandSchema['Text-to-CAD'] }
| { type: 'Prompt-to-edit'; data: ModelingCommandSchema['Prompt-to-edit'] } | { type: 'Prompt-to-edit'; data: ModelingCommandSchema['Prompt-to-edit'] }
| {
type: 'Delete selection'
data: ModelingCommandSchema['Delete selection']
}
| { | {
type: 'Add rectangle origin' type: 'Add rectangle origin'
data: [x: number, y: number] data: [x: number, y: number]
@ -731,38 +737,6 @@ export const modelingMachine = setup({
} }
})().catch(reportRejection) })().catch(reportRejection)
}, },
'AST delete selection': ({ context: { selectionRanges } }) => {
;(async () => {
const errorMessage =
'Unable to delete selection. Please edit manually in code pane.'
let ast = kclManager.ast
const modifiedAst = await deleteFromSelection(
ast,
selectionRanges.graphSelections[0],
kclManager.variables,
engineCommandManager.artifactGraph,
getFaceDetails
)
if (err(modifiedAst)) {
toast.error(errorMessage)
return
}
const testExecute = await executeAst({
ast: modifiedAst,
engineCommandManager,
isMock: true,
})
if (testExecute.errors.length) {
toast.error(errorMessage)
return
}
await kclManager.updateAst(modifiedAst, true)
await codeManager.updateEditorWithAstAndWriteToFile(modifiedAst)
})().catch(reportRejection)
},
'set selection filter to curves only': () => { 'set selection filter to curves only': () => {
;(async () => { ;(async () => {
await engineCommandManager.sendSceneCommand({ await engineCommandManager.sendSceneCommand({
@ -2170,6 +2144,34 @@ export const modelingMachine = setup({
input: ModelingCommandSchema['Prompt-to-edit'] input: ModelingCommandSchema['Prompt-to-edit']
}) => {} }) => {}
), ),
deleteSelectionAstMod: fromPromise(
({
input: { selectionRanges },
}: {
input: { selectionRanges: Selections }
}) => {
return new Promise((resolve, reject) => {
if (!selectionRanges) {
reject(new Error(deletionErrorMessage))
}
const selection = selectionRanges.graphSelections[0]
if (!selectionRanges) {
reject(new Error(deletionErrorMessage))
}
deleteSelectionPromise(selection)
.then((result) => {
if (err(result)) {
reject(result)
return
}
resolve(result)
})
.catch(reject)
})
}
),
}, },
// end actors // end actors
}).createMachine({ }).createMachine({
@ -2243,10 +2245,9 @@ export const modelingMachine = setup({
}, },
'Delete selection': { 'Delete selection': {
target: 'idle', target: 'Applying Delete selection',
guard: 'has valid selection for deletion', guard: 'has valid selection for deletion',
actions: ['AST delete selection'], reenter: true,
reenter: false,
}, },
'Text-to-CAD': { 'Text-to-CAD': {
@ -3366,6 +3367,28 @@ export const modelingMachine = setup({
onError: 'idle', onError: 'idle',
}, },
}, },
'Applying Delete selection': {
invoke: {
src: 'deleteSelectionAstMod',
id: 'deleteSelectionAstMod',
input: ({ event, context }) => {
return { selectionRanges: context.selectionRanges }
},
onDone: 'idle',
onError: {
target: 'idle',
reenter: true,
actions: ({ event }) => {
if ('error' in event && err(event.error)) {
toast.error(event.error.message)
}
},
},
},
},
}, },
initial: 'idle', initial: 'idle',