3036 tests add fillet (#3530)
* addFillet.ts - refactor existing code * move logic from modelingMachine to addFillet * rename getPathForSelection into getPathToExtrudeForSegmentSelection * stuck with kclManager * stuck 2 * remove engineless exe from fillet test * pathToExtrudeNode properly tested * resolve conflicts * engine initialization update * cleanup comments * passed ExecuteArgs instead of Program to executeAst * afterAll engineCommandManager.tearDown * resolve conflicts * mutateAstForRadiusInsertion * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * save banner from hulk mutations * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * sweet errors * purging the as * make type of getNodeFromPath safe again * as cleaning part 2 * cleared mutation logic * last bits * make the linter happy --------- Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Binary file not shown.
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
@ -6,9 +6,13 @@ import {
|
||||
Expr,
|
||||
Program,
|
||||
CallExpression,
|
||||
makeDefaultPlanes,
|
||||
PipeExpression,
|
||||
VariableDeclaration,
|
||||
} from '../wasm'
|
||||
import {
|
||||
addFillet,
|
||||
getPathToExtrudeForSegmentSelection,
|
||||
hasValidFilletSelection,
|
||||
isTagUsedInFillet,
|
||||
} from './addFillet'
|
||||
@ -16,9 +20,204 @@ import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
||||
import { createLiteral } from 'lang/modifyAst'
|
||||
import { err } from 'lib/trap'
|
||||
import { Selections } from 'lib/selections'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { VITE_KC_DEV_TOKEN } from 'env'
|
||||
|
||||
beforeAll(async () => {
|
||||
await initPromise // Initialize the WASM environment before running tests
|
||||
await initPromise
|
||||
|
||||
// THESE TEST WILL FAIL without VITE_KC_DEV_TOKEN set in .env.development.local
|
||||
await new Promise((resolve) => {
|
||||
engineCommandManager.start({
|
||||
token: VITE_KC_DEV_TOKEN,
|
||||
width: 256,
|
||||
height: 256,
|
||||
makeDefaultPlanes: () => makeDefaultPlanes(engineCommandManager),
|
||||
setMediaStream: () => {},
|
||||
setIsStreamReady: () => {},
|
||||
modifyGrid: async () => {},
|
||||
callbackOnEngineLiteConnect: async () => {
|
||||
resolve(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
}, 20_000)
|
||||
|
||||
afterAll(() => {
|
||||
engineCommandManager.tearDown()
|
||||
})
|
||||
|
||||
const runGetPathToExtrudeForSegmentSelectionTest = async (
|
||||
code: string,
|
||||
selectedSegmentSnippet: string,
|
||||
expectedExtrudeSnippet: string
|
||||
) => {
|
||||
// helpers
|
||||
function getExtrudeExpression(
|
||||
ast: Program,
|
||||
pathToExtrudeNode: PathToNode
|
||||
): CallExpression | PipeExpression | undefined | Error {
|
||||
if (pathToExtrudeNode.length === 0) return undefined // no extrude node
|
||||
|
||||
const extrudeNodeResult = getNodeFromPath(ast, pathToExtrudeNode)
|
||||
if (err(extrudeNodeResult)) {
|
||||
return extrudeNodeResult
|
||||
}
|
||||
return extrudeNodeResult.node as CallExpression | PipeExpression
|
||||
}
|
||||
function getExpectedExtrudeExpression(
|
||||
ast: Program,
|
||||
code: string,
|
||||
expectedExtrudeSnippet: string
|
||||
): CallExpression | PipeExpression | Error {
|
||||
const extrudeRange: [number, number] = [
|
||||
code.indexOf(expectedExtrudeSnippet),
|
||||
code.indexOf(expectedExtrudeSnippet) + expectedExtrudeSnippet.length,
|
||||
]
|
||||
const expedtedExtrudePath = getNodePathFromSourceRange(ast, extrudeRange)
|
||||
const expedtedExtrudeNodeResult = getNodeFromPath(ast, expedtedExtrudePath)
|
||||
if (err(expedtedExtrudeNodeResult)) {
|
||||
return expedtedExtrudeNodeResult
|
||||
}
|
||||
const expectedExtrudeNode =
|
||||
expedtedExtrudeNodeResult.node as VariableDeclaration
|
||||
return expectedExtrudeNode.declarations[0].init as
|
||||
| CallExpression
|
||||
| PipeExpression
|
||||
}
|
||||
|
||||
// ast
|
||||
const astOrError = parse(code)
|
||||
if (err(astOrError)) return new Error('AST not found')
|
||||
const ast = astOrError as Program
|
||||
|
||||
// selection
|
||||
const segmentRange: [number, number] = [
|
||||
code.indexOf(selectedSegmentSnippet),
|
||||
code.indexOf(selectedSegmentSnippet) + selectedSegmentSnippet.length,
|
||||
]
|
||||
const selection: Selections = {
|
||||
codeBasedSelections: [
|
||||
{
|
||||
range: segmentRange,
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
otherSelections: [],
|
||||
}
|
||||
|
||||
// programMemory and artifactGraph
|
||||
await kclManager.executeAst({ ast })
|
||||
const programMemory = kclManager.programMemory
|
||||
const artifactGraph = engineCommandManager.artifactGraph
|
||||
|
||||
// get extrude expression
|
||||
const pathResult = getPathToExtrudeForSegmentSelection(
|
||||
ast,
|
||||
selection,
|
||||
programMemory,
|
||||
artifactGraph
|
||||
)
|
||||
if (err(pathResult)) return pathResult
|
||||
const { pathToExtrudeNode } = pathResult
|
||||
const extrudeExpression = getExtrudeExpression(ast, pathToExtrudeNode)
|
||||
|
||||
// test
|
||||
if (expectedExtrudeSnippet) {
|
||||
const expectedExtrudeExpression = getExpectedExtrudeExpression(
|
||||
ast,
|
||||
code,
|
||||
expectedExtrudeSnippet
|
||||
)
|
||||
if (err(expectedExtrudeExpression)) return expectedExtrudeExpression
|
||||
expect(extrudeExpression).toEqual(expectedExtrudeExpression)
|
||||
} else {
|
||||
expect(extrudeExpression).toBeUndefined()
|
||||
}
|
||||
}
|
||||
describe('Testing getPathToExtrudeForSegmentSelection', () => {
|
||||
it('should return the correct paths for a valid selection and extrusion', async () => {
|
||||
const code = `const sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-10, 10], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, -20], %)
|
||||
|> line([-20, 0], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(-15, sketch001)`
|
||||
const selectedSegmentSnippet = `line([20, 0], %)`
|
||||
const expectedExtrudeSnippet = `const extrude001 = extrude(-15, sketch001)`
|
||||
await runGetPathToExtrudeForSegmentSelectionTest(
|
||||
code,
|
||||
selectedSegmentSnippet,
|
||||
expectedExtrudeSnippet
|
||||
)
|
||||
})
|
||||
it('should return the correct paths for a valid selection and extrusion in case of several extrusions and sketches', async () => {
|
||||
const code = `const sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-30, 30], %)
|
||||
|> line([15, 0], %)
|
||||
|> line([0, -15], %)
|
||||
|> line([-15, 0], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const sketch002 = startSketchOn('XY')
|
||||
|> startProfileAt([30, 30], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, -20], %)
|
||||
|> line([-20, 0], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const sketch003 = startSketchOn('XY')
|
||||
|> startProfileAt([30, -30], %)
|
||||
|> line([25, 0], %)
|
||||
|> line([0, -25], %)
|
||||
|> line([-25, 0], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(-15, sketch001)
|
||||
const extrude002 = extrude(-15, sketch002)
|
||||
const extrude003 = extrude(-15, sketch003)`
|
||||
const selectedSegmentSnippet = `line([20, 0], %)`
|
||||
const expectedExtrudeSnippet = `const extrude002 = extrude(-15, sketch002)`
|
||||
await runGetPathToExtrudeForSegmentSelectionTest(
|
||||
code,
|
||||
selectedSegmentSnippet,
|
||||
expectedExtrudeSnippet
|
||||
)
|
||||
})
|
||||
it('should not return any path for missing extrusion', async () => {
|
||||
const code = `const sketch001 = startSketchOn('XY')
|
||||
|> startProfileAt([-30, 30], %)
|
||||
|> line([15, 0], %)
|
||||
|> line([0, -15], %)
|
||||
|> line([-15, 0], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const sketch002 = startSketchOn('XY')
|
||||
|> startProfileAt([30, 30], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, -20], %)
|
||||
|> line([-20, 0], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const sketch003 = startSketchOn('XY')
|
||||
|> startProfileAt([30, -30], %)
|
||||
|> line([25, 0], %)
|
||||
|> line([0, -25], %)
|
||||
|> line([-25, 0], %)
|
||||
|> lineTo([profileStartX(%), profileStartY(%)], %)
|
||||
|> close(%)
|
||||
const extrude001 = extrude(-15, sketch001)
|
||||
const extrude003 = extrude(-15, sketch003)`
|
||||
const selectedSegmentSnippet = `line([20, 0], %)`
|
||||
const expectedExtrudeSnippet = ``
|
||||
await runGetPathToExtrudeForSegmentSelectionTest(
|
||||
code,
|
||||
selectedSegmentSnippet,
|
||||
expectedExtrudeSnippet
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const runFilletTest = async (
|
||||
@ -57,8 +256,6 @@ const runFilletTest = async (
|
||||
return new Error('Path to extrude node not found')
|
||||
}
|
||||
|
||||
// const radius = createLiteral(5) as Expr
|
||||
|
||||
const result = addFillet(ast, pathToSegmentNode, pathToExtrudeNode, radius)
|
||||
if (err(result)) {
|
||||
return result
|
||||
@ -68,7 +265,6 @@ const runFilletTest = async (
|
||||
|
||||
expect(newCode).toContain(expectedCode)
|
||||
}
|
||||
|
||||
describe('Testing addFillet', () => {
|
||||
/**
|
||||
* 1. Ideal Case
|
||||
|
@ -4,9 +4,11 @@ import {
|
||||
ObjectExpression,
|
||||
PathToNode,
|
||||
Program,
|
||||
ProgramMemory,
|
||||
Expr,
|
||||
VariableDeclaration,
|
||||
VariableDeclarator,
|
||||
sketchGroupFromKclValue,
|
||||
} from '../wasm'
|
||||
import {
|
||||
createCallExpressionStdLib,
|
||||
@ -28,62 +30,210 @@ import {
|
||||
getTagFromCallExpression,
|
||||
sketchLineHelperMap,
|
||||
} from '../std/sketch'
|
||||
import { err } from 'lib/trap'
|
||||
import { err, trap } from 'lib/trap'
|
||||
import { Selections, canFilletSelection } from 'lib/selections'
|
||||
import { KclCommandValue } from 'lib/commandTypes'
|
||||
import {
|
||||
ArtifactGraph,
|
||||
getExtrusionFromSuspectedPath,
|
||||
} from 'lang/std/artifactGraph'
|
||||
import { kclManager, engineCommandManager, editorManager } from 'lib/singletons'
|
||||
|
||||
export function addFillet(
|
||||
node: Program,
|
||||
pathToSegmentNode: PathToNode,
|
||||
pathToExtrudeNode: PathToNode,
|
||||
radius = createLiteral(5) as Expr
|
||||
// shouldPipe = false, // TODO: Implement this feature
|
||||
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
|
||||
// clone ast to make mutations safe
|
||||
let _node = structuredClone(node)
|
||||
|
||||
/**
|
||||
* Add Tag to the Segment Expression
|
||||
/**
|
||||
* Apply Fillet To Selection
|
||||
*/
|
||||
|
||||
// Find the specific sketch segment to tag with the new tag
|
||||
const sketchSegmentChunk = getNodeFromPath(
|
||||
_node,
|
||||
export function applyFilletToSelection(
|
||||
selection: Selections,
|
||||
radius: KclCommandValue
|
||||
): void | Error {
|
||||
// 1. get AST
|
||||
let ast = kclManager.ast
|
||||
const astResult = insertRadiusIntoAst(ast, radius)
|
||||
if (err(astResult)) return astResult
|
||||
|
||||
// 2. get path
|
||||
const programMemory = kclManager.programMemory
|
||||
const artifactGraph = engineCommandManager.artifactGraph
|
||||
const getPathToExtrudeForSegmentSelectionResult =
|
||||
getPathToExtrudeForSegmentSelection(
|
||||
ast,
|
||||
selection,
|
||||
programMemory,
|
||||
artifactGraph
|
||||
)
|
||||
if (err(getPathToExtrudeForSegmentSelectionResult))
|
||||
return getPathToExtrudeForSegmentSelectionResult
|
||||
const { pathToSegmentNode, pathToExtrudeNode } =
|
||||
getPathToExtrudeForSegmentSelectionResult
|
||||
|
||||
// 3. add fillet
|
||||
const addFilletResult = addFillet(
|
||||
ast,
|
||||
pathToSegmentNode,
|
||||
pathToExtrudeNode,
|
||||
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
|
||||
)
|
||||
if (trap(addFilletResult)) return addFilletResult
|
||||
const { modifiedAst, pathToFilletNode } = addFilletResult
|
||||
|
||||
// 4. update ast
|
||||
updateAstAndFocus(modifiedAst, pathToFilletNode)
|
||||
}
|
||||
|
||||
function insertRadiusIntoAst(
|
||||
ast: Program,
|
||||
radius: KclCommandValue
|
||||
): { ast: Program } | Error {
|
||||
try {
|
||||
// Validate and update AST
|
||||
if (
|
||||
'variableName' in radius &&
|
||||
radius.variableName &&
|
||||
radius.insertIndex !== undefined
|
||||
) {
|
||||
const newAst = structuredClone(ast)
|
||||
newAst.body.splice(radius.insertIndex, 0, radius.variableDeclarationAst)
|
||||
return { ast: newAst }
|
||||
}
|
||||
return { ast }
|
||||
} catch (error) {
|
||||
return new Error(`Failed to handle AST: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function getPathToExtrudeForSegmentSelection(
|
||||
ast: Program,
|
||||
selection: Selections,
|
||||
programMemory: ProgramMemory,
|
||||
artifactGraph: ArtifactGraph
|
||||
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
|
||||
const pathToSegmentNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selection.codeBasedSelections[0].range
|
||||
)
|
||||
|
||||
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
||||
ast,
|
||||
pathToSegmentNode,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(varDecNode)) return varDecNode
|
||||
const sketchVar = varDecNode.node.declarations[0].id.name
|
||||
|
||||
const sketchGroup = sketchGroupFromKclValue(
|
||||
kclManager.programMemory.get(sketchVar),
|
||||
sketchVar
|
||||
)
|
||||
if (trap(sketchGroup)) return sketchGroup
|
||||
|
||||
const extrusion = getExtrusionFromSuspectedPath(sketchGroup.id, artifactGraph)
|
||||
if (err(extrusion)) return extrusion
|
||||
|
||||
const pathToExtrudeNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
extrusion.codeRef.range
|
||||
)
|
||||
if (err(pathToExtrudeNode)) return pathToExtrudeNode
|
||||
|
||||
return { pathToSegmentNode, pathToExtrudeNode }
|
||||
}
|
||||
|
||||
async function updateAstAndFocus(
|
||||
modifiedAst: Program,
|
||||
pathToFilletNode: PathToNode
|
||||
) {
|
||||
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
||||
focusPath: pathToFilletNode,
|
||||
})
|
||||
if (updatedAst?.selections) {
|
||||
editorManager.selectRange(updatedAst?.selections)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Fillet
|
||||
*/
|
||||
|
||||
export function addFillet(
|
||||
ast: Program,
|
||||
pathToSegmentNode: PathToNode,
|
||||
pathToExtrudeNode: PathToNode,
|
||||
radius: Expr = createLiteral(5)
|
||||
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
|
||||
// Clone AST to ensure safe mutations
|
||||
const astClone = structuredClone(ast)
|
||||
|
||||
// Modify AST clone : TAG the sketch segment and retrieve tag
|
||||
const segmentResult = mutateAstWithTagForSketchSegment(
|
||||
astClone,
|
||||
pathToSegmentNode
|
||||
)
|
||||
if (err(segmentResult)) return segmentResult
|
||||
const { tag } = segmentResult
|
||||
|
||||
// Modify AST clone : Insert FILLET node and retrieve path to fillet
|
||||
const filletResult = mutateAstWithFilletNode(
|
||||
astClone,
|
||||
pathToExtrudeNode,
|
||||
radius,
|
||||
tag
|
||||
)
|
||||
if (err(filletResult)) return filletResult
|
||||
const { pathToFilletNode } = filletResult
|
||||
|
||||
return { modifiedAst: astClone, pathToFilletNode }
|
||||
}
|
||||
|
||||
function mutateAstWithTagForSketchSegment(
|
||||
astClone: Program,
|
||||
pathToSegmentNode: PathToNode
|
||||
): { modifiedAst: Program; tag: string } | Error {
|
||||
const segmentNode = getNodeFromPath<CallExpression>(
|
||||
astClone,
|
||||
pathToSegmentNode,
|
||||
'CallExpression'
|
||||
)
|
||||
if (err(sketchSegmentChunk)) return sketchSegmentChunk
|
||||
const { node: sketchSegmentNode } = sketchSegmentChunk as {
|
||||
node: CallExpression
|
||||
}
|
||||
if (err(segmentNode)) return segmentNode
|
||||
|
||||
// Check whether selection is a valid segment from sketchLineHelpersMap
|
||||
if (!(sketchSegmentNode.callee.name in sketchLineHelperMap)) {
|
||||
// Check whether selection is a valid segment
|
||||
if (!(segmentNode.node.callee.name in sketchLineHelperMap)) {
|
||||
return new Error('Selection is not a sketch segment')
|
||||
}
|
||||
|
||||
// Add tag to the sketch segment or use existing tag
|
||||
// a helper function that creates the updated node and applies the changes to the AST
|
||||
const taggedSegment = addTagForSketchOnFace(
|
||||
{
|
||||
// previousProgramMemory: programMemory,
|
||||
pathToNode: pathToSegmentNode,
|
||||
node: _node,
|
||||
node: astClone,
|
||||
},
|
||||
sketchSegmentNode.callee.name
|
||||
segmentNode.node.callee.name
|
||||
)
|
||||
if (err(taggedSegment)) return taggedSegment
|
||||
const { tag } = taggedSegment
|
||||
|
||||
/**
|
||||
* Find Extrude Expression automatically
|
||||
*/
|
||||
return { modifiedAst: astClone, tag }
|
||||
}
|
||||
|
||||
// 1. Get the sketch name
|
||||
function mutateAstWithFilletNode(
|
||||
astClone: Program,
|
||||
pathToExtrudeNode: PathToNode,
|
||||
radius: Expr,
|
||||
tag: string
|
||||
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
|
||||
// Locate the extrude call
|
||||
const locatedExtrudeDeclarator = locateExtrudeDeclarator(
|
||||
astClone,
|
||||
pathToExtrudeNode
|
||||
)
|
||||
if (err(locatedExtrudeDeclarator)) return locatedExtrudeDeclarator
|
||||
const { extrudeDeclarator } = locatedExtrudeDeclarator
|
||||
|
||||
/**
|
||||
* Add Fillet to the Extrude expression
|
||||
* Prepare changes to the AST
|
||||
*/
|
||||
|
||||
// Create the fillet call expression in one line
|
||||
const filletCall = createCallExpressionStdLib('fillet', [
|
||||
createObjectExpression({
|
||||
radius: radius,
|
||||
@ -92,65 +242,113 @@ export function addFillet(
|
||||
createPipeSubstitution(),
|
||||
])
|
||||
|
||||
// Locate the extrude call
|
||||
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
|
||||
_node,
|
||||
pathToExtrudeNode,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(extrudeChunk)) return extrudeChunk
|
||||
const { node: extrudeVarDecl } = extrudeChunk
|
||||
|
||||
const extrudeDeclarator = extrudeVarDecl.declarations[0]
|
||||
const extrudeInit = extrudeDeclarator.init
|
||||
|
||||
if (
|
||||
!extrudeDeclarator ||
|
||||
(extrudeInit.type !== 'CallExpression' &&
|
||||
extrudeInit.type !== 'PipeExpression')
|
||||
) {
|
||||
return new Error('Extrude PipeExpression / CallExpression not found.')
|
||||
}
|
||||
|
||||
// determine if extrude is in a PipeExpression or CallExpression
|
||||
/**
|
||||
* Mutate the AST
|
||||
*/
|
||||
|
||||
// CallExpression - no fillet
|
||||
// PipeExpression - fillet exists
|
||||
|
||||
const getPathToNodeOfFilletLiteral = (
|
||||
let pathToFilletNode: PathToNode = []
|
||||
|
||||
if (extrudeDeclarator.init.type === 'CallExpression') {
|
||||
// 1. case when no fillet exists
|
||||
|
||||
// modify ast with new fillet call by mutating the extrude node
|
||||
extrudeDeclarator.init = createPipeExpression([
|
||||
extrudeDeclarator.init,
|
||||
filletCall,
|
||||
])
|
||||
|
||||
// get path to the fillet node
|
||||
pathToFilletNode = getPathToNodeOfFilletLiteral(
|
||||
pathToExtrudeNode,
|
||||
extrudeDeclarator,
|
||||
tag
|
||||
)
|
||||
|
||||
return { modifiedAst: astClone, pathToFilletNode }
|
||||
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
|
||||
// 2. case when fillet exists
|
||||
|
||||
const existingFilletCall = extrudeDeclarator.init.body.find((node) => {
|
||||
return node.type === 'CallExpression' && node.callee.name === 'fillet'
|
||||
})
|
||||
|
||||
if (!existingFilletCall || existingFilletCall.type !== 'CallExpression') {
|
||||
return new Error('Fillet CallExpression not found.')
|
||||
}
|
||||
|
||||
// check if the existing fillet has the same tag as the new fillet
|
||||
const filletTag = getFilletTag(existingFilletCall)
|
||||
|
||||
if (filletTag !== tag) {
|
||||
// mutate the extrude node with the new fillet call
|
||||
extrudeDeclarator.init.body.push(filletCall)
|
||||
return {
|
||||
modifiedAst: astClone,
|
||||
pathToFilletNode: getPathToNodeOfFilletLiteral(
|
||||
pathToExtrudeNode,
|
||||
extrudeDeclarator,
|
||||
tag
|
||||
),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return new Error('Unsupported extrude type.')
|
||||
}
|
||||
|
||||
return { modifiedAst: astClone, pathToFilletNode }
|
||||
}
|
||||
|
||||
function locateExtrudeDeclarator(
|
||||
node: Program,
|
||||
pathToExtrudeNode: PathToNode
|
||||
): { extrudeDeclarator: VariableDeclarator } | Error {
|
||||
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
|
||||
node,
|
||||
pathToExtrudeNode,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(extrudeChunk)) return extrudeChunk
|
||||
|
||||
const { node: extrudeVarDecl } = extrudeChunk
|
||||
const extrudeDeclarator = extrudeVarDecl.declarations[0]
|
||||
if (!extrudeDeclarator) {
|
||||
return new Error('Extrude Declarator not found.')
|
||||
}
|
||||
|
||||
const extrudeInit = extrudeDeclarator?.init
|
||||
if (!extrudeInit) {
|
||||
return new Error('Extrude Init not found.')
|
||||
}
|
||||
|
||||
if (
|
||||
extrudeInit.type !== 'CallExpression' &&
|
||||
extrudeInit.type !== 'PipeExpression'
|
||||
) {
|
||||
return new Error('Extrude must be a PipeExpression or CallExpression')
|
||||
}
|
||||
|
||||
return { extrudeDeclarator }
|
||||
}
|
||||
|
||||
function getPathToNodeOfFilletLiteral(
|
||||
pathToExtrudeNode: PathToNode,
|
||||
extrudeDeclarator: VariableDeclarator,
|
||||
tag: string
|
||||
): PathToNode => {
|
||||
let pathToFilletObj: any
|
||||
): PathToNode {
|
||||
let pathToFilletObj: PathToNode = []
|
||||
let inFillet = false
|
||||
|
||||
traverse(extrudeDeclarator.init, {
|
||||
enter(node, path) {
|
||||
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
|
||||
inFillet = true
|
||||
}
|
||||
if (inFillet && node.type === 'ObjectExpression') {
|
||||
const hasTag = node.properties.some((prop) => {
|
||||
const isTagProp = prop.key.name === 'tags'
|
||||
if (isTagProp && prop.value.type === 'ArrayExpression') {
|
||||
return prop.value.elements.some(
|
||||
(element) =>
|
||||
element.type === 'Identifier' && element.name === tag
|
||||
)
|
||||
}
|
||||
return false
|
||||
})
|
||||
if (!hasTag) return false
|
||||
pathToFilletObj = path
|
||||
node.properties.forEach((prop, index) => {
|
||||
if (prop.key.name === 'radius') {
|
||||
pathToFilletObj.push(
|
||||
['properties', 'ObjectExpression'],
|
||||
[index, 'index'],
|
||||
['value', 'Property']
|
||||
)
|
||||
}
|
||||
})
|
||||
if (!hasTag(node, tag)) return false
|
||||
pathToFilletObj = getPathToRadiusLiteral(node, path)
|
||||
}
|
||||
},
|
||||
leave(node) {
|
||||
@ -162,6 +360,7 @@ export function addFillet(
|
||||
let indexOfPipeExpression = pathToExtrudeNode.findIndex(
|
||||
(path) => path[1] === 'PipeExpression'
|
||||
)
|
||||
|
||||
indexOfPipeExpression =
|
||||
indexOfPipeExpression === -1
|
||||
? pathToExtrudeNode.length
|
||||
@ -171,34 +370,34 @@ export function addFillet(
|
||||
...pathToExtrudeNode.slice(0, indexOfPipeExpression),
|
||||
...pathToFilletObj,
|
||||
]
|
||||
}
|
||||
|
||||
function hasTag(node: ObjectExpression, tag: string): boolean {
|
||||
return node.properties.some((prop) => {
|
||||
if (prop.key.name === 'tags' && prop.value.type === 'ArrayExpression') {
|
||||
return prop.value.elements.some(
|
||||
(element) => element.type === 'Identifier' && element.name === tag
|
||||
)
|
||||
}
|
||||
|
||||
if (extrudeInit.type === 'CallExpression') {
|
||||
// 1. no fillet case
|
||||
extrudeDeclarator.init = createPipeExpression([extrudeInit, filletCall])
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToFilletNode: getPathToNodeOfFilletLiteral(
|
||||
pathToExtrudeNode,
|
||||
extrudeDeclarator,
|
||||
tag
|
||||
),
|
||||
}
|
||||
} else if (extrudeInit.type === 'PipeExpression') {
|
||||
// 2. fillet case
|
||||
|
||||
// there are 2 options here:
|
||||
|
||||
const existingFilletCall = extrudeInit.body.find((node) => {
|
||||
return node.type === 'CallExpression' && node.callee.name === 'fillet'
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
if (!existingFilletCall || existingFilletCall.type !== 'CallExpression') {
|
||||
return new Error('Fillet CallExpression not found.')
|
||||
function getPathToRadiusLiteral(node: ObjectExpression, path: any): PathToNode {
|
||||
let pathToFilletObj = path
|
||||
node.properties.forEach((prop, index) => {
|
||||
if (prop.key.name === 'radius') {
|
||||
pathToFilletObj.push(
|
||||
['properties', 'ObjectExpression'],
|
||||
[index, 'index'],
|
||||
['value', 'Property']
|
||||
)
|
||||
}
|
||||
})
|
||||
return pathToFilletObj
|
||||
}
|
||||
|
||||
// check if the existing fillet has the same tag as the new fillet
|
||||
let filletTag = null
|
||||
function getFilletTag(existingFilletCall: CallExpression): string | null {
|
||||
if (existingFilletCall.arguments[0].type === 'ObjectExpression') {
|
||||
const properties = (existingFilletCall.arguments[0] as ObjectExpression)
|
||||
.properties
|
||||
@ -206,31 +405,17 @@ export function addFillet(
|
||||
if (tagsProperty && tagsProperty.value.type === 'ArrayExpression') {
|
||||
const elements = (tagsProperty.value as ArrayExpression).elements
|
||||
if (elements.length > 0 && elements[0].type === 'Identifier') {
|
||||
filletTag = elements[0].name
|
||||
return elements[0].name
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return new Error('Expected an ObjectExpression node')
|
||||
}
|
||||
|
||||
if (filletTag !== tag) {
|
||||
extrudeInit.body.push(filletCall)
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToFilletNode: getPathToNodeOfFilletLiteral(
|
||||
pathToExtrudeNode,
|
||||
extrudeDeclarator,
|
||||
tag
|
||||
),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return new Error('Unsupported extrude type.')
|
||||
}
|
||||
|
||||
return new Error('Unsupported extrude type.')
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Button states
|
||||
*/
|
||||
|
||||
export const hasValidFilletSelection = ({
|
||||
selectionRanges,
|
||||
ast,
|
||||
|
@ -4,7 +4,6 @@ import {
|
||||
VariableDeclarator,
|
||||
parse,
|
||||
recast,
|
||||
sketchGroupFromKclValue,
|
||||
} from 'lang/wasm'
|
||||
import { Axis, Selection, Selections, updateSelections } from 'lib/selections'
|
||||
import { assign, createMachine } from 'xstate'
|
||||
@ -35,7 +34,7 @@ import {
|
||||
setEqualLengthInfo,
|
||||
} from 'components/Toolbar/EqualLength'
|
||||
import { deleteFromSelection, extrudeSketch } from 'lang/modifyAst'
|
||||
import { addFillet } from 'lang/modifyAst/addFillet'
|
||||
import { applyFilletToSelection } from 'lang/modifyAst/addFillet'
|
||||
import { getNodeFromPath } from '../lang/queryAst'
|
||||
import {
|
||||
applyConstraintEqualAngle,
|
||||
@ -59,7 +58,6 @@ import { Coords2d } from 'lang/std/sketch'
|
||||
import { deleteSegment } from 'clientSideScene/ClientSideSceneComp'
|
||||
import { executeAst } from 'lang/langHelpers'
|
||||
import toast from 'react-hot-toast'
|
||||
import { getExtrusionFromSuspectedPath } from 'lang/std/artifactGraph'
|
||||
|
||||
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
|
||||
|
||||
@ -1163,65 +1161,16 @@ export const modelingMachine = createMachine(
|
||||
'AST fillet': async (_, event) => {
|
||||
if (!event.data) return
|
||||
|
||||
// Extract inputs
|
||||
const { selection, radius } = event.data
|
||||
let ast = kclManager.ast
|
||||
|
||||
if (
|
||||
'variableName' in radius &&
|
||||
radius.variableName &&
|
||||
radius.insertIndex !== undefined
|
||||
) {
|
||||
const newBody = [...ast.body]
|
||||
newBody.splice(radius.insertIndex, 0, radius.variableDeclarationAst)
|
||||
ast.body = newBody
|
||||
}
|
||||
|
||||
const pathToSegmentNode = getNodePathFromSourceRange(
|
||||
ast,
|
||||
selection.codeBasedSelections[0].range
|
||||
// Apply fillet to selection
|
||||
const applyFilletToSelectionResult = applyFilletToSelection(
|
||||
selection,
|
||||
radius
|
||||
)
|
||||
|
||||
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
||||
ast,
|
||||
pathToSegmentNode,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (err(varDecNode)) return
|
||||
const sketchVar = varDecNode.node.declarations[0].id.name
|
||||
const sketchGroup = sketchGroupFromKclValue(
|
||||
kclManager.programMemory.get(sketchVar),
|
||||
sketchVar
|
||||
)
|
||||
if (trap(sketchGroup)) return
|
||||
const extrusion = getExtrusionFromSuspectedPath(
|
||||
sketchGroup.id,
|
||||
engineCommandManager.artifactGraph
|
||||
)
|
||||
const pathToExtrudeNode = err(extrusion)
|
||||
? []
|
||||
: getNodePathFromSourceRange(ast, extrusion.codeRef.range)
|
||||
|
||||
// we assume that there is only one body related to the sketch
|
||||
// and apply the fillet to it
|
||||
|
||||
const addFilletResult = addFillet(
|
||||
ast,
|
||||
pathToSegmentNode,
|
||||
pathToExtrudeNode,
|
||||
'variableName' in radius
|
||||
? radius.variableIdentifierAst
|
||||
: radius.valueAst
|
||||
)
|
||||
|
||||
if (trap(addFilletResult)) return
|
||||
const { modifiedAst, pathToFilletNode } = addFilletResult
|
||||
|
||||
const updatedAst = await kclManager.updateAst(modifiedAst, true, {
|
||||
focusPath: pathToFilletNode,
|
||||
})
|
||||
if (updatedAst?.selections) {
|
||||
editorManager.selectRange(updatedAst?.selections)
|
||||
}
|
||||
if (err(applyFilletToSelectionResult))
|
||||
return applyFilletToSelectionResult
|
||||
},
|
||||
'conditionally equip line tool': (_, { type }) => {
|
||||
if (type === 'done.invoke.animate-to-face') {
|
||||
|
Reference in New Issue
Block a user