Nadro/3716/mvp revolve (#3728)
* chore: Implemented a executeAst interrupt to stop processing a KCL program * fix: added a catch since this promise was not being caught * fix: fmt formatting, need to fix some tsc errors next. * fix: fixing tsc errors * fix: cleaning up comment * fix: only rejecting pending modeling commands * fix: adding constant for rejection message, adding rejection in WASM send command * fix: tsc, lint, fmt checks * feat: first pass over revolve with basic hard coded X axis * fix: updated revolve status for DEV only * fix: adding some TODOs to warn others about the Revolve MVP * fix: fmt, lint, tsc checks * fix: codespell got me * fix: xstate v5 upgrade * fix: removing this fix for a different PR. Not needed for initial MVP * fix: renaming extrude function to sweep since it fixes extrude and revolve now * fix: updating selection logic to support revolve * fix: renaming extrude to sweep since it adds revolve * fix: swapping as for type in function parameters * fix: updated from object destruct to structuredClone * fix: addressing PR comments * fix: one other typo for return value of revolve
This commit is contained in:
@ -38,7 +38,7 @@ import {
|
||||
import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
|
||||
import {
|
||||
Selections,
|
||||
canExtrudeSelection,
|
||||
canSweepSelection,
|
||||
handleSelectionBatch,
|
||||
isSelectionLastLine,
|
||||
isRangeInbetweenCharacters,
|
||||
@ -62,8 +62,8 @@ import {
|
||||
} from 'lang/modifyAst'
|
||||
import { Program, parse, recast } from 'lang/wasm'
|
||||
import {
|
||||
doesSceneHaveSweepableSketch,
|
||||
getNodePathFromSourceRange,
|
||||
hasExtrudableGeometry,
|
||||
isSingleCursorInPipe,
|
||||
} from 'lang/queryAst'
|
||||
import { exportFromEngine } from 'lib/exportFromEngine'
|
||||
@ -528,12 +528,32 @@ export const ModelingMachineProvider = ({
|
||||
// they have no selection, we should enable the button
|
||||
// so they can select the face through the cmdbar
|
||||
// BUT only if there's extrudable geometry
|
||||
if (hasExtrudableGeometry(kclManager.ast)) return true
|
||||
if (doesSceneHaveSweepableSketch(kclManager.ast)) return true
|
||||
return false
|
||||
}
|
||||
if (!isPipe) return false
|
||||
|
||||
return canExtrudeSelection(selectionRanges)
|
||||
return canSweepSelection(selectionRanges)
|
||||
},
|
||||
'has valid revolve selection': ({ context: { selectionRanges } }) => {
|
||||
// A user can begin extruding if they either have 1+ faces selected or nothing selected
|
||||
// TODO: I believe this guard only allows for extruding a single face at a time
|
||||
const isPipe = isSketchPipe(selectionRanges)
|
||||
|
||||
if (
|
||||
selectionRanges.codeBasedSelections.length === 0 ||
|
||||
isRangeInbetweenCharacters(selectionRanges) ||
|
||||
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||
) {
|
||||
// they have no selection, we should enable the button
|
||||
// so they can select the face through the cmdbar
|
||||
// BUT only if there's extrudable geometry
|
||||
if (doesSceneHaveSweepableSketch(kclManager.ast)) return true
|
||||
return false
|
||||
}
|
||||
if (!isPipe) return false
|
||||
|
||||
return canSweepSelection(selectionRanges)
|
||||
},
|
||||
'has valid selection for deletion': ({
|
||||
context: { selectionRanges },
|
||||
|
@ -251,7 +251,7 @@ export function extrudeSketch(
|
||||
node: Program,
|
||||
pathToNode: PathToNode,
|
||||
shouldPipe = false,
|
||||
distance = createLiteral(4) as Expr
|
||||
distance: Expr = createLiteral(4)
|
||||
):
|
||||
| {
|
||||
modifiedAst: Program
|
||||
@ -259,7 +259,7 @@ export function extrudeSketch(
|
||||
pathToExtrudeArg: PathToNode
|
||||
}
|
||||
| Error {
|
||||
const _node = { ...node }
|
||||
const _node = structuredClone(node)
|
||||
const _node1 = getNodeFromPath(_node, pathToNode)
|
||||
if (err(_node1)) return _node1
|
||||
const { node: sketchExpression } = _node1
|
||||
@ -342,6 +342,102 @@ export function extrudeSketch(
|
||||
}
|
||||
}
|
||||
|
||||
export function revolveSketch(
|
||||
node: Program,
|
||||
pathToNode: PathToNode,
|
||||
shouldPipe = false,
|
||||
angle: Expr = createLiteral(4)
|
||||
):
|
||||
| {
|
||||
modifiedAst: Program
|
||||
pathToNode: PathToNode
|
||||
pathToRevolveArg: PathToNode
|
||||
}
|
||||
| Error {
|
||||
const _node = structuredClone(node)
|
||||
const _node1 = getNodeFromPath(_node, pathToNode)
|
||||
if (err(_node1)) return _node1
|
||||
const { node: sketchExpression } = _node1
|
||||
|
||||
// determine if sketchExpression is in a pipeExpression or not
|
||||
const _node2 = getNodeFromPath<PipeExpression>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'PipeExpression'
|
||||
)
|
||||
if (err(_node2)) return _node2
|
||||
const { node: pipeExpression } = _node2
|
||||
|
||||
const isInPipeExpression = pipeExpression.type === 'PipeExpression'
|
||||
|
||||
const _node3 = getNodeFromPath<VariableDeclarator>(
|
||||
_node,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
if (err(_node3)) return _node3
|
||||
const { node: variableDeclarator, shallowPath: pathToDecleration } = _node3
|
||||
|
||||
const revolveCall = createCallExpressionStdLib('revolve', [
|
||||
createObjectExpression({
|
||||
angle: angle,
|
||||
// TODO: hard coded 'X' axis for revolve MVP, should be changed.
|
||||
axis: createLiteral('X'),
|
||||
}),
|
||||
createIdentifier(variableDeclarator.id.name),
|
||||
])
|
||||
|
||||
if (shouldPipe) {
|
||||
const pipeChain = createPipeExpression(
|
||||
isInPipeExpression
|
||||
? [...pipeExpression.body, revolveCall]
|
||||
: [sketchExpression as any, revolveCall]
|
||||
)
|
||||
|
||||
variableDeclarator.init = pipeChain
|
||||
const pathToRevolveArg: PathToNode = [
|
||||
...pathToDecleration,
|
||||
['init', 'VariableDeclarator'],
|
||||
['body', ''],
|
||||
[pipeChain.body.length - 1, 'index'],
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
pathToRevolveArg,
|
||||
}
|
||||
}
|
||||
|
||||
// We're not creating a pipe expression,
|
||||
// but rather a separate constant for the extrusion
|
||||
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
|
||||
const VariableDeclaration = createVariableDeclaration(name, revolveCall)
|
||||
const sketchIndexInPathToNode =
|
||||
pathToDecleration.findIndex((a) => a[0] === 'body') + 1
|
||||
const sketchIndexInBody = pathToDecleration[sketchIndexInPathToNode][0]
|
||||
if (typeof sketchIndexInBody !== 'number')
|
||||
return new Error('expected sketchIndexInBody to be a number')
|
||||
_node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
|
||||
|
||||
const pathToRevolveArg: PathToNode = [
|
||||
['body', ''],
|
||||
[sketchIndexInBody + 1, 'index'],
|
||||
['declarations', 'VariableDeclaration'],
|
||||
[0, 'index'],
|
||||
['init', 'VariableDeclarator'],
|
||||
['arguments', 'CallExpression'],
|
||||
[0, 'index'],
|
||||
]
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode: [...pathToNode.slice(0, -1), [-1, 'index']],
|
||||
pathToRevolveArg,
|
||||
}
|
||||
}
|
||||
|
||||
export function sketchOnExtrudedFace(
|
||||
node: Program,
|
||||
sketchPathToNode: PathToNode,
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
hasExtrudeSketchGroup,
|
||||
findUsesOfTagInPipe,
|
||||
hasSketchPipeBeenExtruded,
|
||||
hasExtrudableGeometry,
|
||||
doesSceneHaveSweepableSketch,
|
||||
traverse,
|
||||
} from './queryAst'
|
||||
import { enginelessExecutor } from '../lib/testHelpers'
|
||||
@ -488,7 +488,7 @@ const sketch002 = startSketchOn(extrude001, $seg01)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Testing hasExtrudableGeometry', () => {
|
||||
describe('Testing doesSceneHaveSweepableSketch', () => {
|
||||
it('finds sketch001 pipe to be extruded', async () => {
|
||||
const exampleCode = `const sketch001 = startSketchOn('XZ')
|
||||
|> startProfileAt([3.29, 7.86], %)
|
||||
@ -506,7 +506,7 @@ const sketch002 = startSketchOn(extrude001, $seg01)
|
||||
`
|
||||
const ast = parse(exampleCode)
|
||||
if (err(ast)) throw ast
|
||||
const extrudable = hasExtrudableGeometry(ast)
|
||||
const extrudable = doesSceneHaveSweepableSketch(ast)
|
||||
expect(extrudable).toBeTruthy()
|
||||
})
|
||||
it('find sketch002 NOT pipe to be extruded', async () => {
|
||||
@ -520,7 +520,7 @@ const extrude001 = extrude(10, sketch001)
|
||||
`
|
||||
const ast = parse(exampleCode)
|
||||
if (err(ast)) throw ast
|
||||
const extrudable = hasExtrudableGeometry(ast)
|
||||
const extrudable = doesSceneHaveSweepableSketch(ast)
|
||||
expect(extrudable).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
@ -880,7 +880,7 @@ export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
|
||||
if (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === 'extrude' &&
|
||||
(node.callee.name === 'extrude' || node.callee.name === 'revolve') &&
|
||||
node.arguments?.[1]?.type === 'Identifier' &&
|
||||
node.arguments[1].name === varDec.id.name
|
||||
) {
|
||||
@ -892,7 +892,7 @@ export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
|
||||
}
|
||||
|
||||
/** File must contain at least one sketch that has not been extruded already */
|
||||
export function hasExtrudableGeometry(ast: Program) {
|
||||
export function doesSceneHaveSweepableSketch(ast: Program) {
|
||||
const theMap: any = {}
|
||||
traverse(ast as any, {
|
||||
enter(node) {
|
||||
@ -925,7 +925,7 @@ export function hasExtrudableGeometry(ast: Program) {
|
||||
}
|
||||
} else if (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.name === 'extrude' &&
|
||||
(node.callee.name === 'extrude' || node.callee.name === 'revolve') &&
|
||||
node.arguments[1]?.type === 'Identifier' &&
|
||||
theMap?.[node?.arguments?.[1]?.name]
|
||||
) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Models } from '@kittycad/lib'
|
||||
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
|
||||
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
|
||||
import { components } from 'lib/machine-api'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { machineManager } from 'lib/machineManager'
|
||||
@ -32,6 +32,10 @@ export type ModelingCommandSchema = {
|
||||
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||
distance: KclCommandValue
|
||||
}
|
||||
Revolve: {
|
||||
selection: Selections
|
||||
angle: KclCommandValue
|
||||
}
|
||||
Fillet: {
|
||||
// todo
|
||||
selection: Selections
|
||||
@ -209,6 +213,7 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
args: {
|
||||
selection: {
|
||||
inputType: 'selection',
|
||||
// TODO: These are products of an extrude
|
||||
selectionTypes: ['extrude-wall', 'start-cap', 'end-cap'],
|
||||
multiple: false, // TODO: multiple selection
|
||||
required: true,
|
||||
@ -232,6 +237,26 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
},
|
||||
},
|
||||
},
|
||||
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
|
||||
Revolve: {
|
||||
description: 'Create a 3D body by rotating a sketch region about an axis.',
|
||||
icon: 'revolve',
|
||||
needsReview: true,
|
||||
args: {
|
||||
selection: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['extrude-wall', 'start-cap', 'end-cap'],
|
||||
multiple: false, // TODO: multiple selection
|
||||
required: true,
|
||||
skip: true,
|
||||
},
|
||||
angle: {
|
||||
inputType: 'kcl',
|
||||
defaultValue: KCL_DEFAULT_DEGREE,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Fillet: {
|
||||
// todo
|
||||
description: 'Fillet edge',
|
||||
|
@ -53,9 +53,14 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
|
||||
SKETCH: 'sketch',
|
||||
EXTRUDE: 'extrude',
|
||||
SEGMENT: 'seg',
|
||||
REVOLVE: 'revolve',
|
||||
} as const
|
||||
/** The default KCL length expression */
|
||||
export const KCL_DEFAULT_LENGTH = `5`
|
||||
|
||||
/** The default KCL degree expression */
|
||||
export const KCL_DEFAULT_DEGREE = `360`
|
||||
|
||||
/** localStorage key for the playwright test-specific app settings file */
|
||||
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
|
||||
|
||||
|
@ -46,6 +46,7 @@ export function createMachineCommand<
|
||||
| Command<T, typeof type, S[typeof type]>[]
|
||||
| null {
|
||||
const commandConfig = commandBarConfig && commandBarConfig[type]
|
||||
|
||||
// There may be no command config for this event type,
|
||||
// or there may be multiple commands to create.
|
||||
if (!commandConfig) {
|
||||
|
@ -395,10 +395,16 @@ function buildCommonNodeFromSelection(selectionRanges: Selections, i: number) {
|
||||
}
|
||||
|
||||
function nodeHasExtrude(node: CommonASTNode) {
|
||||
return doesPipeHaveCallExp({
|
||||
return (
|
||||
doesPipeHaveCallExp({
|
||||
calleeName: 'extrude',
|
||||
...node,
|
||||
}) ||
|
||||
doesPipeHaveCallExp({
|
||||
calleeName: 'revolve',
|
||||
...node,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function nodeHasClose(node: CommonASTNode) {
|
||||
@ -408,7 +414,7 @@ function nodeHasClose(node: CommonASTNode) {
|
||||
})
|
||||
}
|
||||
|
||||
export function canExtrudeSelection(selection: Selections) {
|
||||
export function canSweepSelection(selection: Selections) {
|
||||
const commonNodes = selection.codeBasedSelections.map((_, i) =>
|
||||
buildCommonNodeFromSelection(selection, i)
|
||||
)
|
||||
|
@ -94,9 +94,16 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
||||
},
|
||||
{
|
||||
id: 'revolve',
|
||||
onClick: () => console.error('Revolve not yet implemented'),
|
||||
onClick: ({ commandBarSend }) =>
|
||||
commandBarSend({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Revolve', groupId: 'modeling' },
|
||||
}),
|
||||
// TODO: disabled
|
||||
// Who's state is this?
|
||||
disabled: (state) => !state.can({ type: 'Revolve' }),
|
||||
icon: 'revolve',
|
||||
status: 'kcl-only',
|
||||
status: DEV ? 'available' : 'kcl-only',
|
||||
title: 'Revolve',
|
||||
hotkey: 'R',
|
||||
description:
|
||||
|
@ -33,7 +33,11 @@ import {
|
||||
applyConstraintEqualLength,
|
||||
setEqualLengthInfo,
|
||||
} from 'components/Toolbar/EqualLength'
|
||||
import { deleteFromSelection, extrudeSketch } from 'lang/modifyAst'
|
||||
import {
|
||||
deleteFromSelection,
|
||||
extrudeSketch,
|
||||
revolveSketch,
|
||||
} from 'lang/modifyAst'
|
||||
import { applyFilletToSelection } from 'lang/modifyAst/addFillet'
|
||||
import { getNodeFromPath } from '../lang/queryAst'
|
||||
import {
|
||||
@ -202,6 +206,7 @@ export type ModelingMachineEvent =
|
||||
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
|
||||
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
|
||||
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
||||
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
|
||||
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
|
||||
| { type: 'Text-to-CAD'; data: ModelingCommandSchema['Text-to-CAD'] }
|
||||
| {
|
||||
@ -310,6 +315,7 @@ export const modelingMachine = setup({
|
||||
guards: {
|
||||
'Selection is on face': () => false,
|
||||
'has valid extrude selection': () => false,
|
||||
'has valid revolve selection': () => false,
|
||||
'has valid fillet selection': () => false,
|
||||
'Has exportable geometry': () => false,
|
||||
'has valid selection for deletion': () => false,
|
||||
@ -566,6 +572,53 @@ export const modelingMachine = setup({
|
||||
}
|
||||
})().catch(reportRejection)
|
||||
},
|
||||
'AST revolve': ({ context: { store }, event }) => {
|
||||
if (event.type !== 'Revolve') return
|
||||
;(async () => {
|
||||
if (!event.data) return
|
||||
const { selection, angle } = event.data
|
||||
let ast = kclManager.ast
|
||||
if (
|
||||
'variableName' in angle &&
|
||||
angle.variableName &&
|
||||
angle.insertIndex !== undefined
|
||||
) {
|
||||
const newBody = [...ast.body]
|
||||
newBody.splice(angle.insertIndex, 0, angle.variableDeclarationAst)
|
||||
ast.body = newBody
|
||||
}
|
||||
const pathToNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selection.codeBasedSelections[0].range
|
||||
)
|
||||
const revolveSketchRes = revolveSketch(
|
||||
ast,
|
||||
pathToNode,
|
||||
false,
|
||||
'variableName' in angle ? angle.variableIdentifierAst : angle.valueAst
|
||||
)
|
||||
if (trap(revolveSketchRes)) return
|
||||
const { modifiedAst, pathToRevolveArg } = revolveSketchRes
|
||||
|
||||
store.videoElement?.pause()
|
||||
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
||||
focusPath: pathToRevolveArg,
|
||||
zoomToFit: true,
|
||||
zoomOnRangeAndType: {
|
||||
range: selection.codeBasedSelections[0].range,
|
||||
type: 'path',
|
||||
},
|
||||
})
|
||||
if (!engineCommandManager.engineConnection?.idleMode) {
|
||||
store.videoElement?.play().catch((e) => {
|
||||
console.warn('Video playing was prevented', e)
|
||||
})
|
||||
}
|
||||
if (updatedAst?.selections) {
|
||||
editorManager.selectRange(updatedAst?.selections)
|
||||
}
|
||||
})().catch(reportRejection)
|
||||
},
|
||||
'AST delete selection': ({ context: { selectionRanges } }) => {
|
||||
;(async () => {
|
||||
let ast = kclManager.ast
|
||||
@ -1238,6 +1291,13 @@ export const modelingMachine = setup({
|
||||
reenter: false,
|
||||
},
|
||||
|
||||
Revolve: {
|
||||
target: 'idle',
|
||||
guard: 'has valid revolve selection',
|
||||
actions: ['AST revolve'],
|
||||
reenter: false,
|
||||
},
|
||||
|
||||
Fillet: {
|
||||
target: 'idle',
|
||||
guard: 'has valid fillet selection', // TODO: fix selections
|
||||
|
Reference in New Issue
Block a user