* always enter edit mode * initial blocking of extra code-mirror updates * dry out code * rejig selections * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * clean up * stream clean up * update export * sketch mode can be entered and exited for extrude faces But has bugs * startSketchOn working in some cases, editsketch animation working but not orientation of instersection plane etc * Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)" This reverts commit406fca4c55
. * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * remove comment * add sketch on face e2e test * tweenCamToNegYAxis should respect reduced motion * initial sketch on face working with test * remove temporary toolbar button and xState flow * un-used vars * snapshot test tweak * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * type tidy up * Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)" This reverts commitc39b8ebf95
. * Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)" This reverts commitfecf6f490a
. * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * rename * sketch on sketch on sketch * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * typo * startSketchOn Endcaps end works, start is weird still * clear selections for entity_ids that are not recognised * fix sketch on end cap of second order extrustion * tiny clean up * fix sketch on close segment/face * clean up 'lastCodeMirrorSelectionUpdatedFromScene' * add code mode test for sketchOnExtrudedFace * make end cap selection more robust * update js artifacts for extrudes * update kcl docs * clean up --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
652 lines
16 KiB
TypeScript
652 lines
16 KiB
TypeScript
import { Selection } from 'lib/selections'
|
|
import {
|
|
Program,
|
|
CallExpression,
|
|
PipeExpression,
|
|
VariableDeclaration,
|
|
VariableDeclarator,
|
|
Value,
|
|
Literal,
|
|
PipeSubstitution,
|
|
Identifier,
|
|
ArrayExpression,
|
|
ObjectExpression,
|
|
UnaryExpression,
|
|
BinaryExpression,
|
|
PathToNode,
|
|
ProgramMemory,
|
|
} from './wasm'
|
|
import {
|
|
findAllPreviousVariables,
|
|
getNodeFromPath,
|
|
getNodePathFromSourceRange,
|
|
isNodeSafeToReplace,
|
|
} from './queryAst'
|
|
import { addTagForSketchOnFace } from './std/sketch'
|
|
import { isLiteralArrayOrStatic } from './std/sketchcombos'
|
|
import { DefaultPlaneStr } from 'clientSideScene/sceneEntities'
|
|
import { roundOff } from 'lib/utils'
|
|
|
|
export function startSketchOnDefault(
|
|
node: Program,
|
|
axis: DefaultPlaneStr,
|
|
name = ''
|
|
): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
|
|
const _node = { ...node }
|
|
const _name = name || findUniqueName(node, 'part')
|
|
|
|
const startSketchOn = createCallExpressionStdLib('startSketchOn', [
|
|
createLiteral(axis),
|
|
])
|
|
|
|
const variableDeclaration = createVariableDeclaration(_name, startSketchOn)
|
|
_node.body = [...node.body, variableDeclaration]
|
|
const sketchIndex = _node.body.length - 1
|
|
|
|
let pathToNode: PathToNode = [
|
|
['body', ''],
|
|
[sketchIndex, 'index'],
|
|
['declarations', 'VariableDeclaration'],
|
|
['0', 'index'],
|
|
['init', 'VariableDeclarator'],
|
|
]
|
|
|
|
return {
|
|
modifiedAst: _node,
|
|
id: _name,
|
|
pathToNode,
|
|
}
|
|
}
|
|
|
|
export function addStartProfileAt(
|
|
node: Program,
|
|
pathToNode: PathToNode,
|
|
at: [number, number]
|
|
): { modifiedAst: Program; pathToNode: PathToNode } {
|
|
const variableDeclaration = getNodeFromPath<VariableDeclaration>(
|
|
node,
|
|
pathToNode,
|
|
'VariableDeclaration'
|
|
).node
|
|
if (variableDeclaration.type !== 'VariableDeclaration') {
|
|
throw new Error('variableDeclaration.init.type !== PipeExpression')
|
|
}
|
|
const _node = { ...node }
|
|
const init = variableDeclaration.declarations[0].init
|
|
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
|
|
createArrayExpression([
|
|
createLiteral(roundOff(at[0])),
|
|
createLiteral(roundOff(at[1])),
|
|
]),
|
|
createPipeSubstitution(),
|
|
])
|
|
if (init.type === 'PipeExpression') {
|
|
init.body.splice(1, 0, startProfileAt)
|
|
} else {
|
|
variableDeclaration.declarations[0].init = createPipeExpression([
|
|
init,
|
|
startProfileAt,
|
|
])
|
|
}
|
|
return {
|
|
modifiedAst: _node,
|
|
pathToNode,
|
|
}
|
|
}
|
|
|
|
export function addSketchTo(
|
|
node: Program,
|
|
axis: 'xy' | 'xz' | 'yz',
|
|
name = ''
|
|
): { modifiedAst: Program; id: string; pathToNode: PathToNode } {
|
|
const _node = { ...node }
|
|
const _name = name || findUniqueName(node, 'part')
|
|
|
|
const startSketchOn = createCallExpressionStdLib('startSketchOn', [
|
|
createLiteral(axis.toUpperCase()),
|
|
])
|
|
const startProfileAt = createCallExpressionStdLib('startProfileAt', [
|
|
createLiteral('default'),
|
|
createPipeSubstitution(),
|
|
])
|
|
const initialLineTo = createCallExpressionStdLib('line', [
|
|
createLiteral('default'),
|
|
createPipeSubstitution(),
|
|
])
|
|
|
|
const pipeBody = [startSketchOn, startProfileAt, initialLineTo]
|
|
|
|
const variableDeclaration = createVariableDeclaration(
|
|
_name,
|
|
createPipeExpression(pipeBody)
|
|
)
|
|
|
|
_node.body = [...node.body, variableDeclaration]
|
|
let sketchIndex = _node.body.length - 1
|
|
let pathToNode: PathToNode = [
|
|
['body', ''],
|
|
[sketchIndex, 'index'],
|
|
['declarations', 'VariableDeclaration'],
|
|
['0', 'index'],
|
|
['init', 'VariableDeclarator'],
|
|
]
|
|
if (axis !== 'xy') {
|
|
pathToNode = [...pathToNode, ['body', ''], ['0', 'index']]
|
|
}
|
|
|
|
return {
|
|
modifiedAst: _node,
|
|
id: _name,
|
|
pathToNode,
|
|
}
|
|
}
|
|
|
|
export function findUniqueName(
|
|
ast: Program | string,
|
|
name: string,
|
|
pad = 3,
|
|
index = 1
|
|
): string {
|
|
let searchStr: string = typeof ast === 'string' ? ast : JSON.stringify(ast)
|
|
const indexStr = String(index).padStart(pad, '0')
|
|
|
|
const endingDigitsMatcher = /\d+$/
|
|
const nameEndsInDigits = name.match(endingDigitsMatcher)
|
|
let nameIsInString = searchStr.includes(`:"${name}"`)
|
|
|
|
if (nameEndsInDigits !== null) {
|
|
// base case: name is unique and ends in digits
|
|
if (!nameIsInString) return name
|
|
|
|
// recursive case: name is not unique and ends in digits
|
|
const newPad = nameEndsInDigits[1].length
|
|
const newIndex = parseInt(nameEndsInDigits[1]) + 1
|
|
const nameWithoutDigits = name.replace(endingDigitsMatcher, '')
|
|
|
|
return findUniqueName(searchStr, nameWithoutDigits, newPad, newIndex)
|
|
}
|
|
|
|
const newName = `${name}${indexStr}`
|
|
nameIsInString = searchStr.includes(`:"${newName}"`)
|
|
|
|
// base case: name is unique and does not end in digits
|
|
if (!nameIsInString) return newName
|
|
|
|
// recursive case: name is not unique and does not end in digits
|
|
return findUniqueName(searchStr, name, pad, index + 1)
|
|
}
|
|
|
|
export function mutateArrExp(
|
|
node: Value,
|
|
updateWith: ArrayExpression
|
|
): boolean {
|
|
if (node.type === 'ArrayExpression') {
|
|
node.elements.forEach((element, i) => {
|
|
if (isLiteralArrayOrStatic(element)) {
|
|
node.elements[i] = updateWith.elements[i]
|
|
}
|
|
})
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
export function mutateObjExpProp(
|
|
node: Value,
|
|
updateWith: Literal | ArrayExpression,
|
|
key: string
|
|
): boolean {
|
|
if (node.type === 'ObjectExpression') {
|
|
const keyIndex = node.properties.findIndex((a) => a.key.name === key)
|
|
if (keyIndex !== -1) {
|
|
if (
|
|
isLiteralArrayOrStatic(updateWith) &&
|
|
isLiteralArrayOrStatic(node.properties[keyIndex].value)
|
|
) {
|
|
node.properties[keyIndex].value = updateWith
|
|
return true
|
|
} else if (
|
|
node.properties[keyIndex].value.type === 'ArrayExpression' &&
|
|
updateWith.type === 'ArrayExpression'
|
|
) {
|
|
const arrExp = node.properties[keyIndex].value as ArrayExpression
|
|
arrExp.elements.forEach((element, i) => {
|
|
if (isLiteralArrayOrStatic(element)) {
|
|
arrExp.elements[i] = updateWith.elements[i]
|
|
}
|
|
})
|
|
}
|
|
return true
|
|
} else {
|
|
node.properties.push({
|
|
type: 'ObjectProperty',
|
|
key: createIdentifier(key),
|
|
value: updateWith,
|
|
start: 0,
|
|
end: 0,
|
|
})
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
export function extrudeSketch(
|
|
node: Program,
|
|
pathToNode: PathToNode,
|
|
shouldPipe = true,
|
|
distance = createLiteral(4) as Value
|
|
): {
|
|
modifiedAst: Program
|
|
pathToNode: PathToNode
|
|
pathToExtrudeArg: PathToNode
|
|
} {
|
|
const _node = { ...node }
|
|
const dumbyStartend = { start: 0, end: 0 }
|
|
const { node: sketchExpression } = getNodeFromPath(
|
|
_node,
|
|
pathToNode,
|
|
'SketchExpression' // TODO fix this #25
|
|
)
|
|
|
|
// determine if sketchExpression is in a pipeExpression or not
|
|
const { node: pipeExpression } = getNodeFromPath<PipeExpression>(
|
|
_node,
|
|
pathToNode,
|
|
'PipeExpression'
|
|
)
|
|
const isInPipeExpression = pipeExpression.type === 'PipeExpression'
|
|
|
|
const { node: variableDeclorator, shallowPath: pathToDecleration } =
|
|
getNodeFromPath<VariableDeclarator>(_node, pathToNode, 'VariableDeclarator')
|
|
|
|
const extrudeCall = createCallExpressionStdLib('extrude', [
|
|
distance,
|
|
shouldPipe
|
|
? createPipeSubstitution()
|
|
: {
|
|
type: 'Identifier',
|
|
...dumbyStartend,
|
|
name: variableDeclorator.id.name,
|
|
},
|
|
])
|
|
|
|
if (shouldPipe) {
|
|
const pipeChain = createPipeExpression(
|
|
isInPipeExpression
|
|
? [...pipeExpression.body, extrudeCall]
|
|
: [sketchExpression as any, extrudeCall]
|
|
)
|
|
|
|
variableDeclorator.init = pipeChain
|
|
const pathToExtrudeArg: PathToNode = [
|
|
...pathToDecleration,
|
|
['init', 'VariableDeclarator'],
|
|
['body', ''],
|
|
[pipeChain.body.length - 1, 'index'],
|
|
['arguments', 'CallExpression'],
|
|
[0, 'index'],
|
|
]
|
|
|
|
return {
|
|
modifiedAst: _node,
|
|
pathToNode,
|
|
pathToExtrudeArg,
|
|
}
|
|
}
|
|
const name = findUniqueName(node, 'part')
|
|
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
|
|
_node.body.splice(_node.body.length, 0, VariableDeclaration)
|
|
const pathToExtrudeArg: PathToNode = [
|
|
['body', ''],
|
|
[_node.body.length, 'index'],
|
|
['declarations', 'VariableDeclaration'],
|
|
[0, 'index'],
|
|
['init', 'VariableDeclarator'],
|
|
['arguments', 'CallExpression'],
|
|
[0, 'index'],
|
|
]
|
|
return {
|
|
modifiedAst: node,
|
|
pathToNode: [...pathToNode.slice(0, -1), [-1, 'index']],
|
|
pathToExtrudeArg,
|
|
}
|
|
}
|
|
|
|
export function sketchOnExtrudedFace(
|
|
node: Program,
|
|
pathToNode: PathToNode,
|
|
programMemory: ProgramMemory,
|
|
cap: 'none' | 'start' | 'end' = 'none'
|
|
): { modifiedAst: Program; pathToNode: PathToNode } {
|
|
let _node = { ...node }
|
|
const newSketchName = findUniqueName(node, 'part')
|
|
const { node: oldSketchNode } = getNodeFromPath<VariableDeclarator>(
|
|
_node,
|
|
pathToNode,
|
|
'VariableDeclarator',
|
|
true
|
|
)
|
|
const oldSketchName = oldSketchNode.id.name
|
|
const { node: expression } = getNodeFromPath<CallExpression>(
|
|
_node,
|
|
pathToNode,
|
|
'CallExpression'
|
|
)
|
|
|
|
let _tag = ''
|
|
if (cap === 'none') {
|
|
const { modifiedAst, tag } = addTagForSketchOnFace(
|
|
{
|
|
previousProgramMemory: programMemory,
|
|
pathToNode,
|
|
node: _node,
|
|
},
|
|
expression.callee.name
|
|
)
|
|
_tag = tag
|
|
_node = modifiedAst
|
|
} else {
|
|
_tag = cap.toUpperCase()
|
|
}
|
|
|
|
const newSketch = createVariableDeclaration(
|
|
newSketchName,
|
|
createCallExpressionStdLib('startSketchOn', [
|
|
createIdentifier(oldSketchName),
|
|
createLiteral(_tag),
|
|
]),
|
|
'const'
|
|
)
|
|
|
|
const expressionIndex = pathToNode[1][0] as number
|
|
_node.body.splice(expressionIndex + 1, 0, newSketch)
|
|
const newpathToNode: PathToNode = [
|
|
['body', ''],
|
|
[expressionIndex + 1, 'index'],
|
|
['declarations', 'VariableDeclaration'],
|
|
[0, 'index'],
|
|
['init', 'VariableDeclarator'],
|
|
]
|
|
|
|
return {
|
|
modifiedAst: _node,
|
|
pathToNode: newpathToNode,
|
|
}
|
|
}
|
|
|
|
export const getLastIndex = (pathToNode: PathToNode): number =>
|
|
splitPathAtLastIndex(pathToNode).index
|
|
|
|
export function splitPathAtLastIndex(pathToNode: PathToNode): {
|
|
path: PathToNode
|
|
index: number
|
|
} {
|
|
const last = pathToNode[pathToNode.length - 1]
|
|
if (last && typeof last[0] === 'number') {
|
|
return {
|
|
path: pathToNode.slice(0, -1),
|
|
index: last[0],
|
|
}
|
|
} else if (pathToNode.length === 0) {
|
|
return {
|
|
path: [],
|
|
index: -1,
|
|
}
|
|
}
|
|
return splitPathAtLastIndex(pathToNode.slice(0, -1))
|
|
}
|
|
|
|
export function splitPathAtPipeExpression(pathToNode: PathToNode): {
|
|
path: PathToNode
|
|
index: number
|
|
} {
|
|
const last = pathToNode[pathToNode.length - 1]
|
|
|
|
if (
|
|
last &&
|
|
last[1] === 'index' &&
|
|
pathToNode?.[pathToNode.length - 2]?.[1] === 'PipeExpression' &&
|
|
typeof last[0] === 'number'
|
|
) {
|
|
return {
|
|
path: pathToNode.slice(0, -1),
|
|
index: last[0],
|
|
}
|
|
} else if (pathToNode.length === 0) {
|
|
return {
|
|
path: [],
|
|
index: -1,
|
|
}
|
|
}
|
|
|
|
return splitPathAtPipeExpression(pathToNode.slice(0, -1))
|
|
}
|
|
|
|
export function createLiteral(value: string | number): Literal {
|
|
return {
|
|
type: 'Literal',
|
|
start: 0,
|
|
end: 0,
|
|
value,
|
|
raw: `${value}`,
|
|
}
|
|
}
|
|
|
|
export function createIdentifier(name: string): Identifier {
|
|
return {
|
|
type: 'Identifier',
|
|
start: 0,
|
|
end: 0,
|
|
name,
|
|
}
|
|
}
|
|
|
|
export function createPipeSubstitution(): PipeSubstitution {
|
|
return {
|
|
type: 'PipeSubstitution',
|
|
start: 0,
|
|
end: 0,
|
|
}
|
|
}
|
|
|
|
export function createCallExpressionStdLib(
|
|
name: string,
|
|
args: CallExpression['arguments']
|
|
): CallExpression {
|
|
return {
|
|
type: 'CallExpression',
|
|
start: 0,
|
|
end: 0,
|
|
callee: {
|
|
type: 'Identifier',
|
|
start: 0,
|
|
end: 0,
|
|
name,
|
|
},
|
|
optional: false,
|
|
arguments: args,
|
|
}
|
|
}
|
|
|
|
export function createCallExpression(
|
|
name: string,
|
|
args: CallExpression['arguments']
|
|
): CallExpression {
|
|
return {
|
|
type: 'CallExpression',
|
|
start: 0,
|
|
end: 0,
|
|
callee: {
|
|
type: 'Identifier',
|
|
start: 0,
|
|
end: 0,
|
|
name,
|
|
},
|
|
optional: false,
|
|
arguments: args,
|
|
}
|
|
}
|
|
|
|
export function createArrayExpression(
|
|
elements: ArrayExpression['elements']
|
|
): ArrayExpression {
|
|
return {
|
|
type: 'ArrayExpression',
|
|
start: 0,
|
|
end: 0,
|
|
elements,
|
|
}
|
|
}
|
|
|
|
export function createPipeExpression(
|
|
body: PipeExpression['body']
|
|
): PipeExpression {
|
|
return {
|
|
type: 'PipeExpression',
|
|
start: 0,
|
|
end: 0,
|
|
body,
|
|
nonCodeMeta: { nonCodeNodes: {}, start: [] },
|
|
}
|
|
}
|
|
|
|
export function createVariableDeclaration(
|
|
varName: string,
|
|
init: VariableDeclarator['init'],
|
|
kind: VariableDeclaration['kind'] = 'const'
|
|
): VariableDeclaration {
|
|
return {
|
|
type: 'VariableDeclaration',
|
|
start: 0,
|
|
end: 0,
|
|
declarations: [
|
|
{
|
|
type: 'VariableDeclarator',
|
|
start: 0,
|
|
end: 0,
|
|
id: createIdentifier(varName),
|
|
init,
|
|
},
|
|
],
|
|
kind,
|
|
}
|
|
}
|
|
|
|
export function createObjectExpression(properties: {
|
|
[key: string]: Value
|
|
}): ObjectExpression {
|
|
return {
|
|
type: 'ObjectExpression',
|
|
start: 0,
|
|
end: 0,
|
|
properties: Object.entries(properties).map(([key, value]) => ({
|
|
type: 'ObjectProperty',
|
|
start: 0,
|
|
end: 0,
|
|
key: createIdentifier(key),
|
|
value,
|
|
})),
|
|
}
|
|
}
|
|
|
|
export function createUnaryExpression(
|
|
argument: UnaryExpression['argument'],
|
|
operator: UnaryExpression['operator'] = '-'
|
|
): UnaryExpression {
|
|
return {
|
|
type: 'UnaryExpression',
|
|
start: 0,
|
|
end: 0,
|
|
operator,
|
|
argument,
|
|
}
|
|
}
|
|
|
|
export function createBinaryExpression([left, operator, right]: [
|
|
BinaryExpression['left'],
|
|
BinaryExpression['operator'],
|
|
BinaryExpression['right']
|
|
]): BinaryExpression {
|
|
return {
|
|
type: 'BinaryExpression',
|
|
start: 0,
|
|
end: 0,
|
|
operator,
|
|
left,
|
|
right,
|
|
}
|
|
}
|
|
|
|
export function createBinaryExpressionWithUnary([left, right]: [
|
|
BinaryExpression['left'],
|
|
BinaryExpression['right']
|
|
]): BinaryExpression {
|
|
if (right.type === 'UnaryExpression' && right.operator === '-')
|
|
return createBinaryExpression([left, '-', right.argument])
|
|
return createBinaryExpression([left, '+', right])
|
|
}
|
|
|
|
export function giveSketchFnCallTag(
|
|
ast: Program,
|
|
range: Selection['range'],
|
|
tag?: string
|
|
): {
|
|
modifiedAst: Program
|
|
tag: string
|
|
isTagExisting: boolean
|
|
pathToNode: PathToNode
|
|
} {
|
|
const path = getNodePathFromSourceRange(ast, range)
|
|
const { node: primaryCallExp } = getNodeFromPath<CallExpression>(
|
|
ast,
|
|
path,
|
|
'CallExpression'
|
|
)
|
|
// Tag is always 3rd expression now, using arg index feels brittle
|
|
// but we can come up with a better way to identify tag later.
|
|
const thirdArg = primaryCallExp.arguments?.[2]
|
|
const tagLiteral =
|
|
thirdArg || (createLiteral(tag || findUniqueName(ast, 'seg', 2)) as Literal)
|
|
const isTagExisting = !!thirdArg
|
|
if (!isTagExisting) {
|
|
primaryCallExp.arguments[2] = tagLiteral
|
|
}
|
|
if ('value' in tagLiteral) {
|
|
// Now TypeScript knows tagLiteral has a value property
|
|
return {
|
|
modifiedAst: ast,
|
|
tag: String(tagLiteral.value),
|
|
isTagExisting,
|
|
pathToNode: path,
|
|
}
|
|
} else {
|
|
throw new Error('Unable to assign tag without value')
|
|
}
|
|
}
|
|
|
|
export function moveValueIntoNewVariable(
|
|
ast: Program,
|
|
programMemory: ProgramMemory,
|
|
sourceRange: Selection['range'],
|
|
variableName: string
|
|
): {
|
|
modifiedAst: Program
|
|
} {
|
|
const { isSafe, value, replacer } = isNodeSafeToReplace(ast, sourceRange)
|
|
if (!isSafe || value.type === 'Identifier') return { modifiedAst: ast }
|
|
|
|
const { insertIndex } = findAllPreviousVariables(
|
|
ast,
|
|
programMemory,
|
|
sourceRange
|
|
)
|
|
let _node = JSON.parse(JSON.stringify(ast))
|
|
_node = replacer(_node, variableName).modifiedAst
|
|
_node.body.splice(
|
|
insertIndex,
|
|
0,
|
|
createVariableDeclaration(variableName, value)
|
|
)
|
|
return { modifiedAst: _node }
|
|
}
|