initial query and test

This commit is contained in:
Kurt Hutten Irev-Dev
2025-06-19 15:19:54 +10:00
parent 6358eed7e4
commit 4bac168a20
3 changed files with 250 additions and 0 deletions

View File

@ -18,3 +18,7 @@ export const ARG_INTERIOR_ABSOLUTE = 'interiorAbsolute'
export const ARG_AT = 'at'
export const ARG_LEG = 'leg'
export const ARG_HYPOTENUSE = 'hypotenuse'
export const ARG_P1 = 'p1'
export const ARG_P2 = 'p2'
export const ARG_P3 = 'p3'
export const ARG_DIAMETER = 'diameter'

View File

@ -10,6 +10,7 @@ import {
createPipeSubstitution,
} from '@src/lang/create'
import {
doesProfileHaveAnyConstrainedDimension,
doesSceneHaveExtrudedSketch,
doesSceneHaveSweepableSketch,
findAllPreviousVariables,
@ -35,6 +36,95 @@ beforeAll(async () => {
await initPromise
})
describe('doesProfileHaveConstrainDimension', () => {
const code = `sketch001 = startSketchOn(YZ)
profile001 = startProfile(sketch001, at = [100, 101])
|> line(end = [102, 103])
|> line(endAbsolute = [104, 105])
|> angledLine(angle = 206, length = 106)
|> angledLine(angle = -208, lengthX = 107)
|> angledLine(angle = 210, lengthY = 108)
|> angledLine(angle = 212, endAbsoluteX = 109)
|> angledLine(angle = 214, endAbsoluteY = 110)
|> arc(interiorAbsolute = [111, 112], endAbsolute = [113, 114])
|> tangentialArc(end = [115, -116])
|> tangentialArc(endAbsolute = [117, 118])
|> tangentialArc(angle = 224, radius = 119)
|> tangentialArc(angle = 226, diameter = 120)
profile002 = startProfile(sketch001, at = [-121, 122])
|> angledLine(angle = 130, length = 123, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 232, length = 124)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
profile003 = circle(sketch001, center = [-125, -126], radius = 127)
profile004 = circleThreePoint(
sketch001,
p1 = [128, 129],
p2 = [130, 131],
p3 = [132, 133],
)
profile005 = circle(sketch001, center = [-134, -135], diameter = 136)`
const profileSearchStrings = [
{
profileSearchString: 'profile001 = startProfile',
replaceCases: { start: 100, end: 120 },
},
{
profileSearchString: 'profile002 = startProfile',
replaceCases: { start: 121, end: 124 },
},
{
profileSearchString: 'profile003 = circle',
replaceCases: { start: 125, end: 127 },
},
{
profileSearchString: 'profile004 = circleThreePoint',
replaceCases: { start: 128, end: 133 },
},
{
profileSearchString: 'profile005 = circle',
replaceCases: { start: 134, end: 136 },
},
] as const
it('should return false for all profiles (no constrained dimensions detected)', () => {
const ast = assertParse(code)
profileSearchStrings.forEach((profile) => {
const profileStart = code.indexOf(profile.profileSearchString)
const profilePath = getNodePathFromSourceRange(
ast,
topLevelRange(profileStart, profileStart)
)
expect(
doesProfileHaveAnyConstrainedDimension(profilePath, ast)
).toBeFalsy()
})
})
it('should true false when adding constraints for each Profile all profiles (no constrained dimensions detected)', () => {
profileSearchStrings.forEach((profile) => {
for (
let i = profile.replaceCases.start;
i <= profile.replaceCases.end;
i++
) {
const modifiedCode = code.replaceAll(String(i), `${i} + 5`)
const ast = assertParse(modifiedCode)
const profileStart = modifiedCode.indexOf(profile.profileSearchString)
const profilePath = getNodePathFromSourceRange(
ast,
topLevelRange(profileStart, profileStart)
)
expect(
doesProfileHaveAnyConstrainedDimension(profilePath, ast)
).toBeTruthy()
}
// })
})
})
})
describe('findAllPreviousVariables', () => {
it('should find all previous variables', async () => {
const code = `baseThick = 1

View File

@ -55,6 +55,23 @@ import type { OpKclValue, Operation } from '@rust/kcl-lib/bindings/Operation'
import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
import type { KclCommandValue } from '@src/lib/commandTypes'
import type { UnaryExpression } from 'typescript'
import {
ARG_END_ABSOLUTE,
ARG_END,
ARG_LENGTH,
ARG_CIRCLE_CENTER,
ARG_RADIUS,
ARG_LENGTH_X,
ARG_LENGTH_Y,
ARG_END_ABSOLUTE_X,
ARG_END_ABSOLUTE_Y,
ARG_INTERIOR_ABSOLUTE,
ARG_AT,
ARG_P1,
ARG_P2,
ARG_P3,
ARG_DIAMETER,
} from '@src/lang/constants'
/**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -1290,3 +1307,142 @@ export const getPathNormalisedForTruncatedAst = (
Number(nodePathWithCorrectedIndexForTruncatedAst[1][0]) - minIndex
return nodePathWithCorrectedIndexForTruncatedAst
}
export function doesProfileHaveAnyConstrainedDimension(
profilePath: PathToNode,
ast: Node<Program>
): boolean {
// Get the profile node from the path
const profileNodeResult = getNodeFromPath(ast, profilePath)
if (err(profileNodeResult)) return false
const profileNode = profileNodeResult.node
// Single value dimension parameters to check (excluding angle as per requirements)
const singleValueLengthParams = new Set([
ARG_DIAMETER,
ARG_RADIUS,
ARG_LENGTH,
ARG_LENGTH_X,
ARG_LENGTH_Y,
ARG_END_ABSOLUTE_X,
ARG_END_ABSOLUTE_Y,
])
// Tuple value dimension parameters to check
const tupleValueParams = new Set([
ARG_CIRCLE_CENTER,
ARG_P1,
ARG_P2,
ARG_P3,
ARG_AT,
ARG_END,
ARG_END_ABSOLUTE,
ARG_INTERIOR_ABSOLUTE,
])
let hasConstrainedDimension = false
// Traverse the profile node to find all call expressions and their arguments
traverse(profileNode as any, {
enter: (node: any) => {
if (node.type === 'CallExpressionKw' && node.arguments) {
for (const arg of node.arguments) {
if (arg.type === 'LabeledArg' && arg.label?.name) {
const paramName = arg.label.name
// Check if this parameter is in our whitelist
if (
singleValueLengthParams.has(paramName) ||
tupleValueParams.has(paramName)
) {
// Special case: endAbsolute = [profileStartX(%), profileStartY(%)]
// This should NOT count as constrained
if (
paramName === ARG_END_ABSOLUTE &&
arg.arg.type === 'ArrayExpression' &&
arg.arg.elements.length === 2
) {
const [first, second] = arg.arg.elements
if (
first.type === 'CallExpressionKw' &&
second.type === 'CallExpressionKw' &&
first.callee.type === 'Name' &&
second.callee.type === 'Name' &&
first.callee.name.name === 'profileStartX' &&
second.callee.name.name === 'profileStartY'
) {
// This is the special case - don't count as constrained
continue
}
}
// Special case: angledLine length = -segLen(rectangleSegmentA001)
// This should NOT count as constrained
if (
node.callee?.type === 'Name' &&
node.callee.name.name === 'angledLine' &&
paramName === ARG_LENGTH
) {
let callExpr = null
// Check if it's a direct call expression or unary expression with call expression
if (arg.arg.type === 'CallExpressionKw') {
callExpr = arg.arg
} else if (
arg.arg.type === 'UnaryExpression' &&
arg.arg.argument?.type === 'CallExpressionKw'
) {
callExpr = arg.arg.argument
}
if (
callExpr &&
callExpr.callee?.type === 'Name' &&
callExpr.callee.name.name === 'segLen' &&
callExpr.unlabeled?.type === 'Name' &&
callExpr.unlabeled.name.name.startsWith('rectangleSegmentA')
) {
// This is the special case - don't count as constrained
continue
}
}
// Check if the argument value is non-literal
if (!isLiteralValue(arg.arg)) {
hasConstrainedDimension = true
break
}
}
}
}
// If we found a constrained dimension, we can break out of the outer loop too
if (hasConstrainedDimension) {
return false // This stops the traversal
}
}
},
})
return hasConstrainedDimension
}
// Helper function to check if a node represents a literal value
function isLiteralValue(node: any): boolean {
if (node.type === 'Literal') {
return true
}
if (node.type === 'ArrayExpression') {
// Array is literal if all elements are literals
return node.elements.every((element: any) => isLiteralValue(element))
}
if (node.type === 'UnaryExpression' && node.operator === '-') {
// Negative literal numbers
return isLiteralValue(node.argument)
}
// All other node types (Name, CallExpression, BinaryExpression, etc.) are non-literal
return false
}