Refactor Fillet Logic to Combine Multiple Tags Within the Same Fillet Expression (#4058)

* combine tags + tests

* delete getFilletTag function

* fix playwright test

* make eslint happy

* delete missed 'const'

* delete const rev2
This commit is contained in:
max
2024-10-03 11:14:02 +02:00
committed by GitHub
parent c84c0b0fef
commit 46d335f916
4 changed files with 226 additions and 457 deletions

View File

@ -1,18 +1,14 @@
import {
ArrayExpression,
CallExpression,
ObjectExpression,
PathToNode,
Program,
ProgramMemory,
Expr,
VariableDeclaration,
VariableDeclarator,
sketchFromKclValue,
} from '../wasm'
import {
createCallExpressionStdLib,
createLiteral,
createPipeSubstitution,
createObjectExpression,
createArrayExpression,
@ -39,70 +35,142 @@ import {
} from 'lang/std/artifactGraph'
import { kclManager, engineCommandManager, editorManager } from 'lib/singletons'
/**
* Apply Fillet To Selection
*/
// Apply Fillet To Selection
export function applyFilletToSelection(
ast: Program,
selection: Selections,
radius: KclCommandValue
): void | Error {
// 1. clone ast
let clonedAst = structuredClone(ast)
// 2. modify ast clone with fillet and tag
const result = modifyAstWithFilletAndTag(clonedAst, selection, radius)
// 1. clone and modify with fillet and tag
const result = modifyAstCloneWithFilletAndTag(ast, selection, radius)
if (err(result)) return result
const { modifiedAst, pathToFilletNode } = result
// 3. update ast
// 2. update ast
// eslint-disable-next-line @typescript-eslint/no-floating-promises
updateAstAndFocus(modifiedAst, pathToFilletNode)
}
export function modifyAstWithFilletAndTag(
export function modifyAstCloneWithFilletAndTag(
ast: Program,
selection: Selections,
radius: KclCommandValue
): { modifiedAst: Program; pathToFilletNode: Array<PathToNode> } | Error {
const astResult = insertRadiusIntoAst(ast, radius)
if (err(astResult)) return astResult
const programMemory = kclManager.programMemory
const artifactGraph = engineCommandManager.artifactGraph
let clonedAst = structuredClone(ast)
const clonedAstForGetExtrude = structuredClone(ast)
let pathToFilletNodes: Array<PathToNode> = []
const astResult = insertRadiusIntoAst(clonedAst, radius)
if (err(astResult)) return astResult
const artifactGraph = engineCommandManager.artifactGraph
// Step 1: modify ast with tags and group them by extrude nodes (bodies)
const extrudeToTagsMap: Map<PathToNode, string[]> = new Map()
const lookupMap: Map<string, PathToNode> = new Map() // work around for Map key comparison
for (const selectionRange of selection.codeBasedSelections) {
const singleSelection = {
codeBasedSelections: [selectionRange],
otherSelections: [],
}
const getPathToExtrudeForSegmentSelectionResult =
getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
singleSelection,
programMemory,
artifactGraph
)
if (err(getPathToExtrudeForSegmentSelectionResult))
return getPathToExtrudeForSegmentSelectionResult
const { pathToSegmentNode, pathToExtrudeNode } =
getPathToExtrudeForSegmentSelectionResult
const addFilletResult = addFillet(
clonedAst,
pathToSegmentNode,
pathToExtrudeNode,
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
const result = getPathToExtrudeForSegmentSelection(
clonedAstForGetExtrude,
singleSelection,
artifactGraph
)
if (trap(addFilletResult)) return addFilletResult
const { modifiedAst, pathToFilletNode } = addFilletResult
clonedAst = modifiedAst
pathToFilletNodes.push(pathToFilletNode)
if (err(result)) return result
const { pathToSegmentNode, pathToExtrudeNode } = result
const tagResult = mutateAstWithTagForSketchSegment(
clonedAst,
pathToSegmentNode
)
if (err(tagResult)) return tagResult
const { tag } = tagResult
// Group tags by their corresponding extrude node
const extrudeKey = JSON.stringify(pathToExtrudeNode)
if (lookupMap.has(extrudeKey)) {
const existingPath = lookupMap.get(extrudeKey)
if (!existingPath) return new Error('Path to extrude node not found.')
extrudeToTagsMap.get(existingPath)?.push(tag)
} else {
lookupMap.set(extrudeKey, pathToExtrudeNode)
extrudeToTagsMap.set(pathToExtrudeNode, [tag])
}
}
// Step 2: Apply fillet(s) for each extrude node (body)
let pathToFilletNodes: Array<PathToNode> = []
for (const [pathToExtrudeNode, tags] of extrudeToTagsMap.entries()) {
// Create a fillet expression with multiple tags
const radiusValue =
'variableName' in radius ? radius.variableIdentifierAst : radius.valueAst
const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({
radius: radiusValue,
tags: createArrayExpression(tags.map((tag) => createIdentifier(tag))),
}),
createPipeSubstitution(),
])
// Locate the extrude call
const locatedExtrudeDeclarator = locateExtrudeDeclarator(
clonedAst,
pathToExtrudeNode
)
if (err(locatedExtrudeDeclarator)) return locatedExtrudeDeclarator
const { extrudeDeclarator } = locatedExtrudeDeclarator
// Modify the extrude expression to include this fillet expression
// CallExpression - no fillet
// PipeExpression - fillet exists
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,
tags[0]
)
pathToFilletNodes.push(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.')
}
// mutate the extrude node with the new fillet call
extrudeDeclarator.init.body.push(filletCall)
// get path to the fillet node
pathToFilletNode = getPathToNodeOfFilletLiteral(
pathToExtrudeNode,
extrudeDeclarator,
tags[0]
)
pathToFilletNodes.push(pathToFilletNode)
} else {
return new Error('Unsupported extrude type.')
}
}
return { modifiedAst: clonedAst, pathToFilletNode: pathToFilletNodes }
}
@ -131,7 +199,6 @@ function insertRadiusIntoAst(
export function getPathToExtrudeForSegmentSelection(
ast: Program,
selection: Selections,
programMemory: ProgramMemory,
artifactGraph: ArtifactGraph
): { pathToSegmentNode: PathToNode; pathToExtrudeNode: PathToNode } | Error {
const pathToSegmentNode = getNodePathFromSourceRange(
@ -177,40 +244,6 @@ async function updateAstAndFocus(
}
}
/**
* 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
@ -243,91 +276,6 @@ function mutateAstWithTagForSketchSegment(
return { modifiedAst: astClone, tag }
}
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
/**
* Prepare changes to the AST
*/
const filletCall = createCallExpressionStdLib('fillet', [
createObjectExpression({
radius: radius,
tags: createArrayExpression([createIdentifier(tag)]),
}),
createPipeSubstitution(),
])
/**
* Mutate the AST
*/
// CallExpression - no fillet
// PipeExpression - fillet exists
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
@ -424,24 +372,7 @@ function getPathToRadiusLiteral(node: ObjectExpression, path: any): PathToNode {
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
*/
// Button states
export const hasValidFilletSelection = ({
selectionRanges,