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,
|
Expr,
|
||||||
Program,
|
Program,
|
||||||
CallExpression,
|
CallExpression,
|
||||||
|
makeDefaultPlanes,
|
||||||
|
PipeExpression,
|
||||||
|
VariableDeclaration,
|
||||||
} from '../wasm'
|
} from '../wasm'
|
||||||
import {
|
import {
|
||||||
addFillet,
|
addFillet,
|
||||||
|
getPathToExtrudeForSegmentSelection,
|
||||||
hasValidFilletSelection,
|
hasValidFilletSelection,
|
||||||
isTagUsedInFillet,
|
isTagUsedInFillet,
|
||||||
} from './addFillet'
|
} from './addFillet'
|
||||||
@ -16,9 +20,204 @@ import { getNodeFromPath, getNodePathFromSourceRange } from '../queryAst'
|
|||||||
import { createLiteral } from 'lang/modifyAst'
|
import { createLiteral } from 'lang/modifyAst'
|
||||||
import { err } from 'lib/trap'
|
import { err } from 'lib/trap'
|
||||||
import { Selections } from 'lib/selections'
|
import { Selections } from 'lib/selections'
|
||||||
|
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
|
import { VITE_KC_DEV_TOKEN } from 'env'
|
||||||
|
|
||||||
beforeAll(async () => {
|
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 (
|
const runFilletTest = async (
|
||||||
@ -57,8 +256,6 @@ const runFilletTest = async (
|
|||||||
return new Error('Path to extrude node not found')
|
return new Error('Path to extrude node not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
// const radius = createLiteral(5) as Expr
|
|
||||||
|
|
||||||
const result = addFillet(ast, pathToSegmentNode, pathToExtrudeNode, radius)
|
const result = addFillet(ast, pathToSegmentNode, pathToExtrudeNode, radius)
|
||||||
if (err(result)) {
|
if (err(result)) {
|
||||||
return result
|
return result
|
||||||
@ -68,7 +265,6 @@ const runFilletTest = async (
|
|||||||
|
|
||||||
expect(newCode).toContain(expectedCode)
|
expect(newCode).toContain(expectedCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Testing addFillet', () => {
|
describe('Testing addFillet', () => {
|
||||||
/**
|
/**
|
||||||
* 1. Ideal Case
|
* 1. Ideal Case
|
||||||
|
@ -4,9 +4,11 @@ import {
|
|||||||
ObjectExpression,
|
ObjectExpression,
|
||||||
PathToNode,
|
PathToNode,
|
||||||
Program,
|
Program,
|
||||||
|
ProgramMemory,
|
||||||
Expr,
|
Expr,
|
||||||
VariableDeclaration,
|
VariableDeclaration,
|
||||||
VariableDeclarator,
|
VariableDeclarator,
|
||||||
|
sketchGroupFromKclValue,
|
||||||
} from '../wasm'
|
} from '../wasm'
|
||||||
import {
|
import {
|
||||||
createCallExpressionStdLib,
|
createCallExpressionStdLib,
|
||||||
@ -28,62 +30,210 @@ import {
|
|||||||
getTagFromCallExpression,
|
getTagFromCallExpression,
|
||||||
sketchLineHelperMap,
|
sketchLineHelperMap,
|
||||||
} from '../std/sketch'
|
} from '../std/sketch'
|
||||||
import { err } from 'lib/trap'
|
import { err, trap } from 'lib/trap'
|
||||||
import { Selections, canFilletSelection } from 'lib/selections'
|
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'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply Fillet To Selection
|
||||||
|
*/
|
||||||
|
|
||||||
|
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(
|
export function addFillet(
|
||||||
node: Program,
|
ast: Program,
|
||||||
pathToSegmentNode: PathToNode,
|
pathToSegmentNode: PathToNode,
|
||||||
pathToExtrudeNode: PathToNode,
|
pathToExtrudeNode: PathToNode,
|
||||||
radius = createLiteral(5) as Expr
|
radius: Expr = createLiteral(5)
|
||||||
// shouldPipe = false, // TODO: Implement this feature
|
|
||||||
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
|
): { modifiedAst: Program; pathToFilletNode: PathToNode } | Error {
|
||||||
// clone ast to make mutations safe
|
// Clone AST to ensure safe mutations
|
||||||
let _node = structuredClone(node)
|
const astClone = structuredClone(ast)
|
||||||
|
|
||||||
/**
|
// Modify AST clone : TAG the sketch segment and retrieve tag
|
||||||
* Add Tag to the Segment Expression
|
const segmentResult = mutateAstWithTagForSketchSegment(
|
||||||
*/
|
astClone,
|
||||||
|
pathToSegmentNode
|
||||||
|
)
|
||||||
|
if (err(segmentResult)) return segmentResult
|
||||||
|
const { tag } = segmentResult
|
||||||
|
|
||||||
// Find the specific sketch segment to tag with the new tag
|
// Modify AST clone : Insert FILLET node and retrieve path to fillet
|
||||||
const sketchSegmentChunk = getNodeFromPath(
|
const filletResult = mutateAstWithFilletNode(
|
||||||
_node,
|
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,
|
pathToSegmentNode,
|
||||||
'CallExpression'
|
'CallExpression'
|
||||||
)
|
)
|
||||||
if (err(sketchSegmentChunk)) return sketchSegmentChunk
|
if (err(segmentNode)) return segmentNode
|
||||||
const { node: sketchSegmentNode } = sketchSegmentChunk as {
|
|
||||||
node: CallExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether selection is a valid segment from sketchLineHelpersMap
|
// Check whether selection is a valid segment
|
||||||
if (!(sketchSegmentNode.callee.name in sketchLineHelperMap)) {
|
if (!(segmentNode.node.callee.name in sketchLineHelperMap)) {
|
||||||
return new Error('Selection is not a sketch segment')
|
return new Error('Selection is not a sketch segment')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add tag to the sketch segment or use existing tag
|
// 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(
|
const taggedSegment = addTagForSketchOnFace(
|
||||||
{
|
{
|
||||||
// previousProgramMemory: programMemory,
|
|
||||||
pathToNode: pathToSegmentNode,
|
pathToNode: pathToSegmentNode,
|
||||||
node: _node,
|
node: astClone,
|
||||||
},
|
},
|
||||||
sketchSegmentNode.callee.name
|
segmentNode.node.callee.name
|
||||||
)
|
)
|
||||||
if (err(taggedSegment)) return taggedSegment
|
if (err(taggedSegment)) return taggedSegment
|
||||||
const { tag } = taggedSegment
|
const { tag } = taggedSegment
|
||||||
|
|
||||||
/**
|
return { modifiedAst: astClone, tag }
|
||||||
* Find Extrude Expression automatically
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
// 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', [
|
const filletCall = createCallExpressionStdLib('fillet', [
|
||||||
createObjectExpression({
|
createObjectExpression({
|
||||||
radius: radius,
|
radius: radius,
|
||||||
@ -92,104 +242,36 @@ export function addFillet(
|
|||||||
createPipeSubstitution(),
|
createPipeSubstitution(),
|
||||||
])
|
])
|
||||||
|
|
||||||
// Locate the extrude call
|
/**
|
||||||
const extrudeChunk = getNodeFromPath<VariableDeclaration>(
|
* Mutate the AST
|
||||||
_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
|
|
||||||
|
|
||||||
// CallExpression - no fillet
|
// CallExpression - no fillet
|
||||||
// PipeExpression - fillet exists
|
// PipeExpression - fillet exists
|
||||||
|
|
||||||
const getPathToNodeOfFilletLiteral = (
|
let pathToFilletNode: PathToNode = []
|
||||||
pathToExtrudeNode: PathToNode,
|
|
||||||
extrudeDeclarator: VariableDeclarator,
|
if (extrudeDeclarator.init.type === 'CallExpression') {
|
||||||
tag: string
|
// 1. case when no fillet exists
|
||||||
): PathToNode => {
|
|
||||||
let pathToFilletObj: any
|
// modify ast with new fillet call by mutating the extrude node
|
||||||
let inFillet = false
|
extrudeDeclarator.init = createPipeExpression([
|
||||||
traverse(extrudeDeclarator.init, {
|
extrudeDeclarator.init,
|
||||||
enter(node, path) {
|
filletCall,
|
||||||
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
|
])
|
||||||
inFillet = true
|
|
||||||
}
|
// get path to the fillet node
|
||||||
if (inFillet && node.type === 'ObjectExpression') {
|
pathToFilletNode = getPathToNodeOfFilletLiteral(
|
||||||
const hasTag = node.properties.some((prop) => {
|
pathToExtrudeNode,
|
||||||
const isTagProp = prop.key.name === 'tags'
|
extrudeDeclarator,
|
||||||
if (isTagProp && prop.value.type === 'ArrayExpression') {
|
tag
|
||||||
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']
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
leave(node) {
|
|
||||||
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
|
|
||||||
inFillet = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
let indexOfPipeExpression = pathToExtrudeNode.findIndex(
|
|
||||||
(path) => path[1] === 'PipeExpression'
|
|
||||||
)
|
)
|
||||||
indexOfPipeExpression =
|
|
||||||
indexOfPipeExpression === -1
|
|
||||||
? pathToExtrudeNode.length
|
|
||||||
: indexOfPipeExpression
|
|
||||||
|
|
||||||
return [
|
return { modifiedAst: astClone, pathToFilletNode }
|
||||||
...pathToExtrudeNode.slice(0, indexOfPipeExpression),
|
} else if (extrudeDeclarator.init.type === 'PipeExpression') {
|
||||||
...pathToFilletObj,
|
// 2. case when fillet exists
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extrudeInit.type === 'CallExpression') {
|
const existingFilletCall = extrudeDeclarator.init.body.find((node) => {
|
||||||
// 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 node.type === 'CallExpression' && node.callee.name === 'fillet'
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -198,25 +280,13 @@ export function addFillet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check if the existing fillet has the same tag as the new fillet
|
// check if the existing fillet has the same tag as the new fillet
|
||||||
let filletTag = null
|
const filletTag = getFilletTag(existingFilletCall)
|
||||||
if (existingFilletCall.arguments[0].type === 'ObjectExpression') {
|
|
||||||
const properties = (existingFilletCall.arguments[0] as ObjectExpression)
|
|
||||||
.properties
|
|
||||||
const tagsProperty = properties.find((prop) => prop.key.name === 'tags')
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return new Error('Expected an ObjectExpression node')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filletTag !== tag) {
|
if (filletTag !== tag) {
|
||||||
extrudeInit.body.push(filletCall)
|
// mutate the extrude node with the new fillet call
|
||||||
|
extrudeDeclarator.init.body.push(filletCall)
|
||||||
return {
|
return {
|
||||||
modifiedAst: _node,
|
modifiedAst: astClone,
|
||||||
pathToFilletNode: getPathToNodeOfFilletLiteral(
|
pathToFilletNode: getPathToNodeOfFilletLiteral(
|
||||||
pathToExtrudeNode,
|
pathToExtrudeNode,
|
||||||
extrudeDeclarator,
|
extrudeDeclarator,
|
||||||
@ -228,9 +298,124 @@ export function addFillet(
|
|||||||
return new Error('Unsupported extrude type.')
|
return new Error('Unsupported extrude type.')
|
||||||
}
|
}
|
||||||
|
|
||||||
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: 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') {
|
||||||
|
if (!hasTag(node, tag)) return false
|
||||||
|
pathToFilletObj = getPathToRadiusLiteral(node, path)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leave(node) {
|
||||||
|
if (node.type === 'CallExpression' && node.callee.name === 'fillet') {
|
||||||
|
inFillet = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
let indexOfPipeExpression = pathToExtrudeNode.findIndex(
|
||||||
|
(path) => path[1] === 'PipeExpression'
|
||||||
|
)
|
||||||
|
|
||||||
|
indexOfPipeExpression =
|
||||||
|
indexOfPipeExpression === -1
|
||||||
|
? pathToExtrudeNode.length
|
||||||
|
: indexOfPipeExpression
|
||||||
|
|
||||||
|
return [
|
||||||
|
...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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilletTag(existingFilletCall: CallExpression): string | null {
|
||||||
|
if (existingFilletCall.arguments[0].type === 'ObjectExpression') {
|
||||||
|
const properties = (existingFilletCall.arguments[0] as ObjectExpression)
|
||||||
|
.properties
|
||||||
|
const tagsProperty = properties.find((prop) => prop.key.name === 'tags')
|
||||||
|
if (tagsProperty && tagsProperty.value.type === 'ArrayExpression') {
|
||||||
|
const elements = (tagsProperty.value as ArrayExpression).elements
|
||||||
|
if (elements.length > 0 && elements[0].type === 'Identifier') {
|
||||||
|
return elements[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button states
|
||||||
|
*/
|
||||||
|
|
||||||
export const hasValidFilletSelection = ({
|
export const hasValidFilletSelection = ({
|
||||||
selectionRanges,
|
selectionRanges,
|
||||||
ast,
|
ast,
|
||||||
|
@ -4,7 +4,6 @@ import {
|
|||||||
VariableDeclarator,
|
VariableDeclarator,
|
||||||
parse,
|
parse,
|
||||||
recast,
|
recast,
|
||||||
sketchGroupFromKclValue,
|
|
||||||
} from 'lang/wasm'
|
} from 'lang/wasm'
|
||||||
import { Axis, Selection, Selections, updateSelections } from 'lib/selections'
|
import { Axis, Selection, Selections, updateSelections } from 'lib/selections'
|
||||||
import { assign, createMachine } from 'xstate'
|
import { assign, createMachine } from 'xstate'
|
||||||
@ -35,7 +34,7 @@ import {
|
|||||||
setEqualLengthInfo,
|
setEqualLengthInfo,
|
||||||
} from 'components/Toolbar/EqualLength'
|
} from 'components/Toolbar/EqualLength'
|
||||||
import { deleteFromSelection, extrudeSketch } from 'lang/modifyAst'
|
import { deleteFromSelection, extrudeSketch } from 'lang/modifyAst'
|
||||||
import { addFillet } from 'lang/modifyAst/addFillet'
|
import { applyFilletToSelection } from 'lang/modifyAst/addFillet'
|
||||||
import { getNodeFromPath } from '../lang/queryAst'
|
import { getNodeFromPath } from '../lang/queryAst'
|
||||||
import {
|
import {
|
||||||
applyConstraintEqualAngle,
|
applyConstraintEqualAngle,
|
||||||
@ -59,7 +58,6 @@ import { Coords2d } from 'lang/std/sketch'
|
|||||||
import { deleteSegment } from 'clientSideScene/ClientSideSceneComp'
|
import { deleteSegment } from 'clientSideScene/ClientSideSceneComp'
|
||||||
import { executeAst } from 'lang/langHelpers'
|
import { executeAst } from 'lang/langHelpers'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { getExtrusionFromSuspectedPath } from 'lang/std/artifactGraph'
|
|
||||||
|
|
||||||
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
|
export const MODELING_PERSIST_KEY = 'MODELING_PERSIST_KEY'
|
||||||
|
|
||||||
@ -1163,65 +1161,16 @@ export const modelingMachine = createMachine(
|
|||||||
'AST fillet': async (_, event) => {
|
'AST fillet': async (_, event) => {
|
||||||
if (!event.data) return
|
if (!event.data) return
|
||||||
|
|
||||||
|
// Extract inputs
|
||||||
const { selection, radius } = event.data
|
const { selection, radius } = event.data
|
||||||
let ast = kclManager.ast
|
|
||||||
|
|
||||||
if (
|
// Apply fillet to selection
|
||||||
'variableName' in radius &&
|
const applyFilletToSelectionResult = applyFilletToSelection(
|
||||||
radius.variableName &&
|
selection,
|
||||||
radius.insertIndex !== undefined
|
radius
|
||||||
) {
|
|
||||||
const newBody = [...ast.body]
|
|
||||||
newBody.splice(radius.insertIndex, 0, radius.variableDeclarationAst)
|
|
||||||
ast.body = newBody
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathToSegmentNode = getNodePathFromSourceRange(
|
|
||||||
ast,
|
|
||||||
selection.codeBasedSelections[0].range
|
|
||||||
)
|
)
|
||||||
|
if (err(applyFilletToSelectionResult))
|
||||||
const varDecNode = getNodeFromPath<VariableDeclaration>(
|
return applyFilletToSelectionResult
|
||||||
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)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
'conditionally equip line tool': (_, { type }) => {
|
'conditionally equip line tool': (_, { type }) => {
|
||||||
if (type === 'done.invoke.animate-to-face') {
|
if (type === 'done.invoke.animate-to-face') {
|
||||||
|
Reference in New Issue
Block a user