Extend point-and-click edit flow to non-pipe Chamfer and Fillet (#6767)

* enable non-piped fillets and chamfers

* reorder chamferAstMod

* tsc

* editEdgeTreatment + refactor + hookup

* remove unused stuff

* test

* typos

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* else else else

* Apply suggestions from code review

pierre edits

Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>

* const

* parameterName

* fmt

* efficiency !

* graphite being helpful

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
This commit is contained in:
max
2025-05-08 22:16:36 +02:00
committed by GitHub
parent e2fd3948f5
commit c8747bd55a
5 changed files with 245 additions and 123 deletions

View File

@ -2321,11 +2321,12 @@ extrude001 = extrude(sketch001, length = -12)
}) })
}) })
test(`Fillet point-and-click edit rejected when not in pipe`, async ({ test(`Fillet point-and-click edit standalone expression`, async ({
context, context,
page, page,
homePage, homePage,
scene, scene,
editor,
toolbar, toolbar,
cmdBar, cmdBar,
}) => { }) => {
@ -2339,23 +2340,44 @@ profile001 = circle(
extrude001 = extrude(profile001, length = 100) extrude001 = extrude(profile001, length = 100)
fillet001 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg01)]) fillet001 = fillet(extrude001, radius = 5, tags = [getOppositeEdge(seg01)])
` `
await context.addInitScript((initialCode) => { await test.step(`Initial test setup`, async () => {
localStorage.setItem('persistCode', initialCode) await context.addInitScript((initialCode) => {
}, initialCode) localStorage.setItem('persistCode', initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 }) }, initialCode)
await homePage.goToModelingScene() await page.setBodyDimensions({ width: 1000, height: 500 })
await scene.settled(cmdBar) await homePage.goToModelingScene()
await scene.settled(cmdBar)
await test.step('Double-click in feature tree and expect error toast', async () => { })
await test.step('Edit fillet', async () => {
await toolbar.openPane('feature-tree') await toolbar.openPane('feature-tree')
await toolbar.closePane('code')
const operationButton = await toolbar.getFeatureTreeOperation('Fillet', 0) const operationButton = await toolbar.getFeatureTreeOperation('Fillet', 0)
await operationButton.dblclick({ button: 'left' }) await operationButton.dblclick({ button: 'left' })
await expect( await cmdBar.expectState({
page.getByText( commandName: 'Fillet',
'Only chamfer and fillet in pipe expressions are supported for edits' currentArgKey: 'radius',
) currentArgValue: '5',
).toBeVisible() headerArguments: {
await page.waitForTimeout(1000) Radius: '5',
},
highlightedHeaderArg: 'radius',
stage: 'arguments',
})
await page.keyboard.insertText('20')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Radius: '20',
},
commandName: 'Fillet',
})
await cmdBar.progressCmdBar()
})
await test.step('Confirm changes', async () => {
await toolbar.openPane('code')
await toolbar.closePane('feature-tree')
await editor.expectEditor.toContain('radius = 20')
}) })
}) })

View File

@ -371,7 +371,7 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
} }
// apply edge treatment to selection // apply edge treatment to selection
const result = modifyAstWithEdgeTreatmentAndTag( const result = await modifyAstWithEdgeTreatmentAndTag(
ast, ast,
selection, selection,
parameters, parameters,

View File

@ -12,7 +12,6 @@ import {
createLocalName, createLocalName,
createPipeExpression, createPipeExpression,
} from '@src/lang/create' } from '@src/lang/create'
import { updateModelingState } from '@src/lang/modelingWorkflows'
import { import {
getNodeFromPath, getNodeFromPath,
hasSketchPipeBeenExtruded, hasSketchPipeBeenExtruded,
@ -39,7 +38,6 @@ import type {
VariableDeclarator, VariableDeclarator,
} from '@src/lang/wasm' } from '@src/lang/wasm'
import type { KclCommandValue } from '@src/lib/commandTypes' import type { KclCommandValue } from '@src/lib/commandTypes'
import { EXECUTION_TYPE_REAL } from '@src/lib/constants'
import type { Selection, Selections } from '@src/lib/selections' import type { Selection, Selections } from '@src/lib/selections'
import { err } from '@src/lib/trap' import { err } from '@src/lib/trap'
import { isArray } from '@src/lib/utils' import { isArray } from '@src/lib/utils'
@ -65,43 +63,7 @@ export interface FilletParameters {
export type EdgeTreatmentParameters = ChamferParameters | FilletParameters export type EdgeTreatmentParameters = ChamferParameters | FilletParameters
// Apply Edge Treatment (Fillet or Chamfer) To Selection // Apply Edge Treatment (Fillet or Chamfer) To Selection
export async function applyEdgeTreatmentToSelection( export async function modifyAstWithEdgeTreatmentAndTag(
ast: Node<Program>,
selection: Selections,
parameters: EdgeTreatmentParameters,
dependencies: {
kclManager: KclManager
engineCommandManager: EngineCommandManager
editorManager: EditorManager
codeManager: CodeManager
}
): Promise<void | Error> {
// 1. clone and modify with edge treatment and tag
const result = modifyAstWithEdgeTreatmentAndTag(
ast,
selection,
parameters,
dependencies
)
if (err(result)) return result
const { modifiedAst, pathToEdgeTreatmentNode } = result
// 2. update ast
await updateModelingState(
modifiedAst,
EXECUTION_TYPE_REAL,
{
kclManager: dependencies.kclManager,
editorManager: dependencies.editorManager,
codeManager: dependencies.codeManager,
},
{
focusPath: pathToEdgeTreatmentNode,
}
)
}
export function modifyAstWithEdgeTreatmentAndTag(
ast: Node<Program>, ast: Node<Program>,
selections: Selections, selections: Selections,
parameters: EdgeTreatmentParameters, parameters: EdgeTreatmentParameters,
@ -111,9 +73,9 @@ export function modifyAstWithEdgeTreatmentAndTag(
editorManager: EditorManager editorManager: EditorManager
codeManager: CodeManager codeManager: CodeManager
} }
): ): Promise<
| { modifiedAst: Node<Program>; pathToEdgeTreatmentNode: Array<PathToNode> } { modifiedAst: Node<Program>; pathToEdgeTreatmentNode: PathToNode[] } | Error
| Error { > {
let clonedAst = structuredClone(ast) let clonedAst = structuredClone(ast)
const clonedAstForGetExtrude = structuredClone(ast) const clonedAstForGetExtrude = structuredClone(ast)
@ -784,3 +746,47 @@ export async function deleteEdgeTreatment(
return Error('Delete fillets not implemented') return Error('Delete fillets not implemented')
} }
// Edit Edge Treatment
export async function editEdgeTreatment(
ast: Node<Program>,
selection: Selection,
parameters: EdgeTreatmentParameters
): Promise<
{ modifiedAst: Node<Program>; pathToEdgeTreatmentNode: PathToNode } | Error
> {
// 1. clone and modify with new value
const modifiedAst = structuredClone(ast)
// find the edge treatment call
const edgeTreatmentCall = getNodeFromPath<CallExpressionKw>(
modifiedAst,
selection?.codeRef?.pathToNode,
'CallExpressionKw'
)
if (err(edgeTreatmentCall)) return edgeTreatmentCall
// edge treatment parameter
const parameterResult = getParameterNameAndValue(parameters)
if (err(parameterResult)) return parameterResult
const { parameterName, parameterValue } = parameterResult
// find the index of an argument to update
const index = edgeTreatmentCall.node.arguments.findIndex(
(arg) => arg.label.name === parameterName
)
// create a new argument with the updated value
const newArg = createLabeledArg(parameterName, parameterValue)
// if the parameter doesn't exist, add it; otherwise replace it
if (index === -1) {
edgeTreatmentCall.node.arguments.push(newArg)
} else {
edgeTreatmentCall.node.arguments[index] = newArg
}
let pathToEdgeTreatmentNode = selection?.codeRef?.pathToNode
return { modifiedAst, pathToEdgeTreatmentNode }
}

View File

@ -166,15 +166,6 @@ const prepareToEditEdgeTreatment: PrepareToEditCallback = async ({
kclManager.ast, kclManager.ast,
sourceRangeFromRust(operation.sourceRange) sourceRangeFromRust(operation.sourceRange)
) )
const isPipeExpression = nodeToEdit.some(
([_, type]) => type === 'PipeExpression'
)
if (!isPipeExpression) {
return {
reason:
'Only chamfer and fillet in pipe expressions are supported for edits',
}
}
let argDefaultValues: let argDefaultValues:
| ModelingCommandSchema['Chamfer'] | ModelingCommandSchema['Chamfer']

View File

@ -58,7 +58,8 @@ import type {
} from '@src/lang/modifyAst/addEdgeTreatment' } from '@src/lang/modifyAst/addEdgeTreatment'
import { import {
EdgeTreatmentType, EdgeTreatmentType,
applyEdgeTreatmentToSelection, modifyAstWithEdgeTreatmentAndTag,
editEdgeTreatment,
getPathToExtrudeForSegmentSelection, getPathToExtrudeForSegmentSelection,
mutateAstWithTagForSketchSegment, mutateAstWithTagForSketchSegment,
} from '@src/lang/modifyAst/addEdgeTreatment' } from '@src/lang/modifyAst/addEdgeTreatment'
@ -133,7 +134,6 @@ import {
import type { ToolbarModeName } from '@src/lib/toolbar' import type { ToolbarModeName } from '@src/lib/toolbar'
import { err, reportRejection, trap } from '@src/lib/trap' import { err, reportRejection, trap } from '@src/lib/trap'
import { uuidv4 } from '@src/lib/utils' import { uuidv4 } from '@src/lib/utils'
import { deleteNodeInExtrudePipe } from '@src/lang/modifyAst/deleteNodeInExtrudePipe'
import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement' import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement'
export type SetSelections = export type SetSelections =
@ -2311,18 +2311,107 @@ export const modelingMachine = setup({
// Extract inputs // Extract inputs
const ast = kclManager.ast const ast = kclManager.ast
let modifiedAst = structuredClone(ast)
let focusPath: PathToNode[] = []
const { nodeToEdit, selection, radius } = input const { nodeToEdit, selection, radius } = input
// If this is an edit flow, first we're going to remove the old node
if (nodeToEdit) {
const oldNodeDeletion = deleteNodeInExtrudePipe(nodeToEdit, ast)
if (err(oldNodeDeletion)) return oldNodeDeletion
}
const parameters: FilletParameters = { const parameters: FilletParameters = {
type: EdgeTreatmentType.Fillet, type: EdgeTreatmentType.Fillet,
radius, radius,
} }
const dependencies = {
kclManager,
engineCommandManager,
editorManager,
codeManager,
}
// Apply or edit fillet
if (nodeToEdit) {
// Edit existing fillet
// selection is not the edge treatment itself,
// but just the first edge in the fillet expression >
// we need to find the edgeCut artifact
// and build a new selection from it
// TODO: this is a bit of a hack, we should be able
// to get the edgeCut artifact from the selection
const firstSelection = selection.graphSelections[0]
const edgeCutArtifact = Array.from(
kclManager.artifactGraph.values()
).find(
(artifact) =>
artifact.type === 'edgeCut' &&
artifact.consumedEdgeId === firstSelection.artifact?.id
)
if (!edgeCutArtifact || edgeCutArtifact.type !== 'edgeCut') {
return Promise.reject(
new Error(
'Failed to retrieve edgeCut artifact from sweepEdge selection'
)
)
}
const edgeTreatmentSelection = {
artifact: edgeCutArtifact,
codeRef: edgeCutArtifact.codeRef,
}
const editResult = await editEdgeTreatment(
ast,
edgeTreatmentSelection,
parameters
)
if (err(editResult)) return Promise.reject(editResult)
modifiedAst = editResult.modifiedAst
focusPath = [editResult.pathToEdgeTreatmentNode]
} else {
// Apply fillet to selection
const filletResult = await modifyAstWithEdgeTreatmentAndTag(
ast,
selection,
parameters,
dependencies
)
if (err(filletResult)) return Promise.reject(filletResult)
modifiedAst = filletResult.modifiedAst
focusPath = filletResult.pathToEdgeTreatmentNode
}
await updateModelingState(
modifiedAst,
EXECUTION_TYPE_REAL,
{
kclManager,
editorManager,
codeManager,
},
{
focusPath: focusPath,
}
)
}
),
chamferAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Chamfer'] | undefined
}) => {
if (!input) {
return Promise.reject(new Error('No input provided'))
}
// Extract inputs
const ast = kclManager.ast
let modifiedAst = structuredClone(ast)
let focusPath: PathToNode[] = []
const { nodeToEdit, selection, length } = input
const parameters: ChamferParameters = {
type: EdgeTreatmentType.Chamfer,
length,
}
const dependencies = { const dependencies = {
kclManager, kclManager,
engineCommandManager, engineCommandManager,
@ -2330,14 +2419,69 @@ export const modelingMachine = setup({
codeManager, codeManager,
} }
// Apply fillet to selection // Apply or edit chamfer
const filletResult = await applyEdgeTreatmentToSelection( if (nodeToEdit) {
ast, // Edit existing chamfer
selection, // selection is not the edge treatment itself,
parameters, // but just the first edge in the chamfer expression >
dependencies // we need to find the edgeCut artifact
// and build a new selection from it
// TODO: this is a bit of a hack, we should be able
// to get the edgeCut artifact from the selection
const firstSelection = selection.graphSelections[0]
const edgeCutArtifact = Array.from(
kclManager.artifactGraph.values()
).find(
(artifact) =>
artifact.type === 'edgeCut' &&
artifact.consumedEdgeId === firstSelection.artifact?.id
)
if (!edgeCutArtifact || edgeCutArtifact.type !== 'edgeCut') {
return Promise.reject(
new Error(
'Failed to retrieve edgeCut artifact from sweepEdge selection'
)
)
}
const edgeTreatmentSelection = {
artifact: edgeCutArtifact,
codeRef: edgeCutArtifact.codeRef,
}
const editResult = await editEdgeTreatment(
ast,
edgeTreatmentSelection,
parameters
)
if (err(editResult)) return Promise.reject(editResult)
modifiedAst = editResult.modifiedAst
focusPath = [editResult.pathToEdgeTreatmentNode]
} else {
// Apply chamfer to selection
const chamferResult = await modifyAstWithEdgeTreatmentAndTag(
ast,
selection,
parameters,
dependencies
)
if (err(chamferResult)) return Promise.reject(chamferResult)
modifiedAst = chamferResult.modifiedAst
focusPath = chamferResult.pathToEdgeTreatmentNode
}
await updateModelingState(
modifiedAst,
EXECUTION_TYPE_REAL,
{
kclManager,
editorManager,
codeManager,
},
{
focusPath: focusPath,
}
) )
if (err(filletResult)) return filletResult
} }
), ),
'actor.parameter.create': fromPromise( 'actor.parameter.create': fromPromise(
@ -2461,47 +2605,6 @@ export const modelingMachine = setup({
return {} as SketchDetailsUpdate return {} as SketchDetailsUpdate
} }
), ),
chamferAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Chamfer'] | undefined
}) => {
if (!input) {
return new Error('No input provided')
}
// Extract inputs
const ast = kclManager.ast
const { nodeToEdit, selection, length } = input
// If this is an edit flow, first we're going to remove the old node
if (nodeToEdit) {
const oldNodeDeletion = deleteNodeInExtrudePipe(nodeToEdit, ast)
if (err(oldNodeDeletion)) return oldNodeDeletion
}
const parameters: ChamferParameters = {
type: EdgeTreatmentType.Chamfer,
length,
}
const dependencies = {
kclManager,
engineCommandManager,
editorManager,
codeManager,
}
// Apply chamfer to selection
const chamferResult = await applyEdgeTreatmentToSelection(
ast,
selection,
parameters,
dependencies
)
if (err(chamferResult)) return chamferResult
}
),
'submit-prompt-edit': fromPromise( 'submit-prompt-edit': fromPromise(
async ({ async ({
input, input,