Add edit flows for extrude and offset plane operations (#5045)

* Start implementing a "prepareToEdit" callback for extrude

* Start of generic edit flow for operations

* Actually invoking command bar send generically on double-click

* Refactor: break out non-React hook helper to calculate Kcl expression value

* Add unit tests, fmt

* Integrate helper to get calculated KclExpression

* Clean up unused imports, simplify use of `programMemoryFromVariables`

* Implement basic extrude editing

* Refactor: move DefaultPlanesStr to its own lib file

* Add support for editing offset planes

* Add Edit right-click menu option

* Turn off edit flow for sketch for now

* Add e2e tests for sketch and offset plane editing, fix bug found with offset plane editing

* Add failing e2e extrude edit test

* Remove action version of extrude AST mod

* Fix behavior when adding a constant while editing operation, fixing e2e test

* Patch in changes from 61b02b5703

* Remove shell's prepareToEdit

* Add other Surface types to `artifactIsPlaneWithPaths`

* refactor: rename `item` to `operation`

* Allow `prepareToEdit` to fail with a toast, signal sketch-on-offset is unimplemented

* Rework sketch e2e test to test several working and failing cases

* Fix tsc errors related to making `codeRef` optional

* Make basic error messages more friendly

* fmt

* Reset modifyAst.ts to main

* Fix broken artifactGraph unit test

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

* Remove unused import

* Look at this (photo)Graph *in the voice of Nickelback*

* Make the offset plane insert at the end, not one before

* Fix bug caught by e2e test failure with "Command needs review" logic

* Update src/machines/modelingMachine.ts

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

* Remove console logs per @pierremtb

* Update src/components/CommandBar/CommandBarHeader.tsx

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

* Use better programMemory init thanks @jtran

* Fix tsc post merge of #5068

* Fix logic for `artifactIsPlaneWithPaths` post-merge

* Need to disable the sketch-on-face case now that artifactGraph is in Rust. Will active in a future PR (cc @jtran)

* Re-run CI after snapshots

* Update FeatureTreePane to not use `useCommandsContext`, missed during merge

* Fix merge issue, import location change on edited file

* fix click test step, which I believe is waiting for context scripts to load

* Convert toolbarFixture.exeIndicator to getter

We need to convert all these selectors on fixtures to getters, because
they can go stale if called on the fixture constructor.

* Missed a dumb little thing in toolbarFixture.ts

* Fix goof with merge

* fmt

* Another dumb missed thing during merge

I gotta get used to the LazyGit merge tool I'm not good at it yet

* 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)

* Conver sceneFixture's exeIndicator to a getter

Locators on fixtures will be frozen from the time of the fixture's
initialization, I'm increasingly convinced

* 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)

* 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)

* 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)

* 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)

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

* Post-kwargs E2E test cleanup

* 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)

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

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
This commit is contained in:
Frank Noirot
2025-02-05 19:33:40 -05:00
committed by GitHub
parent eb4048cd16
commit 9008fb636f
28 changed files with 1054 additions and 318 deletions

View File

@ -1,19 +1,212 @@
import { CustomIconName } from 'components/CustomIcon'
import { Artifact, getArtifactOfTypes } from 'lang/std/artifactGraph'
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
import { codeManager, engineCommandManager, kclManager } from './singletons'
import { err } from './trap'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { sourceRangeFromRust } from 'lang/wasm'
import { CommandBarMachineEvent } from 'machines/commandBarMachine'
import { stringToKclExpression } from './kclHelpers'
import { ModelingCommandSchema } from './commandBarConfigs/modelingCommandConfig'
import { isDefaultPlaneStr } from './planes'
import { Selections } from './selections'
type ExecuteCommandEvent = CommandBarMachineEvent & {
type: 'Find and select command'
}
type ExecuteCommandEventPayload = ExecuteCommandEvent['data']
type PrepareToEditFailurePayload = { reason: string }
type PrepareToEditCallback = (
props: Omit<EnterEditFlowProps, 'commandBarSend'>
) =>
| ExecuteCommandEventPayload
| Promise<ExecuteCommandEventPayload | PrepareToEditFailurePayload>
interface StdLibCallInfo {
label: string
icon: CustomIconName
/**
* There are operations which are honored by the feature tree
* that do not yet have a corresponding modeling command.
*/
prepareToEdit?:
| ExecuteCommandEventPayload
| PrepareToEditCallback
| PrepareToEditFailurePayload
}
const stdLibMap: Record<string, StdLibCallInfo> = {
/**
* Gather up the argument values for the Extrude command
* to be used in the command bar edit flow.
*/
const prepareToEditExtrude: PrepareToEditCallback =
async function prepareToEditExtrude({ operation, artifact }) {
const baseCommand = {
name: 'Extrude',
groupId: 'modeling',
}
if (
!artifact ||
!('pathId' in artifact) ||
operation.type !== 'StdLibCall'
) {
return baseCommand
}
// We have to go a little roundabout to get from the original artifact
// to the solid2DId that we need to pass to the Extrude command.
const pathArtifact = getArtifactOfTypes(
{
key: artifact.pathId,
types: ['path'],
},
engineCommandManager.artifactGraph
)
if (
err(pathArtifact) ||
pathArtifact.type !== 'path' ||
!pathArtifact.solid2dId
)
return baseCommand
const solid2DArtifact = getArtifactOfTypes(
{
key: pathArtifact.solid2dId,
types: ['solid2d'],
},
engineCommandManager.artifactGraph
)
if (err(solid2DArtifact) || solid2DArtifact.type !== 'solid2d') {
return baseCommand
}
// Convert the length argument from a string to a KCL expression
const distanceResult = await stringToKclExpression({
value: codeManager.code.slice(
operation.labeledArgs?.['length']?.sourceRange[0],
operation.labeledArgs?.['length']?.sourceRange[1]
),
programMemory: kclManager.programMemory.clone(),
})
if (err(distanceResult) || 'errors' in distanceResult) {
return baseCommand
}
// Assemble the default argument values for the Extrude command,
// with `nodeToEdit` set, which will let the Extrude actor know
// to edit the node that corresponds to the StdLibCall.
const argDefaultValues: ModelingCommandSchema['Extrude'] = {
selection: {
graphSelections: [
{
artifact: solid2DArtifact,
codeRef: pathArtifact.codeRef,
},
],
otherSelections: [],
},
distance: distanceResult,
nodeToEdit: getNodePathFromSourceRange(
kclManager.ast,
sourceRangeFromRust(operation.sourceRange)
),
}
return {
...baseCommand,
argDefaultValues,
}
}
const prepareToEditOffsetPlane: PrepareToEditCallback = async ({
operation,
}) => {
const baseCommand = {
name: 'Offset plane',
groupId: 'modeling',
}
if (
operation.type !== 'StdLibCall' ||
!operation.labeledArgs ||
!('std_plane' in operation.labeledArgs) ||
!operation.labeledArgs.std_plane ||
!('offset' in operation.labeledArgs) ||
!operation.labeledArgs.offset
) {
return baseCommand
}
// TODO: Implement conversion to arbitrary plane selection
// once the Offset Plane command supports it.
const planeName = codeManager.code
.slice(
operation.labeledArgs.std_plane.sourceRange[0],
operation.labeledArgs.std_plane.sourceRange[1]
)
.replaceAll(`'`, ``)
if (!isDefaultPlaneStr(planeName)) {
// TODO: error handling
return baseCommand
}
const planeId = engineCommandManager.getDefaultPlaneId(planeName)
if (err(planeId)) {
// TODO: error handling
return baseCommand
}
const plane: Selections = {
graphSelections: [],
otherSelections: [
{
name: planeName,
id: planeId,
},
],
}
// Convert the distance argument from a string to a KCL expression
const distanceResult = await stringToKclExpression({
value: codeManager.code.slice(
operation.labeledArgs.offset.sourceRange[0],
operation.labeledArgs.offset.sourceRange[1]
),
programMemory: kclManager.programMemory.clone(),
})
if (err(distanceResult) || 'errors' in distanceResult) {
return baseCommand
}
// Assemble the default argument values for the Offset Plane command,
// with `nodeToEdit` set, which will let the Offset Plane actor know
// to edit the node that corresponds to the StdLibCall.
const argDefaultValues: ModelingCommandSchema['Offset plane'] = {
distance: distanceResult,
plane,
nodeToEdit: getNodePathFromSourceRange(
kclManager.ast,
sourceRangeFromRust(operation.sourceRange)
),
}
return {
...baseCommand,
argDefaultValues,
}
}
/**
* A map of standard library calls to their corresponding information
* for use in the feature tree UI.
*/
export const stdLibMap: Record<string, StdLibCallInfo> = {
chamfer: {
label: 'Chamfer',
icon: 'chamfer3d',
// modelingEvent: 'Chamfer',
},
extrude: {
label: 'Extrude',
icon: 'extrude',
prepareToEdit: prepareToEditExtrude,
},
fillet: {
label: 'Fillet',
@ -42,6 +235,7 @@ const stdLibMap: Record<string, StdLibCallInfo> = {
offsetPlane: {
label: 'Offset Plane',
icon: 'plane',
prepareToEdit: prepareToEditOffsetPlane,
},
patternCircular2d: {
label: 'Circular Pattern',
@ -70,6 +264,21 @@ const stdLibMap: Record<string, StdLibCallInfo> = {
startSketchOn: {
label: 'Sketch',
icon: 'sketch',
// TODO: fix matching sketches-on-faces and offset planes back to their
// original plane artifacts in order to edit them.
async prepareToEdit({ artifact }) {
if (artifact) {
return {
name: 'Enter sketch',
groupId: 'modeling',
}
} else {
return {
reason:
'Editing sketches on faces or offset planes through the feature tree is not yet supported. Please double-click the path in the scene for now.',
}
}
},
},
sweep: {
label: 'Sweep',
@ -182,3 +391,47 @@ function isNotUserFunctionWithNoOperations(
function isNotUserFunctionReturn(ops: Operation[]): Operation[] {
return ops.filter((op) => op.type !== 'UserDefinedFunctionReturn')
}
export interface EnterEditFlowProps {
operation: Operation
artifact?: Artifact
}
export async function enterEditFlow({
operation,
artifact,
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
if (operation.type !== 'StdLibCall') {
return new Error(
'Feature tree editing not yet supported for user-defined functions. Please edit in the code editor.'
)
}
const stdLibInfo = stdLibMap[operation.name]
if (stdLibInfo && stdLibInfo.prepareToEdit) {
if (typeof stdLibInfo.prepareToEdit === 'function') {
const eventPayload = await stdLibInfo.prepareToEdit({
operation,
artifact,
})
if ('reason' in eventPayload) {
return new Error(eventPayload.reason)
}
return {
type: 'Find and select command',
data: eventPayload,
}
} else {
return 'reason' in stdLibInfo.prepareToEdit
? new Error(stdLibInfo.prepareToEdit.reason)
: {
type: 'Find and select command',
data: stdLibInfo.prepareToEdit,
}
}
}
return new Error(
'Feature tree editing not yet supported for this operation. Please edit in the code editor.'
)
}