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:
@ -2392,8 +2392,6 @@ export class SceneEntities {
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ'
|
||||
|
||||
// calculations/pure-functions/easy to test so no excuse not to
|
||||
|
||||
function prepareTruncatedMemoryAndAst(
|
||||
|
@ -156,6 +156,7 @@ function CommandBarHeader({ children }: React.PropsWithChildren<{}>) {
|
||||
)}
|
||||
{arg.inputType === 'kcl' &&
|
||||
!!argValue &&
|
||||
typeof argValue === 'object' &&
|
||||
'variableName' in (argValue as KclCommandValue) && (
|
||||
<>
|
||||
<CustomIcon
|
||||
|
@ -11,8 +11,14 @@ import {
|
||||
filterOperations,
|
||||
getOperationIcon,
|
||||
getOperationLabel,
|
||||
stdLibMap,
|
||||
} from 'lib/operations'
|
||||
import { editorManager, engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import {
|
||||
codeManager,
|
||||
editorManager,
|
||||
engineCommandManager,
|
||||
kclManager,
|
||||
} from 'lib/singletons'
|
||||
import { ComponentProps, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
import { Actor, Prop } from 'xstate'
|
||||
@ -60,6 +66,7 @@ export const FeatureTreePane = () => {
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
: null
|
||||
|
||||
if (!artifact || !('codeRef' in artifact)) {
|
||||
modelingSend({
|
||||
type: 'Set selection',
|
||||
@ -311,14 +318,12 @@ const OperationItem = (props: {
|
||||
* TODO: https://github.com/KittyCAD/modeling-app/issues/4442
|
||||
*/
|
||||
function enterEditFlow() {
|
||||
if (
|
||||
props.item.type === 'StdLibCall' &&
|
||||
props.item.name === 'startSketchOn'
|
||||
) {
|
||||
if (props.item.type === 'StdLibCall') {
|
||||
props.send({
|
||||
type: 'enterEditFlow',
|
||||
data: {
|
||||
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
|
||||
currentOperation: props.item,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -364,6 +369,14 @@ const OperationItem = (props: {
|
||||
</ContextMenuItem>,
|
||||
]
|
||||
: []),
|
||||
...(props.item.type === 'StdLibCall' &&
|
||||
stdLibMap[props.item.name]?.prepareToEdit
|
||||
? [
|
||||
<ContextMenuItem onClick={enterEditFlow}>
|
||||
Edit {name}
|
||||
</ContextMenuItem>,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[props.item, props.send]
|
||||
)
|
||||
|
@ -16,7 +16,8 @@ import {
|
||||
SegmentArtifact,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||
import { getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||
import { DefaultPlaneStr } from 'lib/planes'
|
||||
import { getNodeFromPath } from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { CallExpression, CallExpressionKw, defaultSourceRange } from 'lang/wasm'
|
||||
|
@ -456,7 +456,7 @@ export class KclManager {
|
||||
// problem this solves, but either way we should strive to remove it.
|
||||
Array.from(this.engineCommandManager.artifactGraph).forEach(
|
||||
([commandId, artifact]) => {
|
||||
if (!('codeRef' in artifact)) return
|
||||
if (!('codeRef' in artifact && artifact.codeRef)) return
|
||||
const _node1 = getNodeFromPath<Node<CallExpression | CallExpressionKw>>(
|
||||
this.ast,
|
||||
artifact.codeRef.pathToNode,
|
||||
|
@ -47,7 +47,7 @@ import {
|
||||
removeSingleConstraint,
|
||||
transformAstSketchLines,
|
||||
} from './std/sketchcombos'
|
||||
import { DefaultPlaneStr } from 'clientSideScene/sceneEntities'
|
||||
import { DefaultPlaneStr } from 'lib/planes'
|
||||
import { isOverlap, roundOff } from 'lib/utils'
|
||||
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
|
||||
import { SimplifiedArgDetails } from './std/stdTypes'
|
||||
@ -284,12 +284,19 @@ export function mutateObjExpProp(
|
||||
return false
|
||||
}
|
||||
|
||||
export function extrudeSketch(
|
||||
node: Node<Program>,
|
||||
pathToNode: PathToNode,
|
||||
export function extrudeSketch({
|
||||
node,
|
||||
pathToNode,
|
||||
shouldPipe = false,
|
||||
distance: Expr = createLiteral(4)
|
||||
):
|
||||
distance = createLiteral(4),
|
||||
extrudeName,
|
||||
}: {
|
||||
node: Node<Program>
|
||||
pathToNode: PathToNode
|
||||
shouldPipe?: boolean
|
||||
distance: Expr
|
||||
extrudeName?: string
|
||||
}):
|
||||
| {
|
||||
modifiedAst: Node<Program>
|
||||
pathToNode: PathToNode
|
||||
@ -357,7 +364,8 @@ export function extrudeSketch(
|
||||
|
||||
// We're not creating a pipe expression,
|
||||
// but rather a separate constant for the extrusion
|
||||
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE)
|
||||
const name =
|
||||
extrudeName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE)
|
||||
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
|
||||
|
||||
const sketchIndexInPathToNode =
|
||||
@ -629,14 +637,19 @@ export function sketchOnExtrudedFace(
|
||||
export function addOffsetPlane({
|
||||
node,
|
||||
defaultPlane,
|
||||
insertIndex,
|
||||
offset,
|
||||
planeName,
|
||||
}: {
|
||||
node: Node<Program>
|
||||
defaultPlane: DefaultPlaneStr
|
||||
insertIndex?: number
|
||||
offset: Expr
|
||||
planeName?: string
|
||||
}): { modifiedAst: Node<Program>; pathToNode: PathToNode } {
|
||||
const modifiedAst = structuredClone(node)
|
||||
const newPlaneName = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.PLANE)
|
||||
const newPlaneName =
|
||||
planeName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.PLANE)
|
||||
|
||||
const newPlane = createVariableDeclaration(
|
||||
newPlaneName,
|
||||
@ -646,10 +659,19 @@ export function addOffsetPlane({
|
||||
])
|
||||
)
|
||||
|
||||
modifiedAst.body.push(newPlane)
|
||||
const insertAt =
|
||||
insertIndex !== undefined
|
||||
? insertIndex
|
||||
: modifiedAst.body.length
|
||||
? modifiedAst.body.length
|
||||
: 0
|
||||
|
||||
modifiedAst.body.length
|
||||
? modifiedAst.body.splice(insertAt, 0, newPlane)
|
||||
: modifiedAst.body.push(newPlane)
|
||||
const pathToNode: PathToNode = [
|
||||
['body', ''],
|
||||
[modifiedAst.body.length - 1, 'index'],
|
||||
[insertAt, 'index'],
|
||||
['declaration', 'VariableDeclaration'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpression'],
|
||||
|
@ -293,7 +293,7 @@ const runModifyAstCloneWithEdgeTreatmentAndTag = async (
|
||||
const selection: Selections = {
|
||||
graphSelections: segmentRanges.map((segmentRange) => {
|
||||
const maybeArtifact = [...artifactGraph].find(([, a]) => {
|
||||
if (!('codeRef' in a)) return false
|
||||
if (!('codeRef' in a && a.codeRef)) return false
|
||||
return isOverlap(a.codeRef.range, segmentRange)
|
||||
})
|
||||
return {
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { err } from 'lib/trap'
|
||||
import { codeManager } from 'lib/singletons'
|
||||
|
||||
export type { Artifact, ArtifactId, SegmentArtifact } from 'lang/wasm'
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 568 KiB After Width: | Height: | Size: 569 KiB |
@ -26,9 +26,11 @@ import {
|
||||
MAKE_TOAST_MESSAGES,
|
||||
} from 'lib/constants'
|
||||
import { KclManager } from 'lang/KclSingleton'
|
||||
import { reportRejection } from 'lib/trap'
|
||||
import { err, reportRejection } from 'lib/trap'
|
||||
import { markOnce } from 'lib/performance'
|
||||
import { MachineManager } from 'components/MachineManagerProvider'
|
||||
import { DefaultPlaneStr } from 'lib/planes'
|
||||
import { defaultPlaneStrToKey } from 'lib/planes'
|
||||
|
||||
// TODO(paultag): This ought to be tweakable.
|
||||
const pingIntervalMs = 5_000
|
||||
@ -2170,6 +2172,16 @@ export class EngineCommandManager extends EventTarget {
|
||||
)
|
||||
}
|
||||
|
||||
getDefaultPlaneId(name: DefaultPlaneStr): string | Error {
|
||||
const key = defaultPlaneStrToKey(name)
|
||||
if (!this.defaultPlanes) {
|
||||
return new Error('Default planes not initialized')
|
||||
} else if (err(key)) {
|
||||
return key
|
||||
}
|
||||
return this.defaultPlanes[key]
|
||||
}
|
||||
|
||||
async setPlaneHidden(id: string, hidden: boolean) {
|
||||
if (this.engineConnection === undefined) return
|
||||
|
||||
@ -2228,7 +2240,11 @@ export class EngineCommandManager extends EventTarget {
|
||||
commandTypeToTarget: string
|
||||
): string | undefined {
|
||||
for (const [artifactId, artifact] of this.artifactGraph) {
|
||||
if ('codeRef' in artifact && isOverlap(range, artifact.codeRef.range)) {
|
||||
if (
|
||||
'codeRef' in artifact &&
|
||||
artifact.codeRef &&
|
||||
isOverlap(range, artifact.codeRef.range)
|
||||
) {
|
||||
if (commandTypeToTarget === artifact.type) return artifactId
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,8 @@ export type ModelingCommandSchema = {
|
||||
machine: components['schemas']['MachineInfoResponse']
|
||||
}
|
||||
Extrude: {
|
||||
// Enables editing workflow
|
||||
nodeToEdit?: PathToNode
|
||||
selection: Selections // & { type: 'face' } would be cool to lock that down
|
||||
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||
distance: KclCommandValue
|
||||
@ -69,6 +71,8 @@ export type ModelingCommandSchema = {
|
||||
length: KclCommandValue
|
||||
}
|
||||
'Offset plane': {
|
||||
// Enables editing workflow
|
||||
nodeToEdit?: PathToNode
|
||||
plane: Selections
|
||||
distance: KclCommandValue
|
||||
}
|
||||
@ -279,6 +283,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
icon: 'extrude',
|
||||
needsReview: true,
|
||||
args: {
|
||||
nodeToEdit: {
|
||||
description:
|
||||
'Path to the node in the AST to edit. Never shown to the user.',
|
||||
skip: true,
|
||||
inputType: 'text',
|
||||
required: false,
|
||||
},
|
||||
selection: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['solid2d', 'segment'],
|
||||
@ -415,6 +426,13 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
description: 'Offset a plane.',
|
||||
icon: 'plane',
|
||||
args: {
|
||||
nodeToEdit: {
|
||||
description:
|
||||
'Path to the node in the AST to edit. Never shown to the user.',
|
||||
skip: true,
|
||||
inputType: 'text',
|
||||
required: false,
|
||||
},
|
||||
plane: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['plane'],
|
||||
|
69
src/lib/kclHelpers.test.ts
Normal file
69
src/lib/kclHelpers.test.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { ParseResult, ProgramMemory } from 'lang/wasm'
|
||||
import { getCalculatedKclExpressionValue } from './kclHelpers'
|
||||
|
||||
describe('KCL expression calculations', () => {
|
||||
it('calculates a simple expression', async () => {
|
||||
const actual = await getCalculatedKclExpressionValue({
|
||||
value: '1 + 2',
|
||||
programMemory: ProgramMemory.empty(),
|
||||
})
|
||||
const coercedActual = actual as Exclude<typeof actual, Error | ParseResult>
|
||||
expect(coercedActual).not.toHaveProperty('errors')
|
||||
expect(coercedActual.valueAsString).toEqual('3')
|
||||
expect(coercedActual?.astNode).toBeDefined()
|
||||
})
|
||||
it('calculates a simple expression with a variable', async () => {
|
||||
const programMemory = ProgramMemory.empty()
|
||||
programMemory.set('x', {
|
||||
type: 'Number',
|
||||
value: 2,
|
||||
__meta: [],
|
||||
})
|
||||
const actual = await getCalculatedKclExpressionValue({
|
||||
value: '1 + x',
|
||||
programMemory,
|
||||
})
|
||||
const coercedActual = actual as Exclude<typeof actual, Error | ParseResult>
|
||||
expect(coercedActual.valueAsString).toEqual('3')
|
||||
expect(coercedActual.astNode).toBeDefined()
|
||||
})
|
||||
it('returns NAN for an invalid expression', async () => {
|
||||
const actual = await getCalculatedKclExpressionValue({
|
||||
value: '1 + x',
|
||||
programMemory: ProgramMemory.empty(),
|
||||
})
|
||||
const coercedActual = actual as Exclude<typeof actual, Error | ParseResult>
|
||||
expect(coercedActual.valueAsString).toEqual('NAN')
|
||||
expect(coercedActual.astNode).toBeDefined()
|
||||
})
|
||||
it('returns NAN for an expression with an invalid variable', async () => {
|
||||
const programMemory = ProgramMemory.empty()
|
||||
programMemory.set('y', {
|
||||
type: 'Number',
|
||||
value: 2,
|
||||
__meta: [],
|
||||
})
|
||||
const actual = await getCalculatedKclExpressionValue({
|
||||
value: '1 + x',
|
||||
programMemory,
|
||||
})
|
||||
const coercedActual = actual as Exclude<typeof actual, Error | ParseResult>
|
||||
expect(coercedActual.valueAsString).toEqual('NAN')
|
||||
expect(coercedActual.astNode).toBeDefined()
|
||||
})
|
||||
it('calculates a more complex expression with a variable', async () => {
|
||||
const programMemory = ProgramMemory.empty()
|
||||
programMemory.set('x', {
|
||||
type: 'Number',
|
||||
value: 2,
|
||||
__meta: [],
|
||||
})
|
||||
const actual = await getCalculatedKclExpressionValue({
|
||||
value: '(1 + x * x) * 2',
|
||||
programMemory,
|
||||
})
|
||||
const coercedActual = actual as Exclude<typeof actual, Error | ParseResult>
|
||||
expect(coercedActual.valueAsString).toEqual('10')
|
||||
expect(coercedActual.astNode).toBeDefined()
|
||||
})
|
||||
})
|
98
src/lib/kclHelpers.ts
Normal file
98
src/lib/kclHelpers.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import { err } from './trap'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { parse, ProgramMemory, programMemoryInit, resultIsOk } from 'lang/wasm'
|
||||
import { PrevVariable } from 'lang/queryAst'
|
||||
import { executeAst } from 'lang/langHelpers'
|
||||
import { KclExpression } from './commandTypes'
|
||||
|
||||
const DUMMY_VARIABLE_NAME = '__result__'
|
||||
|
||||
export function programMemoryFromVariables(
|
||||
variables: PrevVariable<string | number>[]
|
||||
): ProgramMemory | Error {
|
||||
const memory = programMemoryInit()
|
||||
if (err(memory)) return memory
|
||||
for (const { key, value } of variables) {
|
||||
const error = memory.set(
|
||||
key,
|
||||
typeof value === 'number'
|
||||
? {
|
||||
type: 'Number',
|
||||
value,
|
||||
__meta: [],
|
||||
}
|
||||
: {
|
||||
type: 'String',
|
||||
value,
|
||||
__meta: [],
|
||||
}
|
||||
)
|
||||
if (err(error)) return error
|
||||
}
|
||||
return memory
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the value of the KCL expression,
|
||||
* given the value and the variables that are available
|
||||
*/
|
||||
export async function getCalculatedKclExpressionValue({
|
||||
value,
|
||||
programMemory,
|
||||
}: {
|
||||
value: string
|
||||
programMemory: ProgramMemory
|
||||
}) {
|
||||
// Create a one-line program that assigns the value to a variable
|
||||
const dummyProgramCode = `const ${DUMMY_VARIABLE_NAME} = ${value}`
|
||||
const pResult = parse(dummyProgramCode)
|
||||
if (err(pResult) || !resultIsOk(pResult)) return pResult
|
||||
const ast = pResult.program
|
||||
|
||||
// Execute the program without hitting the engine
|
||||
const { execState } = await executeAst({
|
||||
ast,
|
||||
engineCommandManager,
|
||||
programMemoryOverride: programMemory,
|
||||
})
|
||||
|
||||
// Find the variable declaration for the result
|
||||
const resultDeclaration = ast.body.find(
|
||||
(a) =>
|
||||
a.type === 'VariableDeclaration' &&
|
||||
a.declaration.id?.name === DUMMY_VARIABLE_NAME
|
||||
)
|
||||
const variableDeclaratorAstNode =
|
||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||
resultDeclaration?.declaration.init
|
||||
const resultRawValue = execState.memory?.get(DUMMY_VARIABLE_NAME)?.value
|
||||
|
||||
return {
|
||||
astNode: variableDeclaratorAstNode,
|
||||
valueAsString:
|
||||
typeof resultRawValue === 'number' ? String(resultRawValue) : 'NAN',
|
||||
}
|
||||
}
|
||||
|
||||
export async function stringToKclExpression({
|
||||
value,
|
||||
programMemory,
|
||||
}: {
|
||||
value: string
|
||||
programMemory: ProgramMemory
|
||||
}) {
|
||||
const calculatedResult = await getCalculatedKclExpressionValue({
|
||||
value,
|
||||
programMemory,
|
||||
})
|
||||
if (err(calculatedResult) || 'errors' in calculatedResult) {
|
||||
return calculatedResult
|
||||
} else if (!calculatedResult.astNode) {
|
||||
return new Error('Failed to calculate KCL expression')
|
||||
}
|
||||
return {
|
||||
valueAst: calculatedResult.astNode,
|
||||
valueCalculated: calculatedResult.valueAsString,
|
||||
valueText: value,
|
||||
} satisfies KclExpression
|
||||
}
|
@ -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.'
|
||||
)
|
||||
}
|
||||
|
29
src/lib/planes.ts
Normal file
29
src/lib/planes.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
|
||||
// KCL string representation of default planes
|
||||
export type DefaultPlaneStr = 'XY' | 'XZ' | 'YZ' | '-XY' | '-XZ' | '-YZ'
|
||||
|
||||
export function defaultPlaneStrToKey(
|
||||
plane: DefaultPlaneStr
|
||||
): keyof DefaultPlanes | Error {
|
||||
switch (plane) {
|
||||
case 'XY':
|
||||
return 'xy'
|
||||
case 'XZ':
|
||||
return 'xz'
|
||||
case 'YZ':
|
||||
return 'yz'
|
||||
case '-XY':
|
||||
return 'negXy'
|
||||
case '-XZ':
|
||||
return 'negXz'
|
||||
case '-YZ':
|
||||
return 'negYz'
|
||||
default:
|
||||
return new Error(`Invalid plane string: ${plane}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function isDefaultPlaneStr(plane: string): plane is DefaultPlaneStr {
|
||||
return ['XY', 'XZ', 'YZ', '-XY', '-XZ', '-YZ'].includes(plane)
|
||||
}
|
@ -22,7 +22,6 @@ import { getNodeFromPath, isSingleCursorInPipe } from 'lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
|
||||
import { CommandArgument } from './commandTypes'
|
||||
import {
|
||||
DefaultPlaneStr,
|
||||
getParentGroup,
|
||||
SEGMENT_BODIES_PLUS_PROFILE_START,
|
||||
} from 'clientSideScene/sceneEntities'
|
||||
@ -43,6 +42,7 @@ import {
|
||||
ArtifactId,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { Node } from 'wasm-lib/kcl/bindings/Node'
|
||||
import { DefaultPlaneStr } from './planes'
|
||||
|
||||
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
||||
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
||||
@ -614,7 +614,7 @@ export function codeToIdSelections(
|
||||
// TODO #868: loops over all artifacts will become inefficient at a large scale
|
||||
const overlappingEntries = Array.from(engineCommandManager.artifactGraph)
|
||||
.map(([id, artifact]) => {
|
||||
if (!('codeRef' in artifact)) return null
|
||||
if (!('codeRef' in artifact && artifact.codeRef)) return null
|
||||
return isOverlap(artifact.codeRef.range, selection.range)
|
||||
? {
|
||||
artifact,
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { kclManager, engineCommandManager } from 'lib/singletons'
|
||||
import { kclManager } from 'lib/singletons'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { findUniqueName } from 'lang/modifyAst'
|
||||
import { PrevVariable, findAllPreviousVariables } from 'lang/queryAst'
|
||||
import { ProgramMemory, Expr, parse, resultIsOk } from 'lang/wasm'
|
||||
import { Expr } from 'lang/wasm'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { executeAst } from 'lang/langHelpers'
|
||||
import { err, trap } from 'lib/trap'
|
||||
import {
|
||||
getCalculatedKclExpressionValue,
|
||||
programMemoryFromVariables,
|
||||
} from './kclHelpers'
|
||||
|
||||
const isValidVariableName = (name: string) =>
|
||||
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)
|
||||
@ -86,37 +88,25 @@ export function useCalculateKclExpression({
|
||||
|
||||
useEffect(() => {
|
||||
const execAstAndSetResult = async () => {
|
||||
const _code = `const __result__ = ${value}`
|
||||
const pResult = parse(_code)
|
||||
if (err(pResult) || !resultIsOk(pResult)) return
|
||||
const ast = pResult.program
|
||||
|
||||
const _programMem: ProgramMemory = ProgramMemory.empty()
|
||||
for (const { key, value } of availableVarInfo.variables) {
|
||||
const error = _programMem.set(key, {
|
||||
type: 'String',
|
||||
value,
|
||||
__meta: [],
|
||||
})
|
||||
if (trap(error, { suppress: true })) return
|
||||
}
|
||||
const { execState } = await executeAst({
|
||||
ast,
|
||||
engineCommandManager,
|
||||
// We make sure to send an empty program memory to denote we mean mock mode.
|
||||
programMemoryOverride: kclManager.programMemory.clone(),
|
||||
})
|
||||
const resultDeclaration = ast.body.find(
|
||||
(a) =>
|
||||
a.type === 'VariableDeclaration' &&
|
||||
a.declaration.id?.name === '__result__'
|
||||
const programMemory = programMemoryFromVariables(
|
||||
availableVarInfo.variables
|
||||
)
|
||||
const init =
|
||||
resultDeclaration?.type === 'VariableDeclaration' &&
|
||||
resultDeclaration?.declaration.init
|
||||
const result = execState.memory?.get('__result__')?.value
|
||||
setCalcResult(typeof result === 'number' ? String(result) : 'NAN')
|
||||
init && setValueNode(init)
|
||||
if (programMemory instanceof Error) {
|
||||
setCalcResult('NAN')
|
||||
setValueNode(null)
|
||||
return
|
||||
}
|
||||
const result = await getCalculatedKclExpressionValue({
|
||||
value,
|
||||
programMemory,
|
||||
})
|
||||
if (result instanceof Error || 'errors' in result) {
|
||||
setCalcResult('NAN')
|
||||
setValueNode(null)
|
||||
return
|
||||
}
|
||||
setCalcResult(result?.valueAsString || 'NAN')
|
||||
result?.astNode && setValueNode(result.astNode)
|
||||
}
|
||||
if (!value) return
|
||||
execAstAndSetResult().catch(() => {
|
||||
|
@ -249,7 +249,10 @@ export const commandBarMachine = setup({
|
||||
},
|
||||
guards: {
|
||||
'Command needs review': ({ context }) =>
|
||||
context.selectedCommand?.needsReview || false,
|
||||
context.selectedCommand?.needsReview ||
|
||||
('nodeToEdit' in context.argumentsToSubmit &&
|
||||
context.argumentsToSubmit.nodeToEdit !== undefined) ||
|
||||
false,
|
||||
'Command has no arguments': ({ context }) => {
|
||||
return (
|
||||
!context.selectedCommand?.args ||
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { getArtifactFromRange } from 'lang/std/artifactGraph'
|
||||
import { SourceRange } from 'lang/wasm'
|
||||
import { assign, setup } from 'xstate'
|
||||
import { enterEditFlow, EnterEditFlowProps } from 'lib/operations'
|
||||
import { engineCommandManager } from 'lib/singletons'
|
||||
import { err } from 'lib/trap'
|
||||
import toast from 'react-hot-toast'
|
||||
import { Operation } from 'wasm-lib/kcl/bindings/Operation'
|
||||
import { assign, fromPromise, setup } from 'xstate'
|
||||
import { commandBarActor } from './commandBarMachine'
|
||||
|
||||
type FeatureTreeEvent =
|
||||
| {
|
||||
@ -12,27 +19,67 @@ type FeatureTreeEvent =
|
||||
}
|
||||
| {
|
||||
type: 'enterEditFlow'
|
||||
data: { targetSourceRange: SourceRange }
|
||||
data: { targetSourceRange: SourceRange; currentOperation: Operation }
|
||||
}
|
||||
| { type: 'goToError' }
|
||||
| { type: 'codePaneOpened' }
|
||||
| { type: 'selected' }
|
||||
| { type: 'done' }
|
||||
| { type: 'xstate.error.actor.prepareEditCommand'; error: Error }
|
||||
|
||||
type FeatureTreeContext = {
|
||||
targetSourceRange?: SourceRange
|
||||
currentOperation?: Operation
|
||||
}
|
||||
|
||||
export const featureTreeMachine = setup({
|
||||
types: {
|
||||
context: {} as { targetSourceRange?: SourceRange },
|
||||
input: {} as FeatureTreeContext,
|
||||
context: {} as FeatureTreeContext,
|
||||
events: {} as FeatureTreeEvent,
|
||||
},
|
||||
guards: {
|
||||
codePaneIsOpen: () => false,
|
||||
},
|
||||
actors: {
|
||||
prepareEditCommand: fromPromise(
|
||||
({
|
||||
input,
|
||||
}: {
|
||||
input: EnterEditFlowProps & {
|
||||
commandBarSend: (typeof commandBarActor)['send']
|
||||
}
|
||||
}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { commandBarSend, ...editFlowProps } = input
|
||||
enterEditFlow(editFlowProps)
|
||||
.then((result) => {
|
||||
if (err(result)) {
|
||||
reject(result)
|
||||
return
|
||||
}
|
||||
input.commandBarSend(result)
|
||||
resolve(result)
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
||||
),
|
||||
},
|
||||
actions: {
|
||||
saveTargetSourceRange: assign({
|
||||
targetSourceRange: ({ event }) =>
|
||||
'data' in event ? event.data.targetSourceRange : undefined,
|
||||
'data' in event && !err(event.data)
|
||||
? event.data.targetSourceRange
|
||||
: undefined,
|
||||
}),
|
||||
clearTargetSourceRange: assign({
|
||||
saveCurrentOperation: assign({
|
||||
currentOperation: ({ event }) =>
|
||||
'data' in event && 'currentOperation' in event.data
|
||||
? event.data.currentOperation
|
||||
: undefined,
|
||||
}),
|
||||
clearContext: assign({
|
||||
targetSourceRange: undefined,
|
||||
}),
|
||||
sendSelectionEvent: () => {},
|
||||
@ -41,9 +88,10 @@ export const featureTreeMachine = setup({
|
||||
scrollToError: () => {},
|
||||
},
|
||||
}).createMachine({
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QDMwEMAuBXATmAKnmAHQCWEANmAMRQD2+dA0gMYUDKduLYA2gAwBdRKAAOdWKQyk6AOxEgAHogCMAFn7EAbAFYAHAHYAnGoOm9OgMw6tAGhABPRBsvF+a6wYBM-A-0uWepYGAL4h9qiYuAREZJQ0sGBULBgA8qJgOJgysgLCSCDiktJyCsoIKipabmo6-Co2Wip+-DoG9k4IWpp+Ae6W-N16KpZeYRHo2HiEYCTkVNRgshiZAKIQUgBiFHQA7nkKRVI5ZapergN6el46515GVdYdiN4GbjpGXiNGV2pan+MQJEpjFZsR6KRZFBGKwOFwcDxiIlktIodRkWAUpADgUjiV5AVyipWipiGZupYVCY1D5+F5ngg-jpiJ8DFo1OoDA13GNwkDJtEZiQIVCYWxONwSBA5DQcWIJMdSoTVG0yTodGoPAZKfwjP52o5EHVNIYvNd9M0zJZAcDBbERdDmOL4Yi6BlZJCoABhOgQMAABTQshoLF9AaDYHSS2xQkOCvxpy6lWIbS0Bm8JkM+ksDKZLK8bL0-S8NOCNoF01iGJSnqRSUxqKg6PrWIgcsK8ZOyq6aj0avTbU1Hg0KgZzVcrU+Xn+9Q+dPLUUrYOrjeI0uD1HbeK7oCJBj7xkafhMX28agZJeqHz0RhstTUD21WgXIKFxCWKxwnvWWx2uzrKKes2KIxvk8rFDuShGpY1RaKMRiHr4va9gyegPsQQRmiYVK+GYL52mCH6ZN+GwYNsexrjKm6xrinZKruqhaLBmr8PUvj6F4nGoeoxDEpYCFMVSowfPhS7CnQnqMKsOA4HQODEG6Syej6fqBhuoaqRGUbBm2NHgYqBIMV0-EproxLpoE7hWGOnFeGSxgaPwei6EYmaiaCczxLQDB0NJsk4FudGGVBFRpsQwRGGmgx6n4cH0oaFSVH21i6LqD5mtYejuW+DpSTJcmURugUQfRIX6MQfwcpqDxpUETwJSoXxqMQ04PjoSXspYtRhHyshhvABS2mJcYlcF5QALR2Al43TtoVJDD46ZfJS1p8kNHlxFQI0GYmNJjn8c21PuIzjuq2X2hJopOnCkrbQm3ZdXZNhsl1fzOSW1kJdYzKeK5VQqJh+7nWCuXXRKCIkCunp3ZB5RFq46ofOq6geDeo4JZqdlaEWt66l8jXfcD4mSWDLpSjKMOlUSzSwXS2qztVfy5jS2g43UnyVOcZ1rRWG2g7C4Ouu6ylhmpYCU2NiD8Zoz1wZq2NaB9OYYyz2O6gE3TtR8XVEwBDbQ7Ro2JtYT1pnLb2K7UyudIrFU3h8ZoamafhZTzi4bVDUJ6zWUIS7trGmS98vvVb+1GBhjUPtqer3KxOi657UCFeLhs7d2FmHe1ARmFr54NdqLUFrZnLBFUutEV+UI-mRf5+w9NgYZSNyBHB6rxTbcHhTY5wmDVMGrRM7tvhXJG-hRid10ZGjNUEjVWM533t4gaHh8aFgaOc+7XOXyzEVXpHkf+64p-p91T744VBEE7h0oEGpTZ0Dx2Sbhi9r4ozNLroN+XJk8hTBV51SRQGE7JiS9ErJjgnSRWC0bCu0Hq+C6JMf7yUUh6KEKlwzBj-uUKqKYgFQNAYrMcVI+yVFcqxfwFhAhf0uo6FByccHLyaC1JygRp5FkagaTo5CyFUj1KxO+gReRhCAA */
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QDMwEMAuBXATmAKnmAHQCWEANmAMRQD2+dA0gMYUDKduLYA2gAwBdRKAAOdWKQyk6AOxEgAHogCMAFn7EAbAFYAHAHYAnGoOm9OgMw6tAGhABPRBsvF+a6wYBM-A-0uWepYGAL4h9qiYuAREZJQ0sGBULBgA8qJgOJgysgLCSCDiktJyCsoIKipabmo6-Co2Wip+-DoG9k4IWpp+Ae6W-N16KpZeYRHo2HiEYCTkVNRgshiZAKIQUgBiFHQA7nkKRVI5ZapergN6el46515GVdYdiN4GbjpGXiNGV2pan+MQJEpjFZnEFvRGKscDg6DgDgUjiV5AVylU3sEjFo-P9fINRs8KpU9MRrLp+CZPkF9IDgdEZiR6KRZFBGKwOFwcDxiIlktIWdReWAUpAEWIJMdSqjVK0VMQzN1LCoTGofPwvIS-jpiJ8DFo1OoDA13GNwkDJvTYkyWWy2JxuCQIHIaGLChLkacKm15TodGoPAYlRT-O1HIg6ppDF5rvpmmZLLSLdMrXRmazmHbOdy6BlZGmAMJ0CBgAAKaFkNBYRdL5bA6SWoqEh3dJ2lXUqxDa2O8JkM+ksmt0Oq8er0-S8quCiaiybBQpSaZ5SWF-KgguXIogrqRrdA5X1JIMbSPZn9lg0KkJzVcrU+XlxDSM6unIIZS75i6dFeo25bUr3qgGIeRiNH4JhfN4aiEhO1QfHoIH6n6DyBloL6WmCSwrDgabrFsOy7O+K5puufKNvk4rFLuSjhv62hBKY1h-PRUFhggwxeJ2Pa6D4hj1KEZp0rOJCYZkOEbBg2x7MQX4uk2iJ-iiAEVFo1T6ho9S+PoXjaYSwxqMQKj+EYepNEYowfGhQnECJ2EsrhEn4cQoh4KIaB4PZhYALaeeWEDUDJZCyAAbnQADWJCCaCwnLKJdniZJBHOWArnueJXk+bIEAIMyIUsNkch5L+lH-tRCAGtqxlaFcNgqGO3YsZ0ZlGDq+ggWO7j+NYllRdZMW2VA9kJU5LluWAHl0N5vmLDCcJORQmDIHCnnEJFb42WJeFSUlKVjWlE0ZVlOV0HlyKFXJFGSoppXlTqo4WE0dUGGYhJGD8OrKhYwwgYx-ETDOPXWum0KwjgxA5ksBbVmW35VsW0N1rmZHNsVV37pYzU2DohlPYE7hWFe2kcU9Jj8Pwei6K9ah6N1b6A1CM2gzJP7nW6KOevoxB-Aa-oPBSVMBDoBPqMQ95qCBxL6ueOhhGasjVvABSrUQyOXZ6AC0disWrfjaFVpPWNc8FfAmAlJj18xgCrHptqqV5-No6htMMSqBr6NMpmmtocg6VtUeU54cTYernkxWgTvjrHWNqYvBOegbaX4v3mv9tOpjaGbe1yJDzquvsleUY6uL6Hy+uoHjwZerH+hxesgRSXwqOcbumynHvp+y9pZ9Jzp56jqjNKp6qBvUfpi38mqqnRFJ1J8lRN9LLevm36Yd1mJDg3mLKFnDta9566OaEHWgh1VYe1AOVeT7XATdFjHznu7c4brn8ls221iB9ix-+qf4cX50YdtA3HOIMEY5MqaP2zs-RcOc0x7xtqTTsX8T7kz-nbZq7ExaBifPcUmC8-pLyfh+Fk3cKzwKUjjB2tQRjBGoX6K8gYRYjkJoaYIVRIG9SwhtByexyGlUsPqYgVxmimBwU+BooYAG3FJMMCk9EqoTiTkrDCfVuFDVgSyPh5QDQcWEWYYw6pxFtF0o3EWGhPh1ANEaCyi90LRS4XFTaBEZJaOcA0Tm55Rj+isdYdwukjxuE+CYLStwY4cPWo4nhiURqpSkOlXyriECBjlIGM8A9DJkw1KxYY1RAh3gCIEbx1NbFWTpnQYGcJEkCNgr6LEAw-T3jDleSomgzI3F0AbLsHCykVNBhvSGO8yGv1VjbYWxc6mtFVCpLJnRKhvTmU+fWFhAjdLTkDBmpDLbDOtkpcmcoeKFPcGORukj+7zOVIs-wyzrgyxCEAA */
|
||||
id: 'featureTree',
|
||||
description: 'Workflows for interacting with the feature tree pane',
|
||||
context: ({ input }) => input,
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
@ -59,7 +107,7 @@ export const featureTreeMachine = setup({
|
||||
|
||||
enterEditFlow: {
|
||||
target: 'enteringEditFlow',
|
||||
actions: 'saveTargetSourceRange',
|
||||
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
|
||||
},
|
||||
|
||||
goToError: 'goingToError',
|
||||
@ -79,7 +127,7 @@ export const featureTreeMachine = setup({
|
||||
},
|
||||
|
||||
done: {
|
||||
entry: ['clearTargetSourceRange'],
|
||||
entry: ['clearContext'],
|
||||
always: '#featureTree.idle',
|
||||
},
|
||||
|
||||
@ -107,7 +155,7 @@ export const featureTreeMachine = setup({
|
||||
|
||||
done: {
|
||||
always: '#featureTree.idle',
|
||||
entry: 'clearTargetSourceRange',
|
||||
entry: 'clearContext',
|
||||
},
|
||||
},
|
||||
|
||||
@ -118,18 +166,54 @@ export const featureTreeMachine = setup({
|
||||
states: {
|
||||
selecting: {
|
||||
on: {
|
||||
selected: 'done',
|
||||
selected: {
|
||||
target: 'prepareEditCommand',
|
||||
reenter: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
done: {
|
||||
always: '#featureTree.idle',
|
||||
},
|
||||
|
||||
prepareEditCommand: {
|
||||
invoke: {
|
||||
src: 'prepareEditCommand',
|
||||
input: ({ context }) => {
|
||||
const artifact = context.targetSourceRange
|
||||
? getArtifactFromRange(
|
||||
context.targetSourceRange,
|
||||
engineCommandManager.artifactGraph
|
||||
) ?? undefined
|
||||
: undefined
|
||||
return {
|
||||
// currentOperation is guaranteed to be defined here
|
||||
operation: context.currentOperation!,
|
||||
artifact,
|
||||
commandBarSend: commandBarActor.send,
|
||||
}
|
||||
},
|
||||
onDone: {
|
||||
target: 'done',
|
||||
reenter: true,
|
||||
},
|
||||
onError: {
|
||||
target: 'done',
|
||||
reenter: true,
|
||||
actions: ({ event }) => {
|
||||
if ('error' in event && err(event.error)) {
|
||||
toast.error(event.error.message)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
initial: 'selecting',
|
||||
entry: 'sendSelectionEvent',
|
||||
exit: ['clearTargetSourceRange', 'sendEditFlowStart'],
|
||||
exit: ['clearContext'],
|
||||
},
|
||||
|
||||
goingToError: {
|
||||
|
@ -70,7 +70,8 @@ import {
|
||||
} from 'components/Toolbar/SetAbsDistance'
|
||||
import { ModelingCommandSchema } from 'lib/commandBarConfigs/modelingCommandConfig'
|
||||
import { err, reportRejection, trap } from 'lib/trap'
|
||||
import { DefaultPlaneStr, getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||
import { getFaceDetails } from 'clientSideScene/sceneEntities'
|
||||
import { DefaultPlaneStr } from 'lib/planes'
|
||||
import { uuidv4 } from 'lib/utils'
|
||||
import { Coords2d } from 'lang/std/sketch'
|
||||
import { deleteSegment } from 'clientSideScene/ClientSideSceneComp'
|
||||
@ -1452,24 +1453,49 @@ export const modelingMachine = setup({
|
||||
unknown,
|
||||
ModelingCommandSchema['Extrude'] | undefined
|
||||
>(async ({ input }) => {
|
||||
if (!input) return Promise.reject('No input provided')
|
||||
const { selection, distance } = input
|
||||
if (!input) return new Error('No input provided')
|
||||
const { selection, distance, nodeToEdit } = input
|
||||
const isEditing =
|
||||
nodeToEdit !== undefined && typeof nodeToEdit[1][0] === 'number'
|
||||
let ast = structuredClone(kclManager.ast)
|
||||
let extrudeName: string | undefined = undefined
|
||||
|
||||
// If this is an edit flow, first we're going to remove the old extrusion
|
||||
if (isEditing) {
|
||||
// Extract the plane name from the node to edit
|
||||
const extrudeNameNode = getNodeFromPath<VariableDeclaration>(
|
||||
ast,
|
||||
nodeToEdit,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(extrudeNameNode)) {
|
||||
console.error('Error extracting plane name')
|
||||
} else {
|
||||
extrudeName = extrudeNameNode.node.declaration.id.name
|
||||
}
|
||||
|
||||
// Removing the old extrusion statement
|
||||
const newBody = [...ast.body]
|
||||
newBody.splice(nodeToEdit[1][0] as number, 1)
|
||||
ast.body = newBody
|
||||
}
|
||||
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selection.graphSelections[0]?.codeRef.range
|
||||
)
|
||||
// Add an extrude statement to the AST
|
||||
const extrudeSketchRes = extrudeSketch(
|
||||
ast,
|
||||
const extrudeSketchRes = extrudeSketch({
|
||||
node: ast,
|
||||
pathToNode,
|
||||
false,
|
||||
'variableName' in distance
|
||||
? distance.variableIdentifierAst
|
||||
: distance.valueAst
|
||||
)
|
||||
if (err(extrudeSketchRes)) return Promise.reject(extrudeSketchRes)
|
||||
shouldPipe: false,
|
||||
distance:
|
||||
'variableName' in distance
|
||||
? distance.variableIdentifierAst
|
||||
: distance.valueAst,
|
||||
extrudeName,
|
||||
})
|
||||
if (err(extrudeSketchRes)) return extrudeSketchRes
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
|
||||
|
||||
// Insert the distance variable if the user has provided a variable name
|
||||
@ -1513,30 +1539,37 @@ export const modelingMachine = setup({
|
||||
if (!input) return new Error('No input provided')
|
||||
// Extract inputs
|
||||
const ast = kclManager.ast
|
||||
const { plane: selection, distance } = input
|
||||
const { plane: selection, distance, nodeToEdit } = input
|
||||
|
||||
let insertIndex: number | undefined = undefined
|
||||
let planeName: string | undefined = undefined
|
||||
|
||||
// If this is an edit flow, first we're going to remove the old plane
|
||||
if (nodeToEdit && typeof nodeToEdit[1][0] === 'number') {
|
||||
// Extract the plane name from the node to edit
|
||||
const planeNameNode = getNodeFromPath<VariableDeclaration>(
|
||||
ast,
|
||||
nodeToEdit,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(planeNameNode)) {
|
||||
console.error('Error extracting plane name')
|
||||
} else {
|
||||
planeName = planeNameNode.node.declaration.id.name
|
||||
}
|
||||
|
||||
const newBody = [...ast.body]
|
||||
newBody.splice(nodeToEdit[1][0], 1)
|
||||
ast.body = newBody
|
||||
insertIndex = nodeToEdit[1][0]
|
||||
}
|
||||
|
||||
// Extract the default plane from selection
|
||||
const plane = selection.otherSelections[0]
|
||||
if (!(plane && plane instanceof Object && 'name' in plane))
|
||||
return trap('No plane selected')
|
||||
|
||||
// Insert the distance variable if it exists
|
||||
if (
|
||||
'variableName' in distance &&
|
||||
distance.variableName &&
|
||||
distance.insertIndex !== undefined
|
||||
) {
|
||||
const newBody = [...ast.body]
|
||||
newBody.splice(
|
||||
distance.insertIndex,
|
||||
0,
|
||||
distance.variableDeclarationAst
|
||||
)
|
||||
ast.body = newBody
|
||||
}
|
||||
|
||||
// Get the default plane name from the selection
|
||||
|
||||
const offsetPlaneResult = addOffsetPlane({
|
||||
node: ast,
|
||||
defaultPlane: plane.name,
|
||||
@ -1544,8 +1577,27 @@ export const modelingMachine = setup({
|
||||
'variableName' in distance
|
||||
? distance.variableIdentifierAst
|
||||
: distance.valueAst,
|
||||
insertIndex,
|
||||
planeName,
|
||||
})
|
||||
|
||||
// Insert the distance variable if the user has provided a variable name
|
||||
if (
|
||||
'variableName' in distance &&
|
||||
distance.variableName &&
|
||||
typeof offsetPlaneResult.pathToNode[1][0] === 'number'
|
||||
) {
|
||||
const insertIndex = Math.min(
|
||||
offsetPlaneResult.pathToNode[1][0],
|
||||
distance.insertIndex
|
||||
)
|
||||
const newBody = [...offsetPlaneResult.modifiedAst.body]
|
||||
newBody.splice(insertIndex, 0, distance.variableDeclarationAst)
|
||||
offsetPlaneResult.modifiedAst.body = newBody
|
||||
// Since we inserted a new variable, we need to update the path to the extrude argument
|
||||
offsetPlaneResult.pathToNode[1][0]++
|
||||
}
|
||||
|
||||
const updateAstResult = await kclManager.updateAst(
|
||||
offsetPlaneResult.modifiedAst,
|
||||
true,
|
||||
|
Reference in New Issue
Block a user