Files
modeling-app/src/lang/std/sketchcombos.ts
max 4585671a5d Refactor getNodePathFromSourceRange (#5139)
* Refactor getNodePathFromSourceRange

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)

* Trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-27 08:24:28 -05:00

1923 lines
55 KiB
TypeScript

import {
CreatedSketchExprResult,
CreateStdLibSketchCallExpr,
InputArg,
InputArgs,
SimplifiedArgDetails,
TransformInfo,
} from './stdTypes'
import { ToolTip, toolTips } from 'lang/langHelpers'
import { Selections } from 'lib/selections'
import { cleanErrs, err } from 'lib/trap'
import {
CallExpression,
Program,
Expr,
BinaryPart,
VariableDeclarator,
PathToNode,
ProgramMemory,
sketchFromKclValue,
Literal,
SourceRange,
LiteralValue,
} from '../wasm'
import { getNodeFromPath, getNodeFromPathCurry } from '../queryAst'
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import {
createArrayExpression,
createBinaryExpression,
createBinaryExpressionWithUnary,
createCallExpression,
createIdentifier,
createLiteral,
createObjectExpression,
createPipeSubstitution,
createUnaryExpression,
giveSketchFnCallTag,
} from '../modifyAst'
import {
createFirstArg,
getConstraintInfo,
getFirstArg,
replaceSketchLine,
} from './sketch'
import {
getSketchSegmentFromPathToNode,
getSketchSegmentFromSourceRange,
} from './sketchConstraints'
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils'
import { Node } from 'wasm-lib/kcl/bindings/Node'
export type LineInputsType =
| 'xAbsolute'
| 'yAbsolute'
| 'xRelative'
| 'yRelative'
| 'angle'
| 'length'
| 'intersectionOffset'
| 'intersectionTag'
| 'radius'
export type ConstraintType =
| 'equalLength'
| 'vertical'
| 'horizontal'
| 'equalAngle'
| 'setHorzDistance'
| 'setVertDistance'
| 'setAngle'
| 'setLength'
| 'intersect'
| 'removeConstrainingValues'
| 'xAbs'
| 'yAbs'
| 'setAngleBetween'
const REF_NUM_ERR = new Error('Referenced segment does not have a to value')
function asNum(val: LiteralValue): number | Error {
if (typeof val === 'object') return val.value
return REF_NUM_ERR
}
function forceNum(arg: Literal): number {
if (typeof arg.value === 'boolean' || typeof arg.value === 'string') {
return Number(arg.value)
} else {
return arg.value.value
}
}
function isUndef(val: any): val is undefined {
return typeof val === 'undefined'
}
function isValueZero(val?: Expr): boolean {
return (
(val?.type === 'Literal' && forceNum(val) === 0) ||
(val?.type === 'UnaryExpression' &&
val.operator === '-' &&
val.argument.type === 'Literal' &&
Number(val.argument.value) === 0)
)
}
function createCallWrapper(
tooltip: ToolTip,
val: [Expr, Expr] | Expr,
tag?: Expr,
valueUsedInTransform?: number
): CreatedSketchExprResult {
const args =
tooltip === 'circle'
? []
: [createFirstArg(tooltip, val), createPipeSubstitution()]
if (tag) {
args.push(tag)
}
const [hasErr, argsWOutErr] = cleanErrs(args)
if (hasErr) {
console.error(args)
return {
callExp: createCallExpression('', []),
valueUsedInTransform: 0,
}
}
return {
callExp: createCallExpression(tooltip, argsWOutErr),
valueUsedInTransform,
}
}
/**
* Abstracts creation of a callExpression ready for use for a sketchCombo transform
* Assume it exists within a pipe and adds the pipe substitution
* @param tool line, lineTo, angledLine, etc
* @param val The first argument to the function
* @param tag
* @param valueUsedInTransform
* @returns
*/
function createStdlibCallExpression(
tool: ToolTip,
val: Expr,
tag?: Expr,
valueUsedInTransform?: number
): CreatedSketchExprResult {
const args = [val, createPipeSubstitution()]
if (tag) {
args.push(tag)
}
return {
callExp: createCallExpression(tool, args),
valueUsedInTransform,
}
}
function intersectCallWrapper({
fnName,
angleVal,
offsetVal,
intersectTag,
tag,
valueUsedInTransform,
}: {
fnName: string
angleVal: Expr
offsetVal: Expr
intersectTag: Expr
tag?: Expr
valueUsedInTransform?: number
}): CreatedSketchExprResult {
const firstArg: any = {
angle: angleVal,
offset: offsetVal,
intersectTag,
}
const args: Expr[] = [
createObjectExpression(firstArg),
createPipeSubstitution(),
]
if (tag) {
args.push(tag)
}
return {
callExp: createCallExpression(fnName, args),
valueUsedInTransform,
}
}
type TransformMap = {
[key in ToolTip]?: {
[key in LineInputsType | 'free']?: {
[key in ConstraintType]?: TransformInfo
}
}
}
const xyLineSetLength =
(xOrY: 'xLine' | 'yLine', referenceSeg = false): CreateStdLibSketchCallExpr =>
({ referenceSegName, tag, forceValueUsedInTransform, rawArgs: args }) => {
const segRef = createSegLen(referenceSegName)
const lineVal = forceValueUsedInTransform
? forceValueUsedInTransform
: referenceSeg
? segRef
: args[0].expr
const literalARg = asNum(args[0].expr.value)
if (err(literalARg)) return literalARg
return createCallWrapper(xOrY, lineVal, tag, literalARg)
}
type AngLenNone = 'ang' | 'len' | 'none'
const basicAngledLineCreateNode =
(
referenceSeg: AngLenNone = 'none',
valToForce: AngLenNone = 'none',
varValToUse: AngLenNone = 'none'
): CreateStdLibSketchCallExpr =>
({
referenceSegName,
tag,
forceValueUsedInTransform,
inputs,
rawArgs: args,
referencedSegment: path,
}) => {
const refAng = path ? getAngle(path?.from, path?.to) : 0
const argValue = asNum(args[0].expr.value)
if (err(argValue)) return argValue
const nonForcedAng =
varValToUse === 'ang'
? inputs[0].expr
: referenceSeg === 'ang'
? getClosesAngleDirection(
argValue,
refAng,
createSegAngle(referenceSegName)
)
: args[0].expr
const nonForcedLen =
varValToUse === 'len'
? inputs[1].expr
: referenceSeg === 'len'
? createSegLen(referenceSegName)
: args[1].expr
const shouldForceAng = valToForce === 'ang' && forceValueUsedInTransform
const shouldForceLen = valToForce === 'len' && forceValueUsedInTransform
const literalArg = asNum(
valToForce === 'ang' ? args[0].expr.value : args[1].expr.value
)
if (err(literalArg)) return literalArg
return createCallWrapper(
'angledLine',
[
shouldForceAng ? forceValueUsedInTransform : nonForcedAng,
shouldForceLen ? forceValueUsedInTransform : nonForcedLen,
],
tag,
literalArg
)
}
const angledLineAngleCreateNode: CreateStdLibSketchCallExpr = ({
referenceSegName,
inputs,
tag,
}) =>
createCallWrapper(
'angledLine',
[inputs[0].expr, createSegLen(referenceSegName)],
tag
)
const getMinAndSegLenVals = (
referenceSegName: string,
varVal: Expr
): [Expr, BinaryPart] => {
const segLenVal = createSegLen(referenceSegName)
return [
createCallExpression('min', [segLenVal, varVal]),
createCallExpression('legLen', [segLenVal, varVal]),
]
}
const getMinAndSegAngVals = (
referenceSegName: string,
varVal: Expr,
fnName: 'legAngX' | 'legAngY' = 'legAngX'
): [Expr, BinaryPart] => {
const minVal = createCallExpression('min', [
createSegLen(referenceSegName),
varVal,
])
const legAngle = createCallExpression(fnName, [
createSegLen(referenceSegName),
varVal,
])
return [minVal, legAngle]
}
const getSignedLeg = (arg: Literal, legLenVal: BinaryPart) =>
forceNum(arg) < 0 ? createUnaryExpression(legLenVal) : legLenVal
const getLegAng = (ang: number, legAngleVal: BinaryPart) => {
const normalisedAngle = ((ang % 360) + 360) % 360 // between 0 and 360
const truncatedTo90 = Math.floor(normalisedAngle / 90) * 90
const binExp = createBinaryExpressionWithUnary([
createLiteral(truncatedTo90),
legAngleVal,
])
return truncatedTo90 === 0 ? legAngleVal : binExp
}
const getAngleLengthSign = (ang: number, legAngleVal: BinaryPart) => {
const normalisedAngle = ((ang % 180) + 180) % 180 // between 0 and 180
return normalisedAngle > 90 ? createUnaryExpression(legAngleVal) : legAngleVal
}
function getClosesAngleDirection(
currentAng: number,
refAngle: number,
angleVal: BinaryPart
) {
const angDiff = Math.abs(currentAng - refAngle)
const normalisedAngle = ((angDiff % 360) + 360) % 360 // between 0 and 180
return normalisedAngle > 90
? createBinaryExpressionWithUnary([angleVal, createLiteral(180)])
: angleVal
}
const setHorzVertDistanceCreateNode =
(xOrY: 'x' | 'y', index = xOrY === 'x' ? 0 : 1): CreateStdLibSketchCallExpr =>
({
referenceSegName,
tag,
forceValueUsedInTransform,
rawArgs: args,
referencedSegment,
}) => {
const refNum = referencedSegment?.to?.[index]
const literalArg = asNum(args?.[index].expr.value)
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
let finalValue: Node<Expr> = createBinaryExpressionWithUnary([
createSegEnd(referenceSegName, !index),
forceValueUsedInTransform || createLiteral(valueUsedInTransform),
])
if (isValueZero(forceValueUsedInTransform)) {
finalValue = createSegEnd(referenceSegName, !index)
}
return createCallWrapper(
'lineTo',
!index ? [finalValue, args[1].expr] : [args[0].expr, finalValue],
tag,
valueUsedInTransform
)
}
const setHorzVertDistanceForAngleLineCreateNode =
(xOrY: 'x' | 'y', index = xOrY === 'x' ? 0 : 1): CreateStdLibSketchCallExpr =>
({
referenceSegName,
tag,
forceValueUsedInTransform,
inputs,
rawArgs: args,
referencedSegment,
}) => {
const refNum = referencedSegment?.to?.[index]
const literalArg = asNum(args?.[1].expr.value)
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
const binExp = createBinaryExpressionWithUnary([
createSegEnd(referenceSegName, !index),
forceValueUsedInTransform || createLiteral(valueUsedInTransform),
])
return createCallWrapper(
xOrY === 'x' ? 'angledLineToX' : 'angledLineToY',
[inputs[0].expr, binExp],
tag,
valueUsedInTransform
)
}
const setAbsDistanceCreateNode =
(
xOrY: 'x' | 'y',
isXOrYLine = false,
index = xOrY === 'x' ? 0 : 1
): CreateStdLibSketchCallExpr =>
({ tag, forceValueUsedInTransform, rawArgs: args }) => {
const literalArg = asNum(args?.[index].expr.value)
if (err(literalArg)) return literalArg
const valueUsedInTransform = roundOff(literalArg, 2)
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
if (isXOrYLine) {
return createCallWrapper(
xOrY === 'x' ? 'xLineTo' : 'yLineTo',
val,
tag,
valueUsedInTransform
)
}
return createCallWrapper(
'lineTo',
!index ? [val, args[1].expr] : [args[0].expr, val],
tag,
valueUsedInTransform
)
}
const setAbsDistanceForAngleLineCreateNode =
(xOrY: 'x' | 'y'): CreateStdLibSketchCallExpr =>
({ tag, forceValueUsedInTransform, inputs, rawArgs: args }) => {
const literalArg = asNum(args?.[1].expr.value)
if (err(literalArg)) return literalArg
const valueUsedInTransform = roundOff(literalArg, 2)
const val = forceValueUsedInTransform || createLiteral(valueUsedInTransform)
return createCallWrapper(
xOrY === 'x' ? 'angledLineToX' : 'angledLineToY',
[inputs[0].expr, val],
tag,
valueUsedInTransform
)
}
const setHorVertDistanceForXYLines =
(xOrY: 'x' | 'y'): CreateStdLibSketchCallExpr =>
({
referenceSegName,
tag,
forceValueUsedInTransform,
rawArgs: args,
referencedSegment,
}) => {
const index = xOrY === 'x' ? 0 : 1
const refNum = referencedSegment?.to?.[index]
const literalArg = asNum(args?.[index].expr.value)
if (isUndef(refNum) || err(literalArg)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(literalArg - refNum, 2)
const makeBinExp = createBinaryExpressionWithUnary([
createSegEnd(referenceSegName, xOrY === 'x'),
forceValueUsedInTransform || createLiteral(valueUsedInTransform),
])
return createCallWrapper(
xOrY === 'x' ? 'xLineTo' : 'yLineTo',
makeBinExp,
tag,
valueUsedInTransform
)
}
const setHorzVertDistanceConstraintLineCreateNode =
(isX: boolean): CreateStdLibSketchCallExpr =>
({ referenceSegName, tag, inputs, rawArgs: args, referencedSegment }) => {
let varVal = isX ? inputs[1].expr : inputs[0].expr
varVal = isExprBinaryPart(varVal) ? varVal : createLiteral(0)
const varValBinExp = createBinaryExpressionWithUnary([
createLastSeg(!isX),
varVal,
])
const makeBinExp = (index: 0 | 1) => {
const arg = asNum(args?.[index].expr.value)
const refNum = referencedSegment?.to?.[index]
if (err(arg) || isUndef(refNum)) return REF_NUM_ERR
return createBinaryExpressionWithUnary([
createSegEnd(referenceSegName, isX),
createLiteral(roundOff(arg - refNum, 2)),
])
}
const binExpr = isX ? makeBinExp(0) : makeBinExp(1)
if (err(binExpr)) return new Error('Invalid value for distance')
return createCallWrapper(
'lineTo',
isX ? [binExpr, varValBinExp] : [varValBinExp, binExpr],
tag
)
}
const setAngledIntersectLineForLines: CreateStdLibSketchCallExpr = ({
referenceSegName,
tag,
forceValueUsedInTransform,
rawArgs: args,
}) => {
const val = asNum(args[1].expr.value),
angle = asNum(args[0].expr.value)
if (err(val) || err(angle)) return REF_NUM_ERR
const valueUsedInTransform = roundOff(val, 2)
const varNamMap: { [key: number]: string } = {
0: 'ZERO',
90: 'QUARTER_TURN',
180: 'HALF_TURN',
270: 'THREE_QUARTER_TURN',
}
const angleVal = [0, 90, 180, 270].includes(angle)
? createIdentifier(varNamMap[angle])
: createLiteral(angle)
return intersectCallWrapper({
fnName: 'angledLineThatIntersects',
angleVal,
offsetVal: forceValueUsedInTransform || createLiteral(valueUsedInTransform),
intersectTag: createIdentifier(referenceSegName),
tag,
valueUsedInTransform,
})
}
const setAngledIntersectForAngledLines: CreateStdLibSketchCallExpr = ({
referenceSegName,
tag,
forceValueUsedInTransform,
inputs,
rawArgs: args,
}) => {
const val = asNum(args[1].expr.value)
if (err(val)) return val
const valueUsedInTransform = roundOff(val, 2)
return intersectCallWrapper({
fnName: 'angledLineThatIntersects',
angleVal: inputs[0].expr,
offsetVal: forceValueUsedInTransform || createLiteral(valueUsedInTransform),
intersectTag: createIdentifier(referenceSegName),
tag,
valueUsedInTransform,
})
}
const setAngleBetweenCreateNode =
(tranformToType: 'none' | 'xAbs' | 'yAbs'): CreateStdLibSketchCallExpr =>
({
referenceSegName,
tag,
forceValueUsedInTransform,
inputs,
rawArgs: args,
referencedSegment,
}) => {
const refAngle = referencedSegment
? getAngle(referencedSegment?.from, referencedSegment?.to)
: 0
const val = asNum(args[0].expr.value)
if (err(val)) return val
let valueUsedInTransform = roundOff(normaliseAngle(val - refAngle))
let firstHalfValue = createSegAngle(referenceSegName)
if (Math.abs(valueUsedInTransform) > 90) {
firstHalfValue = createBinaryExpression([
firstHalfValue,
'+',
createIdentifier('HALF_TURN'),
])
valueUsedInTransform = normaliseAngle(valueUsedInTransform - 180)
}
const binExp = createBinaryExpressionWithUnary([
firstHalfValue,
forceValueUsedInTransform || createLiteral(valueUsedInTransform),
])
return createCallWrapper(
tranformToType === 'none'
? 'angledLine'
: tranformToType === 'xAbs'
? 'angledLineToX'
: 'angledLineToY',
tranformToType === 'none'
? [binExp, args[1].expr]
: tranformToType === 'xAbs'
? [binExp, inputs[0].expr]
: [binExp, inputs[1].expr],
tag,
valueUsedInTransform
)
}
const transformMap: TransformMap = {
line: {
xRelative: {
equalLength: {
tooltip: 'line',
createNode: ({ referenceSegName, inputs, tag, rawArgs: args }) => {
const [minVal, legLenVal] = getMinAndSegLenVals(
referenceSegName,
inputs[0].expr
)
return createCallWrapper(
'line',
[minVal, getSignedLeg(args[1].expr, legLenVal)],
tag
)
},
},
horizontal: {
tooltip: 'xLine',
createNode: ({ inputs, tag }) =>
createCallWrapper('xLine', inputs[0].expr, tag),
},
setVertDistance: {
tooltip: 'lineTo',
createNode: setHorzVertDistanceConstraintLineCreateNode(false),
},
},
yRelative: {
equalLength: {
tooltip: 'line',
createNode: ({ referenceSegName, inputs, tag, rawArgs: args }) => {
const [minVal, legLenVal] = getMinAndSegLenVals(
referenceSegName,
inputs[1].expr
)
return createCallWrapper(
'line',
[getSignedLeg(args[0].expr, legLenVal), minVal],
tag
)
},
},
vertical: {
tooltip: 'yLine',
createNode: ({ inputs, tag }) =>
createCallWrapper('yLine', inputs[1].expr, tag),
},
setHorzDistance: {
tooltip: 'lineTo',
createNode: setHorzVertDistanceConstraintLineCreateNode(true),
},
},
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('len'),
},
horizontal: {
tooltip: 'xLine',
createNode: ({ tag, rawArgs: args }) =>
createCallWrapper('xLine', args[0].expr, tag),
},
vertical: {
tooltip: 'yLine',
createNode: ({ tag, rawArgs: args }) =>
createCallWrapper(
'yLine',
getInputOfType(args, 'yRelative').expr,
tag
),
},
setHorzDistance: {
tooltip: 'lineTo',
createNode: setHorzVertDistanceCreateNode('x'),
},
xAbs: {
tooltip: 'lineTo',
createNode: setAbsDistanceCreateNode('x'),
},
setVertDistance: {
tooltip: 'lineTo',
createNode: setHorzVertDistanceCreateNode('y'),
},
yAbs: {
tooltip: 'lineTo',
createNode: setAbsDistanceCreateNode('y'),
},
setAngle: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('none', 'ang'),
},
setLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('none', 'len'),
},
equalAngle: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('ang'),
},
intersect: {
tooltip: 'angledLineThatIntersects',
createNode: setAngledIntersectLineForLines,
},
setAngleBetween: {
tooltip: 'angledLine',
createNode: setAngleBetweenCreateNode('none'),
},
},
},
lineTo: {
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('len'),
},
horizontal: {
tooltip: 'xLineTo',
createNode: ({ tag, rawArgs: args }) =>
createCallWrapper('xLineTo', args[0].expr, tag),
},
vertical: {
tooltip: 'yLineTo',
createNode: ({ tag, rawArgs: args }) =>
createCallWrapper(
'yLineTo',
getInputOfType(args, 'yAbsolute').expr,
tag
),
},
xAbs: {
tooltip: 'lineTo',
createNode: setAbsDistanceCreateNode('x'),
},
yAbs: {
tooltip: 'lineTo',
createNode: setAbsDistanceCreateNode('y'),
},
},
xAbsolute: {
equalLength: {
tooltip: 'angledLineToX',
createNode: ({ referenceSegName, inputs, tag, rawArgs: args }) => {
const angleToMatchLengthXCall = createCallExpression(
'angleToMatchLengthX',
[
createIdentifier(referenceSegName),
inputs[0].expr,
createPipeSubstitution(),
]
)
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToX',
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[0].expr],
tag
)
},
},
horizontal: {
tooltip: 'xLineTo',
createNode: ({ inputs, tag }) =>
createCallWrapper('xLineTo', inputs[0].expr, tag),
},
setAngleBetween: {
tooltip: 'angledLineToX',
createNode: setAngleBetweenCreateNode('xAbs'),
},
},
yAbsolute: {
equalLength: {
tooltip: 'angledLineToY',
createNode: ({ referenceSegName, inputs, tag, rawArgs: args }) => {
const angleToMatchLengthYCall = createCallExpression(
'angleToMatchLengthY',
[
createIdentifier(referenceSegName),
inputs[1].expr,
createPipeSubstitution(),
]
)
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToY',
[getAngleLengthSign(val, angleToMatchLengthYCall), inputs[1].expr],
tag
)
},
},
vertical: {
tooltip: 'yLineTo',
createNode: ({ inputs, tag }) =>
createCallWrapper('yLineTo', inputs[1].expr, tag),
},
setAngle: {
tooltip: 'angledLineToY',
createNode: ({
inputs,
tag,
forceValueUsedInTransform,
rawArgs: args,
}) => {
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToY',
[forceValueUsedInTransform || args[0].expr, inputs[1].expr],
tag,
val
)
},
},
setAngleBetween: {
tooltip: 'angledLineToY',
createNode: setAngleBetweenCreateNode('yAbs'),
},
},
},
angledLine: {
angle: {
equalLength: {
tooltip: 'angledLine',
createNode: ({ referenceSegName, inputs, tag }) =>
createCallWrapper(
'angledLine',
[inputs[0].expr, createSegLen(referenceSegName)],
tag
),
},
setLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('none', 'len', 'ang'),
},
setVertDistance: {
tooltip: 'angledLineToY',
createNode: setHorzVertDistanceForAngleLineCreateNode('y'),
},
yAbs: {
tooltip: 'angledLineToY',
createNode: setAbsDistanceForAngleLineCreateNode('y'),
},
setHorzDistance: {
tooltip: 'angledLineToX',
createNode: setHorzVertDistanceForAngleLineCreateNode('x'),
},
xAbs: {
tooltip: 'angledLineToX',
createNode: setAbsDistanceForAngleLineCreateNode('x'),
},
intersect: {
tooltip: 'angledLineThatIntersects',
createNode: setAngledIntersectForAngledLines,
},
},
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('len'),
},
setLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('none', 'len'),
},
vertical: {
tooltip: 'yLine',
createNode: ({ tag, rawArgs: args }) =>
createCallWrapper(
'yLine',
getInputOfType(args, 'yRelative').expr,
tag
),
},
horizontal: {
tooltip: 'xLine',
createNode: ({ tag, rawArgs: args }) =>
createCallWrapper('xLine', args[0].expr, tag),
},
},
length: {
vertical: {
tooltip: 'yLine',
createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr
if (forceNum(args[0].expr) >= 0)
return createCallWrapper('yLine', expr, tag)
if (isExprBinaryPart(expr))
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
// TODO maybe should return error here instead
return createCallWrapper('yLine', expr, tag)
},
},
horizontal: {
tooltip: 'xLine',
createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr
if (forceNum(args[0].expr) >= 0)
return createCallWrapper('xLine', expr, tag)
if (isExprBinaryPart(expr))
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
// TODO maybe should return error here instead
return createCallWrapper('xLine', expr, tag)
},
},
setAngle: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('len', 'ang', 'len'),
},
equalAngle: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('ang', 'len', 'len'),
},
},
},
angledLineOfXLength: {
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('len'),
},
horizontal: {
tooltip: 'xLine',
createNode: ({ tag, rawArgs: args }) =>
createCallWrapper('xLine', args[0].expr, tag),
},
},
angle: {
equalLength: {
tooltip: 'angledLine',
createNode: angledLineAngleCreateNode,
},
},
xRelative: {
equalLength: {
tooltip: 'angledLineOfXLength',
createNode: ({ referenceSegName, inputs, tag, rawArgs: args }) => {
const [minVal, legAngle] = getMinAndSegAngVals(
referenceSegName,
getInputOfType(inputs, 'xRelative').expr
)
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineOfXLength',
[getLegAng(val, legAngle), minVal],
tag
)
},
},
horizontal: {
tooltip: 'xLine',
createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr
if (forceNum(args[0].expr) >= 0)
return createCallWrapper('xLine', expr, tag)
if (isExprBinaryPart(expr))
return createCallWrapper('xLine', createUnaryExpression(expr), tag)
// TODO maybe should return error here instead
return createCallWrapper('xLine', expr, tag)
},
},
},
},
angledLineOfYLength: {
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('len'),
},
vertical: {
tooltip: 'yLine',
createNode: ({ tag, rawArgs: args }) =>
createCallWrapper(
'yLine',
getInputOfType(args, 'yRelative').expr,
tag
),
},
},
angle: {
equalLength: {
tooltip: 'angledLine',
createNode: angledLineAngleCreateNode,
},
},
yRelative: {
equalLength: {
tooltip: 'angledLineOfYLength',
createNode: ({ referenceSegName, inputs, tag, rawArgs: args }) => {
const [minVal, legAngle] = getMinAndSegAngVals(
referenceSegName,
inputs[1].expr,
'legAngY'
)
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineOfXLength',
[getLegAng(val, legAngle), minVal],
tag
)
},
},
vertical: {
tooltip: 'yLine',
createNode: ({ inputs, tag, rawArgs: args }) => {
const expr = inputs[1].expr
if (forceNum(args[0].expr) >= 0)
return createCallWrapper('yLine', expr, tag)
if (isExprBinaryPart(expr))
return createCallWrapper('yLine', createUnaryExpression(expr), tag)
// TODO maybe should return error here instead
return createCallWrapper('yLine', expr, tag)
},
},
},
},
angledLineToX: {
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('len'),
},
horizontal: {
tooltip: 'xLineTo',
createNode: ({ tag, rawArgs: args }) =>
createCallWrapper('xLineTo', args[0].expr, tag),
},
},
angle: {
equalLength: {
tooltip: 'angledLine',
createNode: angledLineAngleCreateNode,
},
},
xAbsolute: {
equalLength: {
tooltip: 'angledLineToX',
createNode: ({ referenceSegName, inputs, tag, rawArgs: args }) => {
const angleToMatchLengthXCall = createCallExpression(
'angleToMatchLengthX',
[
createIdentifier(referenceSegName),
inputs[1].expr,
createPipeSubstitution(),
]
)
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToX',
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr],
tag
)
},
},
horizontal: {
tooltip: 'xLineTo',
createNode: ({ inputs, tag }) =>
createCallWrapper('xLineTo', inputs[1].expr, tag),
},
},
},
angledLineToY: {
free: {
equalLength: {
tooltip: 'angledLine',
createNode: basicAngledLineCreateNode('len'),
},
vertical: {
tooltip: 'yLineTo',
createNode: ({ tag, rawArgs: args }) =>
createCallWrapper(
'yLineTo',
getInputOfType(args, 'yAbsolute').expr,
tag
),
},
},
angle: {
equalLength: {
tooltip: 'angledLine',
createNode: angledLineAngleCreateNode,
},
},
yAbsolute: {
equalLength: {
tooltip: 'angledLineToY',
createNode: ({ referenceSegName, inputs, tag, rawArgs: args }) => {
const angleToMatchLengthXCall = createCallExpression(
'angleToMatchLengthY',
[
createIdentifier(referenceSegName),
inputs[1].expr,
createPipeSubstitution(),
]
)
const val = asNum(args[0].expr.value)
if (err(val)) return val
return createCallWrapper(
'angledLineToY',
[getAngleLengthSign(val, angleToMatchLengthXCall), inputs[1].expr],
tag
)
},
},
vertical: {
tooltip: 'yLineTo',
createNode: ({ inputs, tag }) =>
createCallWrapper('yLineTo', inputs[1].expr, tag),
},
},
},
xLine: {
free: {
equalLength: {
tooltip: 'xLine',
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
const argVal = asNum(args[0].expr.value)
if (err(argVal)) return argVal
const segLen = createSegLen(referenceSegName)
if (argVal > 0) return createCallWrapper('xLine', segLen, tag, argVal)
if (isExprBinaryPart(segLen))
return createCallWrapper(
'xLine',
createUnaryExpression(segLen),
tag,
argVal
)
// should probably return error here instead
return createCallWrapper('xLine', segLen, tag, argVal)
},
},
setHorzDistance: {
tooltip: 'xLineTo',
createNode: setHorVertDistanceForXYLines('x'),
},
setLength: {
tooltip: 'xLine',
createNode: xyLineSetLength('xLine'),
},
intersect: {
tooltip: 'angledLineThatIntersects',
createNode: setAngledIntersectLineForLines,
},
xAbs: {
tooltip: 'xLineTo',
createNode: setAbsDistanceCreateNode('x', true),
},
},
},
yLine: {
free: {
equalLength: {
tooltip: 'yLine',
createNode: ({ referenceSegName, tag, rawArgs: args }) => {
const argVal = asNum(args[0].expr.value)
if (err(argVal)) return argVal
let segLen = createSegLen(referenceSegName)
if (argVal < 0) segLen = createUnaryExpression(segLen)
return createCallWrapper('yLine', segLen, tag, argVal)
},
},
setLength: {
tooltip: 'yLine',
createNode: xyLineSetLength('yLine'),
},
setVertDistance: {
tooltip: 'yLineTo',
createNode: setHorVertDistanceForXYLines('y'),
},
intersect: {
tooltip: 'angledLineThatIntersects',
createNode: setAngledIntersectLineForLines,
},
yAbs: {
tooltip: 'yLineTo',
createNode: setAbsDistanceCreateNode('y', true),
},
},
},
xLineTo: {
free: {
equalLength: {
tooltip: 'xLine',
createNode: ({ referenceSegName, tag }) =>
createCallWrapper('xLine', createSegLen(referenceSegName), tag),
},
setLength: {
tooltip: 'xLine',
createNode: xyLineSetLength('xLine'),
},
},
},
yLineTo: {
free: {
equalLength: {
tooltip: 'yLine',
createNode: ({ referenceSegName, tag }) =>
createCallWrapper('yLine', createSegLen(referenceSegName), tag),
},
setLength: {
tooltip: 'yLine',
createNode: xyLineSetLength('yLine'),
},
},
},
}
export function getRemoveConstraintsTransform(
sketchFnExp: CallExpression,
constraintType: ConstraintType
): TransformInfo | false {
let name = sketchFnExp.callee.name as ToolTip
if (!toolTips.includes(name)) {
return false
}
const xyLineMap: {
[key in ToolTip]?: ToolTip
} = {
xLine: 'line',
yLine: 'line',
xLineTo: 'lineTo',
yLineTo: 'lineTo',
}
const _name = xyLineMap[name]
if (_name) {
name = _name
}
const transformInfo: TransformInfo = {
tooltip: 'line',
// tooltip: name,
createNode: ({ tag, referenceSegName, rawArgs: args }) => {
return createCallWrapper('line', [args[0].expr, args[1].expr], tag)
// The following commented changes values to hardcode, but keeps the line type the same, maybe that's useful?
// if (name === 'angledLineThatIntersects') {
// return intersectCallWrapper({
// fnName: name,
// angleVal: args[0].expr,
// offsetVal: args[1].expr,
// intersectTag: createIdentifier(referenceSegName),
// tag,
// })
// }
// return createCallWrapper(name, args, tag)
},
}
// check if the function is locked down and so can't be transformed
const firstArg = getFirstArg(sketchFnExp)
if (err(firstArg)) {
console.error(firstArg)
return false
}
if (isNotLiteralArrayOrStatic(firstArg.val)) {
return transformInfo
}
// check if the function has no constraints
const isTwoValFree =
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isTwoValFree) {
return false
}
const isOneValFree =
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isOneValFree) {
return transformInfo
}
// check what constraints the function has
const lineInputType = getConstraintType(firstArg.val, name)
if (lineInputType) {
return transformInfo
}
return false
}
export function removeSingleConstraint({
pathToCallExp,
inputDetails,
ast,
}: {
pathToCallExp: PathToNode
inputDetails: SimplifiedArgDetails
ast: Program
}): TransformInfo | false {
const callExp = getNodeFromPath<CallExpression>(
ast,
pathToCallExp,
'CallExpression'
)
if (err(callExp)) {
console.error(callExp)
return false
}
if (callExp.node.type !== 'CallExpression') {
console.error(new Error('Invalid node type'))
return false
}
const transform: TransformInfo = {
tooltip: callExp.node.callee.name as any,
createNode: ({ tag, inputs, rawArgs }) => {
// inputs is the current values for each of the inputs
// rawValues is the raw 'literal' values equivalent to the inputs
// inputDetails is the one variable we're removing the constraint from
// So we should update the call expression to use the inputs, except for
// the inputDetails, input where we should use the rawValue(s)
if (inputDetails.type === 'arrayItem') {
const values = inputs.map((arg) => {
if (
!(
(arg.type === 'arrayItem' || arg.type === 'arrayOrObjItem') &&
arg.index === inputDetails.index
)
)
return arg.expr
const literal = rawArgs.find(
(rawValue) =>
(rawValue.type === 'arrayItem' ||
rawValue.type === 'arrayOrObjItem') &&
rawValue.index === inputDetails.index
)?.expr
return (arg.index === inputDetails.index && literal) || arg.expr
})
return createStdlibCallExpression(
callExp.node.callee.name as any,
createArrayExpression(values),
tag
)
}
if (
inputDetails.type === 'arrayInObject' ||
inputDetails.type === 'objectProperty'
) {
const arrayDetailsNameBetterLater: {
[key: string]: Parameters<typeof createArrayExpression>[0]
} = {}
const otherThing: Parameters<typeof createObjectExpression>[0] = {}
inputs.forEach((arg) => {
if (
arg.type !== 'objectProperty' &&
arg.type !== 'arrayOrObjItem' &&
arg.type !== 'arrayInObject'
)
return
const rawLiteralArrayInObject = rawArgs.find(
(rawValue) =>
rawValue.type === 'arrayInObject' &&
rawValue.key === inputDetails.key &&
rawValue.index === (arg.type === 'arrayInObject' ? arg.index : -1)
)
const rawLiteralObjProp = rawArgs.find(
(rawValue) =>
(rawValue.type === 'objectProperty' ||
rawValue.type === 'arrayOrObjItem' ||
rawValue.type === 'arrayInObject') &&
rawValue.key === inputDetails.key
)
if (
inputDetails.type === 'arrayInObject' &&
rawLiteralArrayInObject?.type === 'arrayInObject' &&
rawLiteralArrayInObject?.index === inputDetails.index &&
rawLiteralArrayInObject?.key === inputDetails.key
) {
if (!arrayDetailsNameBetterLater[arg.key])
arrayDetailsNameBetterLater[arg.key] = []
arrayDetailsNameBetterLater[inputDetails.key][inputDetails.index] =
rawLiteralArrayInObject.expr
} else if (
inputDetails.type === 'objectProperty' &&
(rawLiteralObjProp?.type === 'objectProperty' ||
rawLiteralObjProp?.type === 'arrayOrObjItem') &&
rawLiteralObjProp?.key === inputDetails.key &&
arg.key === inputDetails.key
) {
otherThing[inputDetails.key] = rawLiteralObjProp.expr
} else if (arg.type === 'arrayInObject') {
if (!arrayDetailsNameBetterLater[arg.key])
arrayDetailsNameBetterLater[arg.key] = []
arrayDetailsNameBetterLater[arg.key][arg.index] = arg.expr
} else if (arg.type === 'objectProperty') {
otherThing[arg.key] = arg.expr
}
})
const createObjParam: Parameters<typeof createObjectExpression>[0] = {}
Object.entries(arrayDetailsNameBetterLater).forEach(([key, value]) => {
createObjParam[key] = createArrayExpression(value)
})
const objExp = createObjectExpression({
...createObjParam,
...otherThing,
})
return createStdlibCallExpression(
callExp.node.callee.name as any,
objExp,
tag
)
}
return createCallWrapper(
callExp.node.callee.name as any,
rawArgs[0].expr,
tag
)
},
}
return transform
}
function getTransformMapPath(
sketchFnExp: CallExpression,
constraintType: ConstraintType
):
| {
toolTip: ToolTip
lineInputType: LineInputsType | 'free'
constraintType: ConstraintType
}
| false {
const name = sketchFnExp.callee.name as ToolTip
if (!toolTips.includes(name)) {
return false
}
// check if the function is locked down and so can't be transformed
const firstArg = getFirstArg(sketchFnExp)
if (err(firstArg)) {
console.error(firstArg)
return false
}
if (isNotLiteralArrayOrStatic(firstArg.val)) {
return false
}
// check if the function has no constraints
if (isLiteralArrayOrStatic(firstArg.val)) {
const info = transformMap?.[name]?.free?.[constraintType]
if (info)
return {
toolTip: name,
lineInputType: 'free',
constraintType,
}
// if (info) return info
}
// check what constraints the function has
const lineInputType = getConstraintType(firstArg.val, name)
if (lineInputType) {
const info = transformMap?.[name]?.[lineInputType]?.[constraintType]
if (info)
return {
toolTip: name,
lineInputType,
constraintType,
}
// if (info) return info
}
return false
}
export function getTransformInfo(
sketchFnExp: CallExpression,
constraintType: ConstraintType
): TransformInfo | false {
const path = getTransformMapPath(sketchFnExp, constraintType)
if (!path) return false
const { toolTip, lineInputType, constraintType: _constraintType } = path
const info = transformMap?.[toolTip]?.[lineInputType]?.[_constraintType]
if (!info) return false
return info
}
export function getConstraintType(
val: Expr | [Expr, Expr] | [Expr, Expr, Expr],
fnName: ToolTip
): LineInputsType | null {
// this function assumes that for two val sketch functions that one arg is locked down not both
// and for one val sketch functions that the arg is NOT locked down
// these conditions should have been checked previously.
// completely locked down or not locked down at all does not depend on the fnName so we can check that first
const isArr = Array.isArray(val)
if (!isArr) {
if (fnName === 'xLine') return 'yRelative'
if (fnName === 'yLine') return 'xRelative'
if (fnName === 'xLineTo') return 'yAbsolute'
if (fnName === 'yLineTo') return 'xAbsolute'
} else {
const isFirstArgLockedDown = isNotLiteralArrayOrStatic(val[0])
if (fnName === 'line')
return isFirstArgLockedDown ? 'xRelative' : 'yRelative'
if (fnName === 'lineTo')
return isFirstArgLockedDown ? 'xAbsolute' : 'yAbsolute'
if (fnName === 'angledLine')
return isFirstArgLockedDown ? 'angle' : 'length'
if (fnName === 'angledLineOfXLength')
return isFirstArgLockedDown ? 'angle' : 'xRelative'
if (fnName === 'angledLineToX')
return isFirstArgLockedDown ? 'angle' : 'xAbsolute'
if (fnName === 'angledLineOfYLength')
return isFirstArgLockedDown ? 'angle' : 'yRelative'
if (fnName === 'angledLineToY')
return isFirstArgLockedDown ? 'angle' : 'yAbsolute'
}
return null
}
export function getTransformInfos(
selectionRanges: Selections,
ast: Program,
constraintType: ConstraintType
): TransformInfo[] {
const nodes = selectionRanges.graphSelections.map(({ codeRef }) =>
getNodeFromPath<Expr>(ast, codeRef.pathToNode, 'CallExpression')
)
try {
const theTransforms = nodes.map((nodeMeta) => {
if (err(nodeMeta)) {
console.error(nodeMeta)
return false
}
const node = nodeMeta.node
if (node?.type === 'CallExpression')
return getTransformInfo(node, constraintType)
return false
}) as TransformInfo[]
return theTransforms
} catch (error) {
console.log('error', error)
return []
}
}
export function getRemoveConstraintsTransforms(
selectionRanges: Selections,
ast: Program,
constraintType: ConstraintType
): TransformInfo[] | Error {
const nodes = selectionRanges.graphSelections.map(({ codeRef }) =>
getNodeFromPath<Expr>(ast, codeRef.pathToNode)
)
const theTransforms = nodes.map((nodeMeta) => {
// Typescript is not smart enough to know node will never be Error
// here, but we'll place a condition anyway
if (err(nodeMeta)) {
console.error(nodeMeta)
return false
}
const node = nodeMeta.node
if (node?.type === 'CallExpression')
return getRemoveConstraintsTransform(node, constraintType)
return false
}) as TransformInfo[]
return theTransforms
}
export type PathToNodeMap = { [key: number]: PathToNode }
export function transformSecondarySketchLinesTagFirst({
ast,
selectionRanges,
transformInfos,
programMemory,
forceSegName,
forceValueUsedInTransform,
}: {
ast: Node<Program>
selectionRanges: Selections
transformInfos: TransformInfo[]
programMemory: ProgramMemory
forceSegName?: string
forceValueUsedInTransform?: BinaryPart
}):
| {
modifiedAst: Node<Program>
valueUsedInTransform?: number
pathToNodeMap: PathToNodeMap
tagInfo: {
tag: string
isTagExisting: boolean
}
}
| Error {
// let node = structuredClone(ast)
// We need to sort the selections by their start position
// so that we can process them in dependency order and not write invalid KCL.
const sortedCodeBasedSelections = selectionRanges.graphSelections.toSorted(
(a, b) => a?.codeRef?.range[0] - b?.codeRef?.range[0]
)
const primarySelection = sortedCodeBasedSelections[0]?.codeRef?.range
const secondarySelections = sortedCodeBasedSelections.slice(1)
const _tag = giveSketchFnCallTag(ast, primarySelection, forceSegName)
if (err(_tag)) return _tag
const { modifiedAst, tag, isTagExisting, pathToNode } = _tag
const result = transformAstSketchLines({
ast: modifiedAst,
selectionRanges: {
...selectionRanges,
graphSelections: secondarySelections,
},
referencedSegmentRange: primarySelection,
transformInfos,
programMemory,
referenceSegName: tag,
forceValueUsedInTransform,
})
if (err(result)) return result
const updatedPathToNodeMap = incrementPathToNodeMap(result.pathToNodeMap)
updatedPathToNodeMap[0] = pathToNode
return {
...result,
pathToNodeMap: updatedPathToNodeMap,
tagInfo: {
tag,
isTagExisting,
},
}
}
function incrementPathToNodeMap(
pathToNodeMap: PathToNodeMap,
increment = 1
): PathToNodeMap {
const newMap: PathToNodeMap = {}
Object.entries(pathToNodeMap).forEach(([key, path]) => {
newMap[Number(key) + increment] = path
})
return newMap
}
export function transformAstSketchLines({
ast,
selectionRanges,
transformInfos,
programMemory,
referenceSegName,
forceValueUsedInTransform,
referencedSegmentRange,
}: {
ast: Node<Program>
selectionRanges: Selections | PathToNode[]
transformInfos: TransformInfo[]
programMemory: ProgramMemory
referenceSegName: string
referencedSegmentRange?: SourceRange
forceValueUsedInTransform?: BinaryPart
}):
| {
modifiedAst: Node<Program>
valueUsedInTransform?: number
pathToNodeMap: PathToNodeMap
}
| Error {
// deep clone since we are mutating in a loop, of which any could fail
let node = structuredClone(ast)
let _valueUsedInTransform // TODO should this be an array?
const pathToNodeMap: PathToNodeMap = {}
const processSelection = (_pathToNode: PathToNode, index: number) => {
const callBack = transformInfos?.[index].createNode
const transformTo = transformInfos?.[index].tooltip
if (!callBack || !transformTo) return new Error('no callback helper')
const getNode = getNodeFromPathCurry(node, _pathToNode)
const callExp = getNode<Node<CallExpression>>('CallExpression')
if (err(callExp)) return callExp
const varDec = getNode<VariableDeclarator>('VariableDeclarator')
if (err(varDec)) return varDec
const callBackTag = callExp.node.arguments[2]
const _referencedSegmentNameVal =
callExp.node.arguments[0]?.type === 'ObjectExpression' &&
callExp.node.arguments[0].properties?.find(
(prop) => prop.key.name === 'intersectTag'
)?.value
const _referencedSegmentName =
referenceSegName ||
(_referencedSegmentNameVal &&
_referencedSegmentNameVal.type === 'Identifier' &&
String(_referencedSegmentNameVal.name)) ||
''
const inputs: InputArgs = []
getConstraintInfo(callExp.node, '', _pathToNode).forEach((a) => {
if (
a.type === 'tangentialWithPrevious' ||
a.type === 'horizontal' ||
a.type === 'vertical'
)
return
const nodeMeta = getNodeFromPath<Expr>(ast, a.pathToNode)
if (err(nodeMeta)) return
if (a?.argPosition?.type === 'arrayItem') {
inputs.push({
type: 'arrayItem',
index: a.argPosition.index,
expr: nodeMeta.node,
argType: a.type,
})
} else if (a?.argPosition?.type === 'objectProperty') {
inputs.push({
type: 'objectProperty',
key: a.argPosition.key,
expr: nodeMeta.node,
argType: a.type,
})
} else if (a?.argPosition?.type === 'singleValue') {
inputs.push({
type: 'singleValue',
argType: a.type,
expr: nodeMeta.node,
})
} else if (a?.argPosition?.type === 'arrayInObject') {
inputs.push({
type: 'arrayInObject',
key: a.argPosition.key,
index: a.argPosition.index,
expr: nodeMeta.node,
argType: a.type,
})
}
})
const varName = varDec.node.id.name
let kclVal = programMemory.get(varName)
let sketch
if (kclVal?.type === 'Solid') {
sketch = kclVal.value.sketch
} else {
sketch = sketchFromKclValue(kclVal, varName)
if (err(sketch)) {
return
}
}
const segMeta = getSketchSegmentFromPathToNode(sketch, ast, _pathToNode)
if (err(segMeta)) return segMeta
const seg = segMeta.segment
let referencedSegment
if (referencedSegmentRange) {
const _segment = getSketchSegmentFromSourceRange(
sketch,
referencedSegmentRange
)
if (err(_segment)) return _segment
referencedSegment = _segment.segment
} else {
referencedSegment = sketch.paths.find(
(path) => path.tag?.value === _referencedSegmentName
)
}
const { to, from } = seg
const replacedSketchLine = replaceSketchLine({
node: node,
programMemory,
pathToNode: _pathToNode,
referencedSegment,
fnName: transformTo || (callExp.node.callee.name as ToolTip),
segmentInput:
seg.type === 'Circle'
? {
type: 'arc-segment',
center: seg.center,
radius: seg.radius,
from,
}
: {
type: 'straight-segment',
to,
from,
},
replaceExistingCallback: (rawArgs) =>
callBack({
referenceSegName: _referencedSegmentName,
inputs,
tag: callBackTag,
rawArgs,
forceValueUsedInTransform,
referencedSegment,
}),
})
if (err(replacedSketchLine)) return replacedSketchLine
const { modifiedAst, valueUsedInTransform, pathToNode } = replacedSketchLine
node = modifiedAst
pathToNodeMap[index] = pathToNode
if (typeof valueUsedInTransform === 'number') {
_valueUsedInTransform = valueUsedInTransform
}
}
if ('graphSelections' in selectionRanges) {
// If the processing of any of the selections failed, return the first error
const maybeProcessErrors = selectionRanges.graphSelections
.map(({ codeRef }, index) =>
processSelection(getNodePathFromSourceRange(node, codeRef.range), index)
)
.filter(err)
if (maybeProcessErrors.length) return maybeProcessErrors[0]
} else {
const maybeProcessErrors = selectionRanges.map(processSelection).filter(err)
if (maybeProcessErrors.length) return maybeProcessErrors[0]
}
return {
modifiedAst: node,
valueUsedInTransform: _valueUsedInTransform,
pathToNodeMap,
}
}
function createSegLen(referenceSegName: string): BinaryPart {
return createCallExpression('segLen', [createIdentifier(referenceSegName)])
}
function createSegAngle(referenceSegName: string): BinaryPart {
return createCallExpression('segAng', [createIdentifier(referenceSegName)])
}
function createSegEnd(
referenceSegName: string,
isX: boolean
): Node<CallExpression> {
return createCallExpression(isX ? 'segEndX' : 'segEndY', [
createIdentifier(referenceSegName),
])
}
function createLastSeg(isX: boolean): Node<CallExpression> {
return createCallExpression(isX ? 'lastSegX' : 'lastSegY', [
createPipeSubstitution(),
])
}
export type ConstraintLevel = 'free' | 'partial' | 'full'
export function getConstraintLevelFromSourceRange(
cursorRange: SourceRange,
ast: Program | Error
): Error | { range: [number, number]; level: ConstraintLevel } {
if (err(ast)) return ast
const nodeMeta = getNodeFromPath<Node<CallExpression>>(
ast,
getNodePathFromSourceRange(ast, cursorRange),
'CallExpression'
)
if (err(nodeMeta)) return nodeMeta
const { node: sketchFnExp } = nodeMeta
const name = sketchFnExp?.callee?.name as ToolTip
const range: [number, number] = [sketchFnExp.start, sketchFnExp.end]
if (!toolTips.includes(name)) return { level: 'free', range: range }
const firstArg = getFirstArg(sketchFnExp)
if (err(firstArg)) return firstArg
// check if the function is fully constrained
if (isNotLiteralArrayOrStatic(firstArg.val)) {
return { level: 'full', range: range }
}
// check if the function has no constraints
const isTwoValFree =
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
const isOneValFree =
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isTwoValFree) return { level: 'free', range: range }
if (isOneValFree) return { level: 'partial', range: range }
return { level: 'partial', range: range }
}
export function isLiteralArrayOrStatic(
val: Expr | [Expr, Expr] | [Expr, Expr, Expr] | undefined
): boolean {
if (!val) return false
if (Array.isArray(val)) {
const a = val[0]
const b = val[1]
return isLiteralArrayOrStatic(a) && isLiteralArrayOrStatic(b)
}
return (
val.type === 'Literal' ||
(val.type === 'UnaryExpression' && val.argument.type === 'Literal')
)
}
export function isNotLiteralArrayOrStatic(
val: Expr | [Expr, Expr] | [Expr, Expr, Expr]
): boolean {
if (Array.isArray(val)) {
const a = val[0]
const b = val[1]
return isNotLiteralArrayOrStatic(a) && isNotLiteralArrayOrStatic(b)
}
return (
(val.type !== 'Literal' && val.type !== 'UnaryExpression') ||
(val.type === 'UnaryExpression' && val.argument.type !== 'Literal')
)
}
export function isExprBinaryPart(expr: Expr): expr is BinaryPart {
if (
expr.type === 'Literal' ||
expr.type === 'Identifier' ||
expr.type === 'BinaryExpression' ||
expr.type === 'CallExpression' ||
expr.type === 'UnaryExpression' ||
expr.type === 'MemberExpression'
)
return true
return false
}
function getInputOfType(a: InputArgs, b: LineInputsType | 'radius'): InputArg {
return a.find(({ argType }) => argType === b) || a[0]
}