KCL: Use keyword arguments for line, lineTo, extrude and close (#5249)

Part of #4600.

PR: https://github.com/KittyCAD/modeling-app/pull/4826

# Changes to KCL stdlib

- `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)`
- `close(sketch, tag?)` is now `close(@sketch, tag?)`
- `extrude(length, sketch)` is now `extrude(@sketch, length)`

Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this:

```
sketch = startSketchAt([0, 0])
line(sketch, end = [3, 3], tag = $hi)
```

Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as

```
sketch = startSketchAt([0, 0])
|> line(end = [3, 3], tag = $hi)
```

Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main

The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are:

```
 line\(([^=]*), %\)
 line(end = $1)

 line\((.*), %, (.*)\)
 line(end = $1, tag = $2)

 lineTo\((.*), %\)
 line(endAbsolute = $1)

 lineTo\((.*), %, (.*)\)
 line(endAbsolute = $1, tag = $2)

 extrude\((.*), %\)
 extrude(length = $1)

extrude\(([^=]*), ([a-zA-Z0-9]+)\)
extrude($2, length = $1)

 close\(%, (.*)\)
 close(tag = $1)
```

# Selected notes from commits before I squash them all

* Fix test 'yRelative to horizontal distance'

Fixes:
 - Make a lineTo helper
 - Fix pathToNode to go through the labeled arg .arg property

* Fix test by changing lookups into transformMap

Parts of the code assumed that `line` is always a relative call. But
actually now it might be absolute, if it's got an `endAbsolute` parameter.

So, change whether to look up `line` or `lineTo` and the relevant absolute
or relative line types based on that parameter.

* Stop asserting on exact source ranges

When I changed line to kwargs, all the source ranges we assert on became
slightly different. I find these assertions to be very very low value.
So I'm removing them.

* Fix more tests: getConstraintType calls weren't checking if the
'line' fn was absolute or relative.

* Fixed another queryAst test

There were 2 problems:
 - Test was looking for the old style of `line` call to choose an offset
   for pathToNode
 - Test assumed that the `tag` param was always the third one, but in
   a kwarg call, you have to look it up by label

* Fix test: traverse was not handling CallExpressionKw

* Fix another test, addTagKw

addTag helper was not aware of kw args.

* Convert close from positional to kwargs

If the close() call has 0 args, or a single unlabeled arg, the parser
interprets it as a CallExpression (positional) not a CallExpressionKw.

But then if a codemod wants to add a tag to it, it tries adding a kwarg
called 'tag', which fails because the CallExpression doesn't need
kwargs inserted into it.

The fix is: change the node from CallExpression to CallExpressionKw, and
update getNodeFromPath to take a 'replacement' arg, so we can replace
the old node with the new node in the AST.

* Fix the last test

Test was looking for `lineTo` as a substring of the input KCL program.
But there's no more lineTo function, so I changed it to look for
line() with an endAbsolute arg, which is the new equivalent.

Also changed the getConstraintInfo code to look up the lineTo if using
line with endAbsolute.

* Fix many bad regex find-replaces

I wrote a regex find-and-replace which converted `line` calls from
positional to keyword calls. But it was accidentally applied to more
places than it should be, for example, angledLine, xLine and yLine calls.

Fixes this.

* Fixes test 'Basic sketch › code pane closed at start'

Problem was, the getNodeFromPath call might not actually find a callExpressionKw,
it might find a callExpression. So the `giveSketchFnCallTag` thought
it was modifying a kwargs call, but it was actually modifying a positional
call.

This meant it tried to push a labeled argument in, rather than a normal
arg, and a lot of other problems. Fixed by doing runtime typechecking.

* Fix: Optional args given with wrong type were silently ignored

Optional args don't have to be given. But if the user gives them, they
should be the right type.

Bug: if the KCL interpreter found an optional arg, which was given, but
was the wrong type, it would ignore it and pretend the arg was never
given at all. This was confusing for users.

Fix: Now if you give an optional arg, but it's the wrong type, KCL will
emit a type error just like it would for a mandatory argument.

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: Nick Cameron <nrc@ncameron.org>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
Co-authored-by: Kevin Nadro <kevin@zoo.dev>
Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
This commit is contained in:
Adam Chalmers
2025-02-04 08:31:43 -06:00
committed by GitHub
parent f5b8298735
commit 8397405998
553 changed files with 140134 additions and 136409 deletions

View File

@ -7,6 +7,7 @@ import {
Program,
PipeExpression,
CallExpression,
CallExpressionKw,
VariableDeclarator,
Expr,
VariableDeclaration,
@ -18,6 +19,7 @@ import {
getNodeFromPath,
getNodeFromPathCurry,
getObjExprProperty,
LABELED_ARG_FIELD,
} from 'lang/queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import {
@ -25,7 +27,11 @@ import {
isNotLiteralArrayOrStatic,
} from 'lang/std/sketchcombos'
import { toolTips, ToolTip } from 'lang/langHelpers'
import { createPipeExpression, splitPathAtPipeExpression } from '../modifyAst'
import {
createPipeExpression,
mutateKwArg,
splitPathAtPipeExpression,
} from '../modifyAst'
import {
SketchLineHelper,
@ -38,13 +44,16 @@ import {
SimplifiedArgDetails,
RawArgs,
CreatedSketchExprResult,
SketchLineHelperKw,
} from 'lang/std/stdTypes'
import {
createLiteral,
createTagDeclarator,
createCallExpression,
createCallExpressionStdLibKw,
createArrayExpression,
createLabeledArg,
createPipeSubstitution,
createObjectExpression,
mutateArrExp,
@ -57,6 +66,11 @@ import { perpendicularDistance } from 'sketch-helpers'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { EdgeCutInfo } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import { findKwArg, findKwArgAny, findKwArgAnyIndex } from 'lang/util'
export const ARG_TAG = 'tag'
export const ARG_END = 'end'
export const ARG_END_ABSOLUTE = 'endAbsolute'
const STRAIGHT_SEGMENT_ERR = new Error(
'Invalid input, expected "straight-segment"'
@ -90,8 +104,6 @@ export function createFirstArg(
'angledLineOfYLength',
'angledLineToX',
'angledLineToY',
'line',
'lineTo',
].includes(sketchFn)
)
return createArrayExpression(val)
@ -145,7 +157,7 @@ const constrainInfo = (
})
const commonConstraintInfoHelper = (
callExp: CallExpression,
callExp: CallExpression | CallExpressionKw,
inputConstrainTypes: [ConstrainInfo['type'], ConstrainInfo['type']],
stdLibFnName: ConstrainInfo['stdLibFnName'],
abbreviatedInputs: [
@ -161,22 +173,56 @@ const commonConstraintInfoHelper = (
code: string,
pathToNode: PathToNode
) => {
if (callExp.type !== 'CallExpression') return []
const firstArg = callExp.arguments?.[0]
if (callExp.type !== 'CallExpression' && callExp.type !== 'CallExpressionKw')
return []
const firstArg = (() => {
switch (callExp.type) {
case 'CallExpression':
return callExp.arguments[0]
case 'CallExpressionKw':
return findKwArgAny([ARG_END, ARG_END_ABSOLUTE], callExp)
}
})()
if (firstArg === undefined) {
return []
}
const isArr = firstArg.type === 'ArrayExpression'
if (!isArr && firstArg.type !== 'ObjectExpression') return []
const pipeExpressionIndex = pathToNode.findIndex(
([_, nodeName]) => nodeName === 'PipeExpression'
)
const pathToBase = pathToNode.slice(0, pipeExpressionIndex + 2)
const pathToArrayExpression: PathToNode = [
...pathToBase,
['arguments', 'CallExpression'],
[0, 'index'],
isArr
? ['elements', 'ArrayExpression']
: ['properties', 'ObjectExpression'],
]
const argIndex = (() => {
switch (callExp.type) {
case 'CallExpression':
return 0
case 'CallExpressionKw':
return findKwArgAnyIndex([ARG_END, ARG_END_ABSOLUTE], callExp)
}
})()
if (argIndex === undefined) {
return []
}
// Construct the pathToNode.
const pathToArrayExpression: PathToNode = (() => {
const isKw = callExp.type === 'CallExpressionKw'
let path: PathToNode = [
...pathToBase,
['arguments', callExp.type],
[argIndex, 'index'],
]
if (isKw) {
path.push(['arg', LABELED_ARG_FIELD])
}
path.push(
isArr
? ['elements', 'ArrayExpression']
: ['properties', 'ObjectExpression']
)
return path
})()
const pathToFirstArg: PathToNode = isArr
? [...pathToArrayExpression, [0, 'index']]
: [
@ -294,93 +340,19 @@ function getTag(index = 2): SketchLineHelper['getTag'] {
}
}
export const lineTo: SketchLineHelper = {
add: ({ node, pathToNode, segmentInput, replaceExistingCallback }) => {
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const to = segmentInput.to
const _node = { ...node }
const nodeMeta = getNodeFromPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: pipe } = nodeMeta
const newVals: [Expr, Expr] = [
createLiteral(roundOff(to[0], 2)),
createLiteral(roundOff(to[1], 2)),
]
const newLine = createCallExpression('lineTo', [
createArrayExpression(newVals),
createPipeSubstitution(),
])
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
if (replaceExistingCallback) {
const result = replaceExistingCallback([
{
type: 'arrayItem',
index: 0,
argType: 'xAbsolute',
expr: createLiteral(roundOff(to[0], 2)),
},
{
type: 'arrayItem',
index: 1,
argType: 'yAbsolute',
expr: createLiteral(roundOff(to[1], 2)),
},
])
if (err(result)) return result
const { callExp, valueUsedInTransform } = result
pipe.body[callIndex] = callExp
return {
modifiedAst: _node,
pathToNode,
valueUsedInTransform: valueUsedInTransform,
}
} else {
pipe.body = [...pipe.body, newLine]
}
return {
modifiedAst: _node,
pathToNode,
}
},
updateArgs: ({ node, pathToNode, input }) => {
if (input.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const { to } = input
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
const toArrExp = createArrayExpression([
createLiteral(to[0]),
createLiteral(to[1]),
])
mutateArrExp(callExpression.arguments?.[0], toArrExp) ||
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
return {
modifiedAst: _node,
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper(
callExp,
['xAbsolute', 'yAbsolute'],
'lineTo',
[{ arrayInput: 0 }, { arrayInput: 1 }],
...args
),
function getTagKwArg(): SketchLineHelperKw['getTag'] {
return (callExp: CallExpressionKw) => {
if (callExp.type !== 'CallExpressionKw')
return new Error('Not a CallExpressionKw')
const arg = findKwArg(ARG_TAG, callExp)
if (!arg) return new Error('No argument')
if (arg.type !== 'TagDeclarator')
return new Error('Tag not a TagDeclarator')
return arg.value
}
}
export const line: SketchLineHelper = {
export const line: SketchLineHelperKw = {
add: ({
node,
previousProgramMemory,
@ -392,7 +364,7 @@ export const line: SketchLineHelper = {
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const { from, to } = segmentInput
const _node = { ...node }
const nodeMeta = getNodeFromPath<PipeExpression | CallExpression>(
const nodeMeta = getNodeFromPath<PipeExpression | CallExpressionKw>(
_node,
pathToNode,
'PipeExpression'
@ -415,10 +387,12 @@ export const line: SketchLineHelper = {
!replaceExistingCallback &&
pipe.type === 'PipeExpression'
) {
const callExp = createCallExpression('line', [
createArrayExpression([newXVal, newYVal]),
createPipeSubstitution(),
])
const callExp = createCallExpressionStdLibKw(
'line',
null, // Assumes this is being called in a pipeline, so the first arg is optional and if not given, will become pipeline substitution.
// TODO: ADAM: This should have a tag sometimes.
[createLabeledArg(ARG_END, createArrayExpression([newXVal, newYVal]))]
)
const pathToNodeIndex = pathToNode.findIndex(
(x) => x[1] === 'PipeExpression'
)
@ -437,7 +411,7 @@ export const line: SketchLineHelper = {
}
}
if (replaceExistingCallback && pipe.type !== 'CallExpression') {
if (replaceExistingCallback && pipe.type !== 'CallExpressionKw') {
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
const result = replaceExistingCallback([
{
@ -463,10 +437,11 @@ export const line: SketchLineHelper = {
}
}
const callExp = createCallExpression('line', [
createArrayExpression([newXVal, newYVal]),
createPipeSubstitution(),
])
const callExp = createCallExpressionStdLibKw(
'line',
null, // Assumes this is being called in a pipeline, so the first arg is optional and if not given, will become pipeline substitution.
[createLabeledArg(ARG_END, createArrayExpression([newXVal, newYVal]))]
)
if (pipe.type === 'PipeExpression') {
pipe.body = [...pipe.body, callExp]
return {
@ -474,7 +449,7 @@ export const line: SketchLineHelper = {
pathToNode: [
...pathToNode,
['body', 'PipeExpression'],
[pipe.body.length - 1, 'CallExpression'],
[pipe.body.length - 1, 'CallExpressionKw'],
],
}
} else {
@ -489,7 +464,7 @@ export const line: SketchLineHelper = {
if (input.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const { to, from } = input
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpression>(_node, pathToNode)
const nodeMeta = getNodeFromPath<CallExpressionKw>(_node, pathToNode)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
@ -498,22 +473,166 @@ export const line: SketchLineHelper = {
createLiteral(roundOff(to[1] - from[1], 2)),
])
if (callExpression.arguments?.[0].type === 'ObjectExpression') {
mutateObjExpProp(callExpression.arguments?.[0], toArrExp, 'to')
mutateKwArg(ARG_END, callExpression, toArrExp)
return {
modifiedAst: _node,
pathToNode,
}
},
getTag: getTagKwArg(),
addTag: addTagKw(),
getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper(
callExp,
['xRelative', 'yRelative'],
'line',
[{ arrayInput: 0 }, { arrayInput: 1 }],
...args
),
}
export const lineTo: SketchLineHelperKw = {
add: ({
node,
previousProgramMemory,
pathToNode,
segmentInput,
replaceExistingCallback,
spliceBetween,
}) => {
if (segmentInput.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const to = segmentInput.to
const _node = { ...node }
const nodeMeta = getNodeFromPath<PipeExpression | CallExpressionKw>(
_node,
pathToNode,
'PipeExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: pipe } = nodeMeta
const nodeMeta2 = getNodeFromPath<VariableDeclarator>(
_node,
pathToNode,
'VariableDeclarator'
)
if (err(nodeMeta2)) return nodeMeta2
const { node: varDec } = nodeMeta2
const newXVal = createLiteral(roundOff(to[0], 2))
const newYVal = createLiteral(roundOff(to[1], 2))
if (
spliceBetween &&
!replaceExistingCallback &&
pipe.type === 'PipeExpression'
) {
const callExp = createCallExpressionStdLibKw(
'line',
null, // Assumes this is being called in a pipeline, so the first arg is optional and if not given, will become pipeline substitution.
[
createLabeledArg(
ARG_END_ABSOLUTE,
createArrayExpression([newXVal, newYVal])
),
]
)
const pathToNodeIndex = pathToNode.findIndex(
(x) => x[1] === 'PipeExpression'
)
const pipeIndex = pathToNode[pathToNodeIndex + 1][0]
if (typeof pipeIndex === 'undefined' || typeof pipeIndex === 'string') {
return new Error('pipeIndex is undefined')
}
pipe.body = [
...pipe.body.slice(0, pipeIndex),
callExp,
...pipe.body.slice(pipeIndex),
]
return {
modifiedAst: _node,
pathToNode,
}
}
if (replaceExistingCallback && pipe.type !== 'CallExpressionKw') {
const { index: callIndex } = splitPathAtPipeExpression(pathToNode)
const result = replaceExistingCallback([
{
type: 'arrayItem',
index: 0,
argType: 'xRelative',
expr: newXVal,
},
{
type: 'arrayItem',
index: 1,
argType: 'yRelative',
expr: newYVal,
},
])
if (err(result)) return result
const { callExp, valueUsedInTransform } = result
pipe.body[callIndex] = callExp
return {
modifiedAst: _node,
pathToNode: [...pathToNode],
valueUsedInTransform,
}
}
const callExp = createCallExpressionStdLibKw(
'line',
null, // Assumes this is being called in a pipeline, so the first arg is optional and if not given, will become pipeline substitution.
[
createLabeledArg(
ARG_END_ABSOLUTE,
createArrayExpression([newXVal, newYVal])
),
]
)
if (pipe.type === 'PipeExpression') {
pipe.body = [...pipe.body, callExp]
return {
modifiedAst: _node,
pathToNode: [
...pathToNode,
['body', 'PipeExpression'],
[pipe.body.length - 1, 'CallExpressionKw'],
],
}
} else {
mutateArrExp(callExpression.arguments?.[0], toArrExp)
varDec.init = createPipeExpression([varDec.init, callExp])
}
return {
modifiedAst: _node,
pathToNode,
}
},
getTag: getTag(),
addTag: addTag(),
updateArgs: ({ node, pathToNode, input }) => {
if (input.type !== 'straight-segment') return STRAIGHT_SEGMENT_ERR
const { to, from } = input
const _node = { ...node }
const nodeMeta = getNodeFromPath<CallExpressionKw>(_node, pathToNode)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression } = nodeMeta
const toArrExp = createArrayExpression([
createLiteral(roundOff(to[0] - from[0], 2)),
createLiteral(roundOff(to[1] - from[1], 2)),
])
mutateKwArg(ARG_END_ABSOLUTE, callExpression, toArrExp)
return {
modifiedAst: _node,
pathToNode,
}
},
getTag: getTagKwArg(),
addTag: addTagKw(),
getConstraintInfo: (callExp, ...args) =>
commonConstraintInfoHelper(
callExp,
['xRelative', 'yRelative'],
['xAbsolute', 'yAbsolute'],
'line',
[{ arrayInput: 0 }, { arrayInput: 1 }],
...args
@ -1855,8 +1974,6 @@ export const updateStartProfileAtArgs: SketchLineHelper['updateArgs'] = ({
}
export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
line,
lineTo,
xLine,
yLine,
xLineTo,
@ -1871,6 +1988,11 @@ export const sketchLineHelperMap: { [key: string]: SketchLineHelper } = {
circle,
} as const
export const sketchLineHelperMapKw: { [key: string]: SketchLineHelperKw } = {
line,
lineTo,
} as const
export function changeSketchArguments(
node: Node<Program>,
programMemory: ProgramMemory,
@ -1890,12 +2012,16 @@ export function changeSketchArguments(
sourceRangeOrPath.type === 'sourceRange'
? getNodePathFromSourceRange(_node, sourceRangeOrPath.sourceRange)
: sourceRangeOrPath.pathToNode
const nodeMeta = getNodeFromPath<CallExpression>(_node, thePath)
const nodeMeta = getNodeFromPath<CallExpression | CallExpressionKw>(
_node,
thePath
)
if (err(nodeMeta)) return nodeMeta
const { node: callExpression, shallowPath } = nodeMeta
if (callExpression?.callee?.name in sketchLineHelperMap) {
const fnName = callExpression?.callee?.name
if (fnName in sketchLineHelperMap) {
const { updateArgs } = sketchLineHelperMap[callExpression.callee.name]
if (!updateArgs) {
return new Error('not a sketch line helper')
@ -1908,8 +2034,25 @@ export function changeSketchArguments(
input,
})
}
if (fnName in sketchLineHelperMapKw) {
const isAbsolute =
callExpression.type === 'CallExpressionKw' &&
findKwArg('endAbsolute', callExpression) !== undefined
const correctFnName = fnName === 'line' && isAbsolute ? 'lineTo' : fnName
const { updateArgs } = sketchLineHelperMapKw[correctFnName]
if (!updateArgs) {
return new Error('not a sketch line keyword helper')
}
return new Error(`not a sketch line helper: ${callExpression?.callee?.name}`)
return updateArgs({
node: _node,
previousProgramMemory: programMemory,
pathToNode: shallowPath,
input,
})
}
return new Error(`not a sketch line helper: ${fnName}`)
}
export function getConstraintInfo(
@ -1926,6 +2069,22 @@ export function getConstraintInfo(
)
}
export function getConstraintInfoKw(
callExpression: Node<CallExpressionKw>,
code: string,
pathToNode: PathToNode
): ConstrainInfo[] {
const fnName = callExpression?.callee?.name || ''
const isAbsolute = findKwArg('endAbsolute', callExpression) !== undefined
if (!(fnName in sketchLineHelperMapKw)) return []
const correctFnName = fnName === 'line' && isAbsolute ? 'lineTo' : fnName
return sketchLineHelperMapKw[correctFnName].getConstraintInfo(
callExpression,
code,
pathToNode
)
}
export function compareVec2Epsilon(
vec1: [number, number],
vec2: [number, number],
@ -1973,9 +2132,10 @@ export function addNewSketchLn({
}
| Error {
const node = structuredClone(_node)
const { add, updateArgs } = sketchLineHelperMap?.[fnName] || {}
const { add, updateArgs } =
sketchLineHelperMap?.[fnName] || sketchLineHelperMapKw?.[fnName] || {}
if (!add || !updateArgs) {
return new Error('not a sketch line helper')
return new Error(`${fnName} is not a sketch line helper`)
}
getNodeFromPath<Node<VariableDeclarator>>(
@ -1983,7 +2143,7 @@ export function addNewSketchLn({
pathToNode,
'VariableDeclarator'
)
getNodeFromPath<Node<PipeExpression | CallExpression>>(
getNodeFromPath<Node<PipeExpression | CallExpression | CallExpressionKw>>(
node,
pathToNode,
'PipeExpression'
@ -2005,7 +2165,7 @@ export function addCallExpressionsToPipe({
node: Node<Program>
programMemory: ProgramMemory
pathToNode: PathToNode
expressions: Node<CallExpression>[]
expressions: Node<CallExpression | CallExpressionKw>[]
}) {
const _node = { ...node }
const pipeExpression = getNodeFromPath<Node<PipeExpression>>(
@ -2031,9 +2191,7 @@ export function addCloseToPipe({
pathToNode: PathToNode
}) {
const _node = { ...node }
const closeExpression = createCallExpression('close', [
createPipeSubstitution(),
])
const closeExpression = createCallExpressionStdLibKw('close', null, [])
const pipeExpression = getNodeFromPath<PipeExpression>(
_node,
pathToNode,
@ -2076,7 +2234,10 @@ export function replaceSketchLine({
}
const _node = { ...node }
const { add } = sketchLineHelperMap[fnName]
const { add } =
sketchLineHelperMap[fnName] === undefined
? sketchLineHelperMapKw[fnName]
: sketchLineHelperMap[fnName]
const addRetVal = add({
node: _node,
previousProgramMemory: programMemory,
@ -2115,7 +2276,7 @@ export function replaceSketchLine({
*/
function addTagToChamfer(
tagInfo: AddTagInfo,
edgeCutMeta: EdgeCutInfo | null
edgeCutMeta: EdgeCutInfo
):
| {
modifiedAst: Node<Program>
@ -2250,11 +2411,20 @@ export function addTagForSketchOnFace(
}
| Error {
if (expressionName === 'close') {
return addTag(1)(tagInfo)
return addTagKw()(tagInfo)
}
if (expressionName === 'chamfer') {
if (edgeCutMeta === null) {
return new Error(
'Cannot add tag to chamfer because no edge cut was provided'
)
}
return addTagToChamfer(tagInfo, edgeCutMeta)
}
if (expressionName in sketchLineHelperMapKw) {
const { addTag } = sketchLineHelperMapKw[expressionName]
return addTag(tagInfo)
}
if (expressionName in sketchLineHelperMap) {
const { addTag } = sketchLineHelperMap[expressionName]
return addTag(tagInfo)
@ -2321,6 +2491,64 @@ function addTag(tagIndex = 2): addTagFn {
}
}
function addTagKw(): addTagFn {
return ({ node, pathToNode }) => {
const _node = { ...node }
// We have to allow for the possibility that the path is actually to a call expression.
// That's because if the parser reads something like `close()`, it doesn't know if this
// is a keyword or positional call.
// In fact, even something like `close(%)` could be either (because we allow 1 unlabeled
// starting param).
const callExpr = getNodeFromPath<Node<CallExpressionKw | CallExpression>>(
_node,
pathToNode,
['CallExpressionKw', 'CallExpression']
)
if (err(callExpr)) return callExpr
// If the original node is a call expression, we'll need to change it to a call with keyword args.
const primaryCallExp: CallExpressionKw =
callExpr.node.type === 'CallExpressionKw'
? callExpr.node
: {
type: 'CallExpressionKw',
callee: callExpr.node.callee,
unlabeled: callExpr.node.arguments.length
? callExpr.node.arguments[0]
: null,
arguments: [],
}
const tagArg = findKwArg(ARG_TAG, primaryCallExp)
const tagDeclarator =
tagArg || createTagDeclarator(findUniqueName(_node, 'seg', 2))
const isTagExisting = !!tagArg
if (!isTagExisting) {
const labeledArg = createLabeledArg(ARG_TAG, tagDeclarator)
primaryCallExp.arguments.push(labeledArg)
}
// If we changed the node, we must replace the old node with the new node in the AST.
const mustReplaceNode = primaryCallExp.type !== callExpr.node.type
if (mustReplaceNode) {
getNodeFromPath(_node, pathToNode, ['CallExpression'], false, {
...primaryCallExp,
start: callExpr.node.start,
end: callExpr.node.end,
})
}
if ('value' in tagDeclarator) {
// Now TypeScript knows tagDeclarator has a value property
return {
modifiedAst: _node,
tag: String(tagDeclarator.value),
}
} else {
return new Error('Unable to assign tag without value')
}
}
}
export function getYComponent(
angleDegree: number,
xComponent: number
@ -2349,12 +2577,21 @@ function getFirstArgValuesForXYFns(callExpression: CallExpression):
| Error {
// used for lineTo, line
const firstArg = callExpression.arguments[0]
if (firstArg.type === 'ArrayExpression') {
return { val: [firstArg.elements[0], firstArg.elements[1]] }
return getValuesForXYFns(firstArg)
}
function getValuesForXYFns(arg: Expr):
| {
val: [Expr, Expr]
tag?: Expr
}
| Error {
if (arg.type === 'ArrayExpression') {
return { val: [arg.elements[0], arg.elements[1]] }
}
if (firstArg.type === 'ObjectExpression') {
const to = firstArg.properties.find((p) => p.key.name === 'to')?.value
const tag = firstArg.properties.find((p) => p.key.name === 'tag')?.value
if (arg.type === 'ObjectExpression') {
const to = arg.properties.find((p) => p.key.name === 'to')?.value
const tag = arg.properties.find((p) => p.key.name === 'tag')?.value
if (to?.type === 'ArrayExpression') {
const [x, y] = to.elements
return { val: [x, y], tag }
@ -2471,6 +2708,48 @@ const getAngledLineThatIntersects = (
return new Error('expected ArrayExpression or ObjectExpression')
}
/**
Given a line call, return whether it's using absolute or relative end.
*/
export function isAbsoluteLine(lineCall: CallExpressionKw): boolean | Error {
const name = lineCall?.callee?.name
switch (name) {
case 'line':
if (findKwArg(ARG_END, lineCall) !== undefined) {
return false
}
if (findKwArg(ARG_END_ABSOLUTE, lineCall) !== undefined) {
return true
}
return new Error(
`line call has neither ${ARG_END} nor ${ARG_END_ABSOLUTE} params`
)
}
return new Error(`Unknown sketch function ${name}`)
}
/**
Get the argument corresponding to 'end' or 'endAbsolute' or wherever the line actually ends.
*/
export function getArgForEnd(lineCall: CallExpressionKw):
| {
val: Expr | [Expr, Expr] | [Expr, Expr, Expr]
tag?: Expr
}
| Error {
const name = lineCall?.callee?.name
let arg
if (name === 'line') {
arg = findKwArgAny([ARG_END, ARG_END_ABSOLUTE], lineCall)
} else {
return new Error('cannot find end of line function: ' + name)
}
if (arg === undefined) {
return new Error("no end of the line was found in fn '" + name + "'")
}
return getValuesForXYFns(arg)
}
export function getFirstArg(callExp: CallExpression):
| {
val: Expr | [Expr, Expr] | [Expr, Expr, Expr]
@ -2478,9 +2757,6 @@ export function getFirstArg(callExp: CallExpression):
}
| Error {
const name = callExp?.callee?.name
if (['lineTo', 'line'].includes(name)) {
return getFirstArgValuesForXYFns(callExp)
}
if (
[
'angledLine',