Compare commits
13 Commits
jtran/plus
...
kurt-scale
Author | SHA1 | Date | |
---|---|---|---|
b55a10208b | |||
a3331e1e33 | |||
af18dbb148 | |||
3459ac53ba | |||
cf0e10e1f4 | |||
cdc1d790bc | |||
b8eb3266c9 | |||
d2d0af1aa6 | |||
5c2ca466e5 | |||
963d2400d0 | |||
917ce728b3 | |||
218fc12051 | |||
4bac168a20 |
@ -1105,6 +1105,270 @@ part002 = startSketchOn(XZ)
|
||||
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(3)
|
||||
})
|
||||
|
||||
test.describe('Sketch scaling on first constraint', () => {
|
||||
test('Should scale entire sketch when constraining first dimension with scale checkbox enabled', async ({
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
cmdBar,
|
||||
}) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`sketch001 = startSketchOn(XZ)
|
||||
profile001 = startProfile(sketch001, at = [100, 100])
|
||||
|> line(end = [200, 0])
|
||||
|> line(end = [0, 200])
|
||||
|> line(end = [-200, 0])
|
||||
|> close()
|
||||
profile002 = startProfile(sketch001, at = [400, 400])
|
||||
|> circle(center = [0, 0], radius = 50)`
|
||||
)
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// Click on the first line segment to select it
|
||||
await page.getByText('line(end = [200, 0])').click()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
|
||||
// Wait for overlays to populate
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Click on the first line segment in the sketch
|
||||
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="1"]`)
|
||||
await page.mouse.click(line1.x, line1.y)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// Open constraints menu and click length constraint
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'constraints: open menu',
|
||||
})
|
||||
.click()
|
||||
await page.getByTestId('constraint-length').click()
|
||||
|
||||
// Verify the scale sketch checkbox is present and enabled
|
||||
const scaleCheckbox = page.getByTestId('scale-sketch-checkbox')
|
||||
await expect(scaleCheckbox).toBeVisible()
|
||||
await expect(scaleCheckbox).toBeEnabled()
|
||||
await expect(scaleCheckbox).toBeChecked() // Should be checked by default since no constraints exist
|
||||
|
||||
// Enter new value (100, which is half of original 200)
|
||||
await page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.getByRole('textbox')
|
||||
.fill('100')
|
||||
|
||||
// Ensure scale checkbox is still checked
|
||||
await expect(scaleCheckbox).toBeChecked()
|
||||
|
||||
// Submit the constraint
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'arrow right Continue',
|
||||
})
|
||||
.click()
|
||||
|
||||
// Wait for the changes to be applied
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Verify the constraint was applied with a variable
|
||||
await expect(page.locator('.cm-content')).toContainText('length001 = 100')
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
'line(end = [length001, 0])'
|
||||
)
|
||||
|
||||
// Verify scaling occurred - all dimensions should be scaled by 0.5 (100/200)
|
||||
// Original: line(end = [0, 200]) -> Scaled: line(end = [0, 100])
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
'line(end = [0, 100])'
|
||||
)
|
||||
// Original: line(end = [-200, 0]) -> Scaled: line(end = [-100, 0])
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
'line(end = [-100, 0])'
|
||||
)
|
||||
|
||||
// Original: startProfile(at = [100, 100]) -> Scaled: startProfile(at = [50, 50])
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
'startProfile(sketch001, at = [50, 50])'
|
||||
)
|
||||
// Original: startProfile(at = [400, 400]) -> Scaled: startProfile(at = [200, 200])
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
'startProfile(sketch001, at = [200, 200])'
|
||||
)
|
||||
|
||||
// Original: radius = 50 -> Scaled: radius = 25
|
||||
await expect(page.locator('.cm-content')).toContainText('radius = 25')
|
||||
})
|
||||
|
||||
test('Should not scale sketch when constraining with scale checkbox disabled', async ({
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
cmdBar,
|
||||
}) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`sketch001 = startSketchOn(XZ)
|
||||
profile001 = startProfile(sketch001, at = [100, 100])
|
||||
|> line(end = [200, 0])
|
||||
|> line(end = [0, 200])
|
||||
|> close()`
|
||||
)
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// Click on the first line segment to select it
|
||||
await page.getByText('line(end = [200, 0])').click()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
|
||||
// Wait for overlays to populate
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Click on the first line segment in the sketch
|
||||
const line1 = await u.getSegmentBodyCoords(`[data-overlay-index="1"]`)
|
||||
await page.mouse.click(line1.x, line1.y)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// Open constraints menu and click length constraint
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'constraints: open menu',
|
||||
})
|
||||
.click()
|
||||
await page.getByTestId('constraint-length').click()
|
||||
|
||||
// Verify the scale sketch checkbox is present and enabled
|
||||
const scaleCheckbox = page.getByTestId('scale-sketch-checkbox')
|
||||
await expect(scaleCheckbox).toBeVisible()
|
||||
await expect(scaleCheckbox).toBeEnabled()
|
||||
await expect(scaleCheckbox).toBeChecked()
|
||||
|
||||
// Uncheck the scale checkbox
|
||||
await scaleCheckbox.click()
|
||||
await expect(scaleCheckbox).not.toBeChecked()
|
||||
|
||||
// Enter new value (100, which is half of original 200)
|
||||
await page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.getByRole('textbox')
|
||||
.fill('100')
|
||||
|
||||
// Submit the constraint
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'arrow right Continue',
|
||||
})
|
||||
.click()
|
||||
|
||||
// Wait for the changes to be applied
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Verify the constraint was applied with a variable
|
||||
await expect(page.locator('.cm-content')).toContainText('length001 = 100')
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
'line(end = [length001, 0])'
|
||||
)
|
||||
|
||||
// Verify NO scaling occurred - other dimensions should remain unchanged
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
'line(end = [0, 200])'
|
||||
) // Should remain 200, not 100
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
'startProfile(sketch001, at = [100, 100])'
|
||||
) // Should remain [100, 100]
|
||||
})
|
||||
|
||||
test('Should disable scale checkbox when sketch already has constraints', async ({
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
cmdBar,
|
||||
}) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
'persistCode',
|
||||
`length_var = 150
|
||||
sketch001 = startSketchOn(XZ)
|
||||
profile001 = startProfile(sketch001, at = [100, 100])
|
||||
|> line(end = [length_var, 0])
|
||||
|> line(end = [0, 200])
|
||||
|> close()`
|
||||
)
|
||||
})
|
||||
|
||||
const u = await getUtils(page)
|
||||
await page.setBodyDimensions({ width: 1200, height: 500 })
|
||||
|
||||
await homePage.goToModelingScene()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// Click on the second line segment (the one without constraints)
|
||||
await page.getByText('line(end = [0, 200])').click()
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
|
||||
// Wait for overlays to populate
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Click on the second line segment in the sketch
|
||||
const line2 = await u.getSegmentBodyCoords(`[data-overlay-index="2"]`)
|
||||
await page.mouse.click(line2.x, line2.y)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
// Open constraints menu and click length constraint
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'constraints: open menu',
|
||||
})
|
||||
.click()
|
||||
await page.getByTestId('constraint-length').click()
|
||||
|
||||
// Verify the scale sketch checkbox is present but disabled/unchecked
|
||||
// because the sketch already has constraints (length_var)
|
||||
const scaleCheckbox = page.getByTestId('scale-sketch-checkbox')
|
||||
await expect(scaleCheckbox).toBeVisible()
|
||||
await expect(scaleCheckbox).not.toBeChecked() // Should be unchecked because constraints exist
|
||||
|
||||
// Enter new value
|
||||
await page
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.getByRole('textbox')
|
||||
.fill('100')
|
||||
|
||||
// Submit the constraint
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'arrow right Continue',
|
||||
})
|
||||
.click()
|
||||
|
||||
// Wait for the changes to be applied
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Verify the constraint was applied
|
||||
await expect(page.locator('.cm-content')).toContainText('length002 = 100')
|
||||
|
||||
// Verify existing constraint and values remain unchanged
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
'length_var = 150'
|
||||
)
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
'line(end = [length_var, 0])'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
test.describe('Electron constraint tests', () => {
|
||||
test(
|
||||
|
@ -1026,7 +1026,7 @@ export class SceneEntities {
|
||||
const sketch = sketchFromPathToNode({
|
||||
pathToNode: sketchEntryNodePath,
|
||||
variables: this.kclManager.variables,
|
||||
kclManager: this.kclManager,
|
||||
ast: this.kclManager.ast,
|
||||
})
|
||||
if (err(sketch)) return Promise.reject(sketch)
|
||||
if (!sketch) return Promise.reject(new Error('No sketch found'))
|
||||
@ -1171,12 +1171,64 @@ export class SceneEntities {
|
||||
codeManager: this.codeManager,
|
||||
})
|
||||
|
||||
// Check if we need to update sketchNodePaths due to CallExpression -> PipeExpression transformation
|
||||
let updatedSketchNodePaths = [...sketchNodePaths]
|
||||
let updatedSketchEntryNodePath = sketchEntryNodePath
|
||||
let needsSelectionUpdate = false
|
||||
|
||||
// Check if the sketch entry was converted from CallExpression to PipeExpression
|
||||
const _nodeAfter = getNodeFromPath<VariableDeclaration>(
|
||||
modifiedAst,
|
||||
sketchEntryNodePath,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
if (!err(_nodeAfter)) {
|
||||
const initAfter = _nodeAfter.node?.declaration?.init
|
||||
if (initAfter?.type === 'PipeExpression') {
|
||||
const INIT_PATH_STEP_INDEX = 3
|
||||
const pathNeedingUpdatingIndex = sketchNodePaths.findIndex(
|
||||
(pathToNode) =>
|
||||
pathToNode.length === INIT_PATH_STEP_INDEX + 1 &&
|
||||
pathToNode.every(
|
||||
(step, index) =>
|
||||
step?.[0] === sketchEntryNodePath?.[index]?.[0]
|
||||
)
|
||||
)
|
||||
if (pathNeedingUpdatingIndex !== -1) {
|
||||
const updatedPath: PathToNode = [
|
||||
...sketchNodePaths[pathNeedingUpdatingIndex],
|
||||
['body', 'PipeExpression'],
|
||||
[0, 'CallExpressionKw'],
|
||||
]
|
||||
updatedSketchNodePaths[pathNeedingUpdatingIndex] = updatedPath
|
||||
updatedSketchEntryNodePath = updatedPath
|
||||
needsSelectionUpdate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (intersectsProfileStart) {
|
||||
this.sceneInfra.modelingSend({ type: 'Close sketch' })
|
||||
} else {
|
||||
// Send selection update if paths were modified
|
||||
if (needsSelectionUpdate) {
|
||||
this.sceneInfra.modelingSend({
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'completeSelection',
|
||||
selection: {
|
||||
graphSelections: [],
|
||||
otherSelections: [],
|
||||
},
|
||||
updatedSketchEntryNodePath,
|
||||
updatedSketchNodePaths,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await this.setupDraftSegment(
|
||||
sketchEntryNodePath,
|
||||
sketchNodePaths,
|
||||
updatedSketchNodePaths,
|
||||
planeNodePath,
|
||||
forward,
|
||||
up,
|
||||
@ -2530,7 +2582,7 @@ export class SceneEntities {
|
||||
const sketch = sketchFromPathToNode({
|
||||
pathToNode,
|
||||
variables: this.kclManager.variables,
|
||||
kclManager: this.kclManager,
|
||||
ast: this.kclManager.ast,
|
||||
})
|
||||
if (trap(sketch)) return
|
||||
if (!sketch) {
|
||||
@ -3784,14 +3836,14 @@ function prepareTruncatedAst(
|
||||
function sketchFromPathToNode({
|
||||
pathToNode,
|
||||
variables,
|
||||
kclManager,
|
||||
ast,
|
||||
}: {
|
||||
pathToNode: PathToNode
|
||||
variables: VariableMap
|
||||
kclManager: KclManager
|
||||
ast: Node<Program>
|
||||
}): Sketch | null | Error {
|
||||
const _varDec = getNodeFromPath<VariableDeclarator>(
|
||||
kclManager.ast,
|
||||
ast,
|
||||
pathToNode,
|
||||
'VariableDeclarator'
|
||||
)
|
||||
@ -3808,6 +3860,141 @@ function sketchFromPathToNode({
|
||||
return sg
|
||||
}
|
||||
|
||||
export function scaleProfiles({
|
||||
ast,
|
||||
pathsToProfile,
|
||||
factor,
|
||||
variables,
|
||||
}: {
|
||||
ast: Node<Program>
|
||||
pathsToProfile: PathToNode[]
|
||||
factor: number
|
||||
variables: VariableMap
|
||||
}) {
|
||||
let clonedAst = structuredClone(ast)
|
||||
for (const pathToProfile of pathsToProfile) {
|
||||
const profile = sketchFromPathToNode({
|
||||
pathToNode: pathToProfile,
|
||||
variables: variables,
|
||||
ast,
|
||||
})
|
||||
if (err(profile)) return profile
|
||||
if (!profile) return Error('Profile not found')
|
||||
|
||||
// Scale the startProfile 'at' parameter
|
||||
const scaledStartAt: [number, number] = [
|
||||
profile.start.from[0] * factor,
|
||||
profile.start.from[1] * factor,
|
||||
]
|
||||
if (
|
||||
profile.paths?.[0]?.type !== 'Circle' &&
|
||||
profile.paths?.[0]?.type !== 'CircleThreePoint'
|
||||
) {
|
||||
const startProfileResult = changeSketchArguments(
|
||||
clonedAst,
|
||||
variables,
|
||||
{
|
||||
type: 'path',
|
||||
pathToNode: pathToProfile,
|
||||
},
|
||||
{
|
||||
type: 'straight-segment',
|
||||
from: [0, 0], // not used for startProfile
|
||||
to: scaledStartAt,
|
||||
}
|
||||
)
|
||||
if (trap(startProfileResult)) return startProfileResult
|
||||
clonedAst = startProfileResult.modifiedAst
|
||||
}
|
||||
|
||||
// Scale the path segments
|
||||
for (let pathIndex = 0; pathIndex < profile.paths.length; pathIndex++) {
|
||||
const path = profile.paths[pathIndex]
|
||||
let input: Parameters<typeof changeSketchArguments>[3] | undefined =
|
||||
undefined
|
||||
const pathToSegment = getNodePathFromSourceRange(
|
||||
clonedAst,
|
||||
sourceRangeFromRust(path.__geoMeta.sourceRange)
|
||||
)
|
||||
if (!pathToSegment) {
|
||||
console.log(
|
||||
'Could not find path for segment:',
|
||||
path.type,
|
||||
'sourceRange:',
|
||||
path.__geoMeta.sourceRange
|
||||
)
|
||||
}
|
||||
const scaleTuple = (tuple: [number, number]): [number, number] => [
|
||||
tuple[0] * factor,
|
||||
tuple[1] * factor,
|
||||
]
|
||||
const previous = profile.paths[pathIndex - 1]
|
||||
if (
|
||||
path.type === 'ToPoint' ||
|
||||
path.type === 'TangentialArcTo' ||
|
||||
path.type === 'TangentialArc'
|
||||
) {
|
||||
input = {
|
||||
type: 'straight-segment',
|
||||
from: scaleTuple(path.from),
|
||||
to: scaleTuple(path.to),
|
||||
previousEndTangent: previous
|
||||
? findTangentDirectionPath(previous)
|
||||
: undefined,
|
||||
}
|
||||
} else if (
|
||||
path.type === 'ArcThreePoint' ||
|
||||
path.type === 'CircleThreePoint'
|
||||
) {
|
||||
input = {
|
||||
type: 'circle-three-point-segment',
|
||||
p1: scaleTuple(path.p1),
|
||||
p2: scaleTuple(path.p2),
|
||||
p3: scaleTuple(path.p3),
|
||||
}
|
||||
} else if (path.type === 'Circle') {
|
||||
input = {
|
||||
type: 'arc-segment',
|
||||
from: scaleTuple(path.from),
|
||||
to: scaleTuple(path.to),
|
||||
center: scaleTuple(path.center),
|
||||
radius: path.radius * factor,
|
||||
ccw: path.ccw,
|
||||
}
|
||||
} else {
|
||||
// Skip close calls - they don't have dimensions to scale
|
||||
console.log('Unhandled path type:', path.type)
|
||||
continue
|
||||
}
|
||||
if (input && pathToSegment) {
|
||||
try {
|
||||
const changeSketchResult = changeSketchArguments(
|
||||
clonedAst,
|
||||
variables,
|
||||
{
|
||||
type: 'path',
|
||||
pathToNode: pathToSegment,
|
||||
},
|
||||
input
|
||||
)
|
||||
if (!err(changeSketchResult)) {
|
||||
clonedAst = changeSketchResult.modifiedAst
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error scaling segment:', path.type, 'error:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const pResult = parse(recast(clonedAst))
|
||||
if (trap(pResult) || !resultIsOk(pResult)) {
|
||||
return Error('Unexpected compilation error')
|
||||
}
|
||||
return {
|
||||
modifiedAst: pResult.program,
|
||||
}
|
||||
}
|
||||
|
||||
function colorSegment(object: any, color: number) {
|
||||
const segmentHead = getParentGroup(object, [ARROWHEAD, PROFILE_START])
|
||||
if (segmentHead) {
|
||||
@ -3837,7 +4024,7 @@ export function getSketchQuaternion(
|
||||
const sketch = sketchFromPathToNode({
|
||||
pathToNode: sketchPathToNode,
|
||||
variables: kclManager.variables,
|
||||
kclManager,
|
||||
ast: kclManager.ast,
|
||||
})
|
||||
if (err(sketch)) return sketch
|
||||
const zAxis =
|
||||
@ -3895,7 +4082,7 @@ function getSketchesInfo({
|
||||
const sketch = sketchFromPathToNode({
|
||||
pathToNode: path,
|
||||
variables,
|
||||
kclManager,
|
||||
ast: kclManager.ast,
|
||||
})
|
||||
if (err(sketch)) continue
|
||||
if (!sketch) continue
|
||||
@ -3974,3 +4161,44 @@ function findTangentDirection(segmentGroup: Group) {
|
||||
}
|
||||
return tangentDirection
|
||||
}
|
||||
|
||||
// implements the same as, but for a Path instead of segment Group
|
||||
function findTangentDirectionPath(path: Path): Coords2d | undefined {
|
||||
let tangentDirection: Coords2d | undefined
|
||||
|
||||
if (path.type === 'TangentialArcTo' || path.type === 'TangentialArc') {
|
||||
// For tangential arcs with center, calculate tangent at the end point
|
||||
const tangentAngle =
|
||||
deg2Rad(getAngle(path.center, path.to)) +
|
||||
(Math.PI / 2) * (path.ccw ? 1 : -1)
|
||||
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
|
||||
} else if (path.type === 'Arc') {
|
||||
// For regular arcs, calculate tangent at the end point
|
||||
const tangentAngle =
|
||||
deg2Rad(getAngle(path.center, path.to)) +
|
||||
(Math.PI / 2) * (path.ccw ? 1 : -1)
|
||||
tangentDirection = [Math.cos(tangentAngle), Math.sin(tangentAngle)]
|
||||
} else if (path.type === 'ArcThreePoint') {
|
||||
// For three-point arcs, we need to calculate the center first
|
||||
// This is more complex, so for now we'll skip tangent calculation
|
||||
console.warn(
|
||||
'ArcThreePoint tangent direction calculation not implemented yet'
|
||||
)
|
||||
} else if (path.type === 'ToPoint') {
|
||||
// For straight lines, the tangent is the direction from start to end
|
||||
const to = path.to as Coords2d
|
||||
const from = path.from as Coords2d
|
||||
tangentDirection = subVec(to, from)
|
||||
const normalized = normalizeVec(tangentDirection)
|
||||
if (normalized) {
|
||||
tangentDirection = normalized
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
'Unsupported path type for tangent direction calculation: ',
|
||||
path.type
|
||||
)
|
||||
}
|
||||
|
||||
return tangentDirection
|
||||
}
|
||||
|
@ -31,6 +31,8 @@ import { roundOff, roundOffWithUnits } from '@src/lib/utils'
|
||||
import { varMentions } from '@src/lib/varCompletionExtension'
|
||||
import { useSettings } from '@src/lib/singletons'
|
||||
import { commandBarActor, useCommandBarState } from '@src/lib/singletons'
|
||||
import { doesProfileHaveAnyConstrainedDimension } from '@src/lang/queryAst'
|
||||
import type { PathToNode } from '@src/lang/wasm'
|
||||
|
||||
import styles from './CommandBarKclInput.module.css'
|
||||
|
||||
@ -50,6 +52,7 @@ function CommandBarKclInput({
|
||||
stepBack: () => void
|
||||
onSubmit: (event: unknown) => void
|
||||
}) {
|
||||
// console.log('arg', arg)
|
||||
const commandBarState = useCommandBarState()
|
||||
const previouslySetValue = commandBarState.context.argumentsToSubmit[
|
||||
arg.name
|
||||
@ -109,6 +112,32 @@ function CommandBarKclInput({
|
||||
arg.createVariable === 'force' ||
|
||||
false
|
||||
)
|
||||
|
||||
// Check if this is the "Constrain with named value" command
|
||||
const isConstrainWithNamedValueCommand =
|
||||
commandBarState.context.selectedCommand?.name ===
|
||||
'Constrain with named value'
|
||||
const sketchDetails = argMachineContext?.sketchDetails
|
||||
|
||||
// Checkbox should be enabled (clickable) when it's the right command
|
||||
const shouldEnableScaleCheckbox = isConstrainWithNamedValueCommand
|
||||
|
||||
// Checkbox should be checked by default when ALL profiles have no constrained dimensions
|
||||
const shouldCheckScaleByDefault = useMemo(() => {
|
||||
if (!isConstrainWithNamedValueCommand || !sketchDetails?.sketchNodePaths) {
|
||||
return false
|
||||
}
|
||||
// Check if ALL profiles have no constrained dimensions (meaning we can safely scale)
|
||||
return sketchDetails.sketchNodePaths.every((pathToProfile: PathToNode) => {
|
||||
const hasConstrainedDimension = doesProfileHaveAnyConstrainedDimension(
|
||||
pathToProfile,
|
||||
kclManager.ast
|
||||
)
|
||||
return !hasConstrainedDimension // We want profiles with NO constrained dimensions
|
||||
})
|
||||
}, [isConstrainWithNamedValueCommand, sketchDetails, kclManager.ast])
|
||||
|
||||
const [scaleSketch, setScaleSketch] = useState(shouldCheckScaleByDefault)
|
||||
const [canSubmit, setCanSubmit] = useState(true)
|
||||
useHotkeys('mod + k, mod + /', () => commandBarActor.send({ type: 'Close' }))
|
||||
const editorRef = useRef<HTMLDivElement>(null)
|
||||
@ -225,6 +254,23 @@ function CommandBarKclInput({
|
||||
)
|
||||
}, [calcResult, createNewVariable, isNewVariableNameUnique, isExecuting])
|
||||
|
||||
// Update scale checkbox when the condition changes
|
||||
useEffect(() => {
|
||||
setScaleSketch(shouldCheckScaleByDefault)
|
||||
}, [shouldCheckScaleByDefault])
|
||||
|
||||
// Store scaleSketch value in command bar context for "Constrain with named value" command
|
||||
useEffect(() => {
|
||||
if (isConstrainWithNamedValueCommand) {
|
||||
commandBarActor.send({
|
||||
type: 'Set additional data',
|
||||
data: {
|
||||
scaleSketch,
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [scaleSketch, isConstrainWithNamedValueCommand])
|
||||
|
||||
function handleSubmit(e?: React.FormEvent<HTMLFormElement>) {
|
||||
e?.preventDefault()
|
||||
if (!canSubmit || valueNode === null) {
|
||||
@ -237,26 +283,26 @@ function CommandBarKclInput({
|
||||
return
|
||||
}
|
||||
|
||||
onSubmit(
|
||||
createNewVariable
|
||||
? ({
|
||||
valueAst: valueNode,
|
||||
valueText: value,
|
||||
valueCalculated: calcResult,
|
||||
variableName: newVariableName,
|
||||
insertIndex: newVariableInsertIndex,
|
||||
variableIdentifierAst: createLocalName(newVariableName),
|
||||
variableDeclarationAst: createVariableDeclaration(
|
||||
newVariableName,
|
||||
valueNode
|
||||
),
|
||||
} satisfies KclCommandValue)
|
||||
: ({
|
||||
valueAst: valueNode,
|
||||
valueText: value,
|
||||
valueCalculated: calcResult,
|
||||
} satisfies KclCommandValue)
|
||||
)
|
||||
const commandValue = createNewVariable
|
||||
? ({
|
||||
valueAst: valueNode,
|
||||
valueText: value,
|
||||
valueCalculated: calcResult,
|
||||
variableName: newVariableName,
|
||||
insertIndex: newVariableInsertIndex,
|
||||
variableIdentifierAst: createLocalName(newVariableName),
|
||||
variableDeclarationAst: createVariableDeclaration(
|
||||
newVariableName,
|
||||
valueNode
|
||||
),
|
||||
} satisfies KclCommandValue)
|
||||
: ({
|
||||
valueAst: valueNode,
|
||||
valueText: value,
|
||||
valueCalculated: calcResult,
|
||||
} satisfies KclCommandValue)
|
||||
|
||||
onSubmit(commandValue)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -359,6 +405,34 @@ function CommandBarKclInput({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isConstrainWithNamedValueCommand && (
|
||||
<div className="flex items-baseline gap-4 mx-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="scale-sketch-checkbox"
|
||||
data-testid="scale-sketch-checkbox"
|
||||
checked={scaleSketch}
|
||||
disabled={!shouldEnableScaleCheckbox}
|
||||
onChange={(e) => {
|
||||
setScaleSketch(e.target.checked)
|
||||
}}
|
||||
className="bg-chalkboard-10 dark:bg-chalkboard-80"
|
||||
/>
|
||||
<label
|
||||
htmlFor="scale-sketch-checkbox"
|
||||
className={`text-blue border-none bg-transparent font-sm flex gap-1 items-center pl-0 pr-1 ${
|
||||
!shouldEnableScaleCheckbox ? 'opacity-50 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
Scale sketch
|
||||
</label>
|
||||
{!shouldEnableScaleCheckbox && (
|
||||
<span className="text-sm text-chalkboard-60 dark:text-chalkboard-50">
|
||||
(disabled - sketch has constrained dimensions)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -25,12 +25,13 @@ import type { Artifact } from '@src/lang/std/artifactGraph'
|
||||
import { codeRefFromRange } from '@src/lang/std/artifactGraph'
|
||||
import { topLevelRange } from '@src/lang/util'
|
||||
import type { Identifier, Literal, LiteralValue } from '@src/lang/wasm'
|
||||
import { assertParse, recast } from '@src/lang/wasm'
|
||||
import { assertParse, recast, parse, resultIsOk } from '@src/lang/wasm'
|
||||
import { initPromise } from '@src/lang/wasmUtils'
|
||||
import { enginelessExecutor } from '@src/lib/testHelpers'
|
||||
import { err } from '@src/lib/trap'
|
||||
import { deleteFromSelection } from '@src/lang/modifyAst/deleteFromSelection'
|
||||
import { assertNotErr } from '@src/unitTestUtils'
|
||||
import { scaleProfiles } from '@src/clientSideScene/sceneEntities'
|
||||
|
||||
beforeAll(async () => {
|
||||
await initPromise
|
||||
@ -917,3 +918,223 @@ extrude001 = extrude(part001, length = 5)
|
||||
expect(result instanceof Error).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing sketch scaling', () => {
|
||||
it('can scale sketch by half simple case', async () => {
|
||||
const basicSketch = `sketch002 = startSketchOn(XZ)
|
||||
profile006 = startProfile(sketch002, at = [114.64, 124.18])
|
||||
|> line(end = [173.02, 306.77])
|
||||
|> line(end = [175.14, -282.35])
|
||||
|> tangentialArc(end = [-52.01, -194.25])`
|
||||
const ast = assertParse(basicSketch)
|
||||
const result = await enginelessExecutor(ast)
|
||||
const searchSnippet = 'startProfile(sketch002, at = [114.64, 124.18])'
|
||||
const startIndex = basicSketch.indexOf(searchSnippet)
|
||||
const range = topLevelRange(startIndex, startIndex + searchSnippet.length)
|
||||
const pathToProfile = getNodePathFromSourceRange(ast, range)
|
||||
|
||||
if (err(result)) throw result
|
||||
const scaledProfile = scaleProfiles({
|
||||
ast,
|
||||
factor: 0.5,
|
||||
variables: result.variables,
|
||||
pathsToProfile: [pathToProfile],
|
||||
})
|
||||
if (err(scaledProfile)) throw scaledProfile
|
||||
const modifiedAst = scaledProfile.modifiedAst
|
||||
const newCode = recast(modifiedAst)
|
||||
if (err(newCode)) throw newCode
|
||||
|
||||
expect(newCode).toBe(`sketch002 = startSketchOn(XZ)
|
||||
profile006 = startProfile(sketch002, at = [57.32, 62.09])
|
||||
|> line(end = [86.51, 153.39])
|
||||
|> line(end = [87.57, -141.18])
|
||||
|> tangentialArc(end = [-26, -97.12])
|
||||
`)
|
||||
})
|
||||
it('can scale sketch more complex', async () => {
|
||||
let 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)`
|
||||
let ast = assertParse(code)
|
||||
const result = await enginelessExecutor(ast)
|
||||
const searchSnippets = [
|
||||
'startProfile(sketch001, at = [100, 101])',
|
||||
'startProfile(sketch001, at = [-121, 122])',
|
||||
'circle(sketch001, center = [-125, -126], radius = 127)',
|
||||
'circleThreePoint(',
|
||||
'circle(sketch001, center = [-134, -135], diameter = 136)',
|
||||
]
|
||||
const ranges = searchSnippets.map((searchSnippet) => {
|
||||
const startIndex = code.indexOf(searchSnippet)
|
||||
return topLevelRange(startIndex, startIndex + searchSnippet.length)
|
||||
})
|
||||
const pathsToProfiles = ranges.map((range) =>
|
||||
getNodePathFromSourceRange(ast, range)
|
||||
)
|
||||
|
||||
if (err(result)) throw result
|
||||
const scaledProfile = scaleProfiles({
|
||||
ast,
|
||||
factor: 0.5,
|
||||
variables: result.variables,
|
||||
pathsToProfile: pathsToProfiles,
|
||||
})
|
||||
if (err(scaledProfile)) throw scaledProfile
|
||||
const pResult = parse(recast(scaledProfile.modifiedAst))
|
||||
if (err(pResult) || !resultIsOk(pResult)) return
|
||||
ast = pResult.program
|
||||
const newCode = recast(ast)
|
||||
if (err(newCode)) throw newCode
|
||||
|
||||
expect(newCode).toBe(`sketch001 = startSketchOn(YZ)
|
||||
profile001 = startProfile(sketch001, at = [50, 50.5])
|
||||
|> line(end = [51, 51.5])
|
||||
|> line(endAbsolute = [52, 52.5])
|
||||
|> angledLine(angle = -154, length = 53)
|
||||
|> angledLine(angle = 152, lengthX = 53.5)
|
||||
|> angledLine(angle = -150, lengthY = 54)
|
||||
|> angledLine(angle = 32, endAbsoluteX = 54.5)
|
||||
|> angledLine(angle = -146, endAbsoluteY = 55)
|
||||
|> arc(interiorAbsolute = [55.5, 56], endAbsolute = [56.5, 57])
|
||||
|> tangentialArc(end = [57.5, -58])
|
||||
|> tangentialArc(endAbsolute = [58.5, 59])
|
||||
|> tangentialArc(angle = 224deg, radius = 59.5)
|
||||
|> tangentialArc(angle = 226deg, diameter = 60)
|
||||
|
||||
profile002 = startProfile(sketch001, at = [-60.5, 61])
|
||||
|> angledLine(angle = 130, length = 61.5, tag = $rectangleSegmentA001)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA001) - 232, length = 62)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
profile003 = circle(sketch001, center = [-62.5, -63], radius = 63.5)
|
||||
profile004 = circleThreePoint(
|
||||
sketch001,
|
||||
p1 = [64, 64.5],
|
||||
p2 = [65, 65.5],
|
||||
p3 = [66, 66.5],
|
||||
)
|
||||
profile005 = circle(sketch001, center = [-67, -67.5], diameter = 68)
|
||||
`)
|
||||
})
|
||||
it("make sure it doesn't stomp constraints", async () => {
|
||||
let code = `sketch001 = startSketchOn(YZ)
|
||||
profile001 = startProfile(sketch001, at = [100 + 0, 101])
|
||||
|> line(end = [102, 103 + 0])
|
||||
|> line(endAbsolute = [104 + 0, 105])
|
||||
|> angledLine(angle = 206, length = 106 + 0)
|
||||
|> angledLine(angle = -208 + 0, lengthX = 107)
|
||||
|> angledLine(angle = 210, lengthY = 108 + 0)
|
||||
|> angledLine(angle = 212 + 0, endAbsoluteX = 109)
|
||||
|> angledLine(angle = 214, endAbsoluteY = 110 + 0)
|
||||
|> arc(interiorAbsolute = [111 + 0, 112], endAbsolute = [113, 114 + 0])
|
||||
|> tangentialArc(end = [115, -116 + 0])
|
||||
|> tangentialArc(endAbsolute = [117 + 0, 118])
|
||||
|> tangentialArc(angle = 224, radius = 119 + 0)
|
||||
|> tangentialArc(angle = 226 + 0, diameter = 120)
|
||||
|
||||
profile002 = startProfile(sketch001, at = [-121 + 0, 122])
|
||||
|> angledLine(angle = 130, length = 123 + 0, 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 + 0, -126], radius = 127 + 0)
|
||||
profile004 = circleThreePoint(
|
||||
sketch001,
|
||||
p1 = [128, 129 + 0],
|
||||
p2 = [130 + 0, 131],
|
||||
p3 = [133, 132],
|
||||
)
|
||||
profile005 = circle(sketch001, center = [-134, -135 + 0], diameter = 136)
|
||||
`
|
||||
let ast = assertParse(code)
|
||||
const result = await enginelessExecutor(ast)
|
||||
const searchSnippets = [
|
||||
'startProfile(sketch001, at = [100 + 0, 101])',
|
||||
'startProfile(sketch001, at = [-121 + 0, 122])',
|
||||
'circle(sketch001, center = [-125 + 0, -126], radius = 127 + 0)',
|
||||
'circleThreePoint(',
|
||||
'circle(sketch001, center = [-134, -135 + 0], diameter = 136)',
|
||||
]
|
||||
const ranges = searchSnippets.map((searchSnippet) => {
|
||||
const startIndex = code.indexOf(searchSnippet)
|
||||
return topLevelRange(startIndex, startIndex + searchSnippet.length)
|
||||
})
|
||||
const pathsToProfiles = ranges.map((range) =>
|
||||
getNodePathFromSourceRange(ast, range)
|
||||
)
|
||||
|
||||
if (err(result)) throw result
|
||||
const scaledProfile = scaleProfiles({
|
||||
ast,
|
||||
factor: 0.5,
|
||||
variables: result.variables,
|
||||
pathsToProfile: pathsToProfiles,
|
||||
})
|
||||
if (err(scaledProfile)) throw scaledProfile
|
||||
const pResult = parse(recast(scaledProfile.modifiedAst))
|
||||
if (err(pResult) || !resultIsOk(pResult)) return
|
||||
ast = pResult.program
|
||||
const newCode = recast(ast)
|
||||
if (err(newCode)) throw newCode
|
||||
|
||||
expect(newCode).toBe(`sketch001 = startSketchOn(YZ)
|
||||
profile001 = startProfile(sketch001, at = [100 + 0, 50.5])
|
||||
|> line(end = [51, 103 + 0])
|
||||
|> line(endAbsolute = [104 + 0, 52.5])
|
||||
|> angledLine(angle = -154, length = 106 + 0)
|
||||
|> angledLine(angle = -208 + 0, lengthX = 53.5)
|
||||
|> angledLine(angle = -150, lengthY = 108 + 0)
|
||||
|> angledLine(angle = 212 + 0, endAbsoluteX = 54.5)
|
||||
|> angledLine(angle = -146, endAbsoluteY = 110 + 0)
|
||||
|> arc(interiorAbsolute = [111 + 0, 56], endAbsolute = [56.5, 114 + 0])
|
||||
|> tangentialArc(end = [57.5, -116 + 0])
|
||||
|> tangentialArc(endAbsolute = [117 + 0, 59])
|
||||
|> tangentialArc(angle = 224deg, radius = 119 + 0)
|
||||
|> tangentialArc(angle = 226 + 0, diameter = 60)
|
||||
|
||||
profile002 = startProfile(sketch001, at = [-121 + 0, 61])
|
||||
|> angledLine(angle = 130, length = 123 + 0, tag = $rectangleSegmentA001)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA001) - 232, length = 62)
|
||||
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
profile003 = circle(sketch001, center = [-125 + 0, -63], radius = 127 + 0)
|
||||
profile004 = circleThreePoint(
|
||||
sketch001,
|
||||
p1 = [64, 129 + 0],
|
||||
p2 = [130 + 0, 65.5],
|
||||
p3 = [66.5, 66],
|
||||
)
|
||||
profile005 = circle(sketch001, center = [-67, -135 + 0], diameter = 68)
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
import type { NumericType } from '@rust/kcl-lib/bindings/NumericType'
|
||||
|
||||
/**
|
||||
@ -1293,3 +1310,146 @@ 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<Node<VariableDeclaration>>(
|
||||
ast,
|
||||
profilePath,
|
||||
'VariableDeclaration'
|
||||
)
|
||||
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, {
|
||||
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-static
|
||||
if (!isStaticValue(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 static/constant value (literal, array of literals, or negative literal)
|
||||
function isStaticValue(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) => isStaticValue(element))
|
||||
}
|
||||
|
||||
if (node.type === 'UnaryExpression' && node.operator === '-') {
|
||||
// Negative literal numbers
|
||||
return isStaticValue(node.argument)
|
||||
}
|
||||
|
||||
// All other node types (Name, CallExpression, BinaryExpression, etc.) are non-literal
|
||||
return false
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
ARG_TAG,
|
||||
DETERMINING_ARGS,
|
||||
ARG_INTERIOR_ABSOLUTE,
|
||||
ARG_DIAMETER,
|
||||
} from '@src/lang/constants'
|
||||
import {
|
||||
createArrayExpression,
|
||||
@ -1424,13 +1425,27 @@ export const circle: SketchLineHelperKw = {
|
||||
|
||||
const { node: callExpression } = nodeMeta
|
||||
|
||||
// All function arguments, except the tag
|
||||
const functionArguments = callExpression.arguments
|
||||
.map((arg) => arg.label?.name)
|
||||
.filter((n) => n && n !== ARG_TAG)
|
||||
|
||||
const newCenter = createArrayExpression([
|
||||
createLiteral(roundOff(center[0])),
|
||||
createLiteral(roundOff(center[1])),
|
||||
])
|
||||
mutateKwArg(ARG_CIRCLE_CENTER, callExpression, newCenter)
|
||||
const newRadius = createLiteral(roundOff(radius))
|
||||
mutateKwArg(ARG_RADIUS, callExpression, newRadius)
|
||||
|
||||
// Check if the circle uses diameter or radius
|
||||
const isDiameter = functionArguments.includes(ARG_DIAMETER)
|
||||
if (isDiameter) {
|
||||
const newDiameter = createLiteral(roundOff(radius * 2))
|
||||
mutateKwArg(ARG_DIAMETER, callExpression, newDiameter)
|
||||
} else {
|
||||
const newRadius = createLiteral(roundOff(radius))
|
||||
mutateKwArg(ARG_RADIUS, callExpression, newRadius)
|
||||
}
|
||||
|
||||
return {
|
||||
modifiedAst: _node,
|
||||
pathToNode,
|
||||
@ -4168,7 +4183,14 @@ const tangentialArcHelpers = {
|
||||
.map((arg) => arg.label?.name)
|
||||
.filter((n) => n && n !== ARG_TAG)
|
||||
|
||||
if (areArraysEqual(functionArguments, [ARG_ANGLE, ARG_RADIUS])) {
|
||||
const isDiameter = areArraysEqual(functionArguments, [
|
||||
ARG_ANGLE,
|
||||
ARG_DIAMETER,
|
||||
])
|
||||
if (
|
||||
areArraysEqual(functionArguments, [ARG_ANGLE, ARG_RADIUS]) ||
|
||||
isDiameter
|
||||
) {
|
||||
// Using length and radius -> convert "from", "to" to the matching length and radius
|
||||
const previousEndTangent = input.previousEndTangent
|
||||
if (previousEndTangent) {
|
||||
@ -4219,11 +4241,19 @@ const tangentialArcHelpers = {
|
||||
|
||||
const radius = distance2d(center, from)
|
||||
|
||||
mutateKwArg(
|
||||
ARG_RADIUS,
|
||||
callExpression,
|
||||
createLiteral(roundOff(radius, 2))
|
||||
)
|
||||
if (!isDiameter) {
|
||||
mutateKwArg(
|
||||
ARG_RADIUS,
|
||||
callExpression,
|
||||
createLiteral(roundOff(radius, 2))
|
||||
)
|
||||
} else {
|
||||
mutateKwArg(
|
||||
ARG_DIAMETER,
|
||||
callExpression,
|
||||
createLiteral(roundOff(radius * 2, 2))
|
||||
)
|
||||
}
|
||||
const angleValue = createLiteralMaybeSuffix({
|
||||
value: roundOff(angle, 2),
|
||||
suffix: 'Deg',
|
||||
|
@ -172,6 +172,7 @@ export type ModelingCommandSchema = {
|
||||
variableName: string
|
||||
}
|
||||
namedValue: KclCommandValue
|
||||
scaleSketch?: boolean
|
||||
}
|
||||
'Prompt-to-edit': {
|
||||
prompt: string
|
||||
|
@ -15,6 +15,7 @@ export type CommandBarContext = {
|
||||
currentArgument?: CommandArgument<unknown> & { name: string }
|
||||
argumentsToSubmit: { [x: string]: unknown }
|
||||
machineManager: MachineManager
|
||||
additionalData: { [x: string]: unknown } // For storing extra parameters like scaleSketch
|
||||
}
|
||||
|
||||
export type CommandBarMachineEvent =
|
||||
@ -77,6 +78,7 @@ export type CommandBarMachineEvent =
|
||||
data: { [x: string]: CommandArgumentWithName<unknown> }
|
||||
}
|
||||
| { type: 'Set machine manager'; data: MachineManager }
|
||||
| { type: 'Set additional data'; data: { [x: string]: unknown } }
|
||||
|
||||
export const commandBarMachine = setup({
|
||||
types: {
|
||||
@ -117,6 +119,12 @@ export const commandBarMachine = setup({
|
||||
resolvedArgs[argName] =
|
||||
typeof argValue === 'function' ? argValue(context) : argValue
|
||||
}
|
||||
|
||||
// Special handling for "Constrain with named value" command to include scaleSketch
|
||||
if (selectedCommand.name === 'Constrain with named value') {
|
||||
resolvedArgs.scaleSketch = context.additionalData.scaleSketch
|
||||
}
|
||||
|
||||
selectedCommand?.onSubmit(resolvedArgs)
|
||||
} else {
|
||||
selectedCommand?.onSubmit({ context, event })
|
||||
@ -224,6 +232,16 @@ export const commandBarMachine = setup({
|
||||
selectedCommand: undefined,
|
||||
currentArgument: undefined,
|
||||
argumentsToSubmit: {},
|
||||
additionalData: {},
|
||||
}),
|
||||
'Set additional data': assign({
|
||||
additionalData: ({ context, event }) => {
|
||||
if (event.type !== 'Set additional data') return context.additionalData
|
||||
return {
|
||||
...context.additionalData,
|
||||
...event.data,
|
||||
}
|
||||
},
|
||||
}),
|
||||
'Set selected command': assign({
|
||||
selectedCommand: ({ context, event }) =>
|
||||
@ -468,6 +486,7 @@ export const commandBarMachine = setup({
|
||||
codeBasedSelections: [],
|
||||
},
|
||||
argumentsToSubmit: {},
|
||||
additionalData: {},
|
||||
machineManager: {
|
||||
machines: [],
|
||||
machineApiIp: null,
|
||||
@ -626,6 +645,11 @@ export const commandBarMachine = setup({
|
||||
actions: 'Set machine manager',
|
||||
},
|
||||
|
||||
'Set additional data': {
|
||||
reenter: false,
|
||||
actions: 'Set additional data',
|
||||
},
|
||||
|
||||
Close: {
|
||||
target: '.Closed',
|
||||
actions: 'Clear selected command',
|
||||
|
@ -19,6 +19,7 @@ import { err } from '@src/lib/trap'
|
||||
import {
|
||||
createIdentifier,
|
||||
createLiteral,
|
||||
createLocalName,
|
||||
createVariableDeclaration,
|
||||
} from '@src/lang/create'
|
||||
import { ARG_END_ABSOLUTE, ARG_INTERIOR_ABSOLUTE } from '@src/lang/constants'
|
||||
@ -1157,7 +1158,7 @@ p3 = [342.51, 216.38],
|
||||
filter
|
||||
)
|
||||
const constraint = constraintInfo[constraintIndex]
|
||||
console.log('constraint', constraint)
|
||||
|
||||
if (!constraint.argPosition) {
|
||||
throw new Error(
|
||||
`Constraint at index ${constraintIndex} does not have argPosition`
|
||||
@ -1292,3 +1293,279 @@ p3 = [342.51, 216.38],
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('testing sketch scale on first length constraint', () => {
|
||||
it('should scale sketch when constrain with named value is used with scale checkbox enabled', async () => {
|
||||
// Create a sketch with multiple segments using only literal values (no constraints)
|
||||
const code = `sketch001 = startSketchOn(XZ)
|
||||
profile001 = startProfile(sketch001, at = [100, 100])
|
||||
|> line(end = [200, 0])
|
||||
|> line(end = [0, 200])
|
||||
|> line(end = [-200, 0])
|
||||
|> close()
|
||||
profile002 = circle(sketch001, center = [400, 400], radius = 50)
|
||||
`
|
||||
|
||||
const ast = assertParse(code)
|
||||
await kclManager.executeAst({ ast })
|
||||
expect(kclManager.errors).toEqual([])
|
||||
|
||||
// Find a segment to constrain (the first line segment)
|
||||
const indexOfInterest = code.indexOf('line(end = [200, 0])')
|
||||
const artifact = [...kclManager.artifactGraph].find(
|
||||
([_, artifact]) =>
|
||||
artifact?.type === 'segment' &&
|
||||
artifact.codeRef.range[0] <= indexOfInterest &&
|
||||
indexOfInterest <= artifact.codeRef.range[1]
|
||||
)?.[1]
|
||||
|
||||
if (!artifact || !('codeRef' in artifact)) {
|
||||
throw new Error('Artifact not found or invalid artifact structure')
|
||||
}
|
||||
|
||||
// Create modeling machine actor
|
||||
const modelingActor = createActor(modelingMachine, {
|
||||
input: modelingMachineDefaultContext,
|
||||
}).start()
|
||||
|
||||
// Enter sketch mode
|
||||
modelingActor.send({
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'mirrorCodeMirrorSelections',
|
||||
selection: {
|
||||
graphSelections: [
|
||||
{
|
||||
artifact: artifact,
|
||||
codeRef: artifact.codeRef,
|
||||
},
|
||||
],
|
||||
otherSelections: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
modelingActor.send({ type: 'Enter sketch' })
|
||||
|
||||
// Wait for sketch mode
|
||||
await waitForCondition(() => {
|
||||
const snapshot = modelingActor.getSnapshot()
|
||||
return snapshot.value !== 'animating to existing sketch'
|
||||
}, 5000)
|
||||
|
||||
expect(modelingActor.getSnapshot().value).toEqual({
|
||||
Sketch: { SketchIdle: 'scene drawn' },
|
||||
})
|
||||
|
||||
// Get constraint info for the segment
|
||||
const callExp = getNodeFromPath<Node<CallExpressionKw>>(
|
||||
kclManager.ast,
|
||||
artifact.codeRef.pathToNode,
|
||||
'CallExpressionKw'
|
||||
)
|
||||
if (err(callExp)) {
|
||||
throw new Error('Failed to get CallExpressionKw node')
|
||||
}
|
||||
|
||||
const constraintInfo = getConstraintInfoKw(
|
||||
callExp.node,
|
||||
codeManager.code,
|
||||
artifact.codeRef.pathToNode
|
||||
)
|
||||
const constraint = constraintInfo[0] // First constraint (x value)
|
||||
|
||||
// Store original code to compare scaling
|
||||
const originalCode = codeManager.code
|
||||
|
||||
// No need for command bar setup, we're testing the modeling machine directly
|
||||
|
||||
// Simulate submitting the command with scaling enabled
|
||||
// The new value will be 100 (half of original 200), so scale factor should be 0.5
|
||||
modelingActor.send({
|
||||
type: 'Constrain with named value',
|
||||
data: {
|
||||
currentValue: {
|
||||
valueText: constraint.value,
|
||||
pathToNode: constraint.pathToNode,
|
||||
variableName: 'length_var',
|
||||
},
|
||||
namedValue: {
|
||||
valueText: '100',
|
||||
variableName: 'length_var',
|
||||
insertIndex: 0,
|
||||
valueCalculated: '100',
|
||||
variableDeclarationAst: createVariableDeclaration(
|
||||
'length_var',
|
||||
createLiteral('100')
|
||||
),
|
||||
variableIdentifierAst: createLocalName('length_var'),
|
||||
valueAst: createLiteral('100'),
|
||||
},
|
||||
scaleSketch: true, // This should trigger scaling
|
||||
},
|
||||
})
|
||||
|
||||
// Wait for the constraint to be applied and sketch to be scaled
|
||||
await waitForCondition(() => {
|
||||
const snapshot = modelingActor.getSnapshot()
|
||||
return (
|
||||
JSON.stringify(snapshot.value) !==
|
||||
JSON.stringify({ Sketch: 'Converting to named value' })
|
||||
)
|
||||
}, 5000)
|
||||
|
||||
// Wait for code to be updated
|
||||
const startTime = Date.now()
|
||||
while (codeManager.code === originalCode && Date.now() - startTime < 5000) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
console.log('code is', codeManager.code)
|
||||
|
||||
// Verify the constraint was applied
|
||||
expect(codeManager.code).toContain('length_var')
|
||||
expect(codeManager.code).toContain("length_var = '100'")
|
||||
|
||||
// Verify scaling occurred - all dimensions should be scaled by 0.5 (100/200)
|
||||
// Original values: line(end = [200, 0]), line(end = [0, 200]), line(end = [-200, 0])
|
||||
// Scaled values should be: line(end = [100, 0]), line(end = [0, 100]), line(end = [-100, 0])
|
||||
expect(codeManager.code).toContain('line(end = [length_var, 0])') // First line uses variable
|
||||
expect(codeManager.code).toContain('line(end = [0, 100])') // Second line scaled
|
||||
expect(codeManager.code).toContain('line(end = [-100, 0])') // Third line scaled
|
||||
|
||||
// Circle should also be scaled: radius = 50 -> radius = 25
|
||||
expect(codeManager.code).toContain('radius = 25')
|
||||
|
||||
// Start positions should be scaled: at = [100, 100] -> at = [50, 50], at = [400, 400] -> at = [200, 200]
|
||||
expect(codeManager.code).toContain('at = [50, 50]')
|
||||
expect(codeManager.code).toContain('center = [200, 200]')
|
||||
}, 15_000)
|
||||
|
||||
it('should not scale sketch when constrain with named value is used with scale checkbox disabled', async () => {
|
||||
// Create a sketch with multiple segments using only literal values (no constraints)
|
||||
const code = `sketch001 = startSketchOn(XZ)
|
||||
profile001 = startProfile(sketch001, at = [100, 100])
|
||||
|> line(end = [200, 0])
|
||||
|> line(end = [0, 200])
|
||||
|> close()`
|
||||
|
||||
const ast = assertParse(code)
|
||||
await kclManager.executeAst({ ast })
|
||||
expect(kclManager.errors).toEqual([])
|
||||
|
||||
// Find a segment to constrain (the first line segment)
|
||||
const indexOfInterest = code.indexOf('line(end = [200, 0])')
|
||||
const artifact = [...kclManager.artifactGraph].find(
|
||||
([_, artifact]) =>
|
||||
artifact?.type === 'segment' &&
|
||||
artifact.codeRef.range[0] <= indexOfInterest &&
|
||||
indexOfInterest <= artifact.codeRef.range[1]
|
||||
)?.[1]
|
||||
|
||||
if (!artifact || !('codeRef' in artifact)) {
|
||||
throw new Error('Artifact not found or invalid artifact structure')
|
||||
}
|
||||
|
||||
// Create modeling machine actor
|
||||
const modelingActor = createActor(modelingMachine, {
|
||||
input: modelingMachineDefaultContext,
|
||||
}).start()
|
||||
|
||||
// Enter sketch mode
|
||||
modelingActor.send({
|
||||
type: 'Set selection',
|
||||
data: {
|
||||
selectionType: 'mirrorCodeMirrorSelections',
|
||||
selection: {
|
||||
graphSelections: [
|
||||
{
|
||||
artifact: artifact,
|
||||
codeRef: artifact.codeRef,
|
||||
},
|
||||
],
|
||||
otherSelections: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
modelingActor.send({ type: 'Enter sketch' })
|
||||
|
||||
// Wait for sketch mode
|
||||
await waitForCondition(() => {
|
||||
const snapshot = modelingActor.getSnapshot()
|
||||
return snapshot.value !== 'animating to existing sketch'
|
||||
}, 5000)
|
||||
|
||||
// Get constraint info for the segment
|
||||
const callExp = getNodeFromPath<Node<CallExpressionKw>>(
|
||||
kclManager.ast,
|
||||
artifact.codeRef.pathToNode,
|
||||
'CallExpressionKw'
|
||||
)
|
||||
if (err(callExp)) {
|
||||
throw new Error('Failed to get CallExpressionKw node')
|
||||
}
|
||||
|
||||
const constraintInfo = getConstraintInfoKw(
|
||||
callExp.node,
|
||||
codeManager.code,
|
||||
artifact.codeRef.pathToNode
|
||||
)
|
||||
const constraint = constraintInfo[0] // First constraint (x value)
|
||||
|
||||
// Store original code to compare
|
||||
const originalCode = codeManager.code
|
||||
|
||||
// Submit command with scaling disabled directly to modeling machine
|
||||
|
||||
modelingActor.send({
|
||||
type: 'Constrain with named value',
|
||||
data: {
|
||||
currentValue: {
|
||||
valueText: constraint.value,
|
||||
pathToNode: constraint.pathToNode,
|
||||
variableName: 'length_var',
|
||||
},
|
||||
namedValue: {
|
||||
valueText: '100',
|
||||
variableName: 'length_var',
|
||||
insertIndex: 0,
|
||||
valueCalculated: '100',
|
||||
variableDeclarationAst: createVariableDeclaration(
|
||||
'length_var',
|
||||
createLiteral('100')
|
||||
),
|
||||
variableIdentifierAst: createLocalName('length_var'),
|
||||
valueAst: createLiteral('100'),
|
||||
},
|
||||
scaleSketch: false, // Scaling disabled
|
||||
},
|
||||
})
|
||||
|
||||
// Wait for the constraint to be applied
|
||||
await waitForCondition(() => {
|
||||
const snapshot = modelingActor.getSnapshot()
|
||||
return (
|
||||
JSON.stringify(snapshot.value) !==
|
||||
JSON.stringify({ Sketch: 'Converting to named value' })
|
||||
)
|
||||
}, 5000)
|
||||
|
||||
// Wait for code to be updated
|
||||
const startTime = Date.now()
|
||||
while (codeManager.code === originalCode && Date.now() - startTime < 5000) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
}
|
||||
|
||||
// Verify the constraint was applied but no scaling occurred
|
||||
expect(codeManager.code).toContain('length_var')
|
||||
expect(codeManager.code).toContain("length_var = '100'")
|
||||
expect(codeManager.code).toContain('line(end = [length_var, 0])')
|
||||
|
||||
// Other dimensions should remain unchanged (no scaling)
|
||||
expect(codeManager.code).toContain('line(end = [0, 200])') // Should remain 200, not scaled to 100
|
||||
expect(codeManager.code).toContain('at = [100, 100]') // Should remain [100, 100], not scaled
|
||||
}, 15_000)
|
||||
|
||||
// Note: The third test for checking if scale checkbox is disabled when sketch has existing constraints
|
||||
// would be better tested in the UI layer (CommandBarKclInput.tsx) or as an e2e test
|
||||
// since it's primarily a UI behavior test
|
||||
})
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
orthoScale,
|
||||
quaternionFromUpNForward,
|
||||
} from '@src/clientSideScene/helpers'
|
||||
import { scaleProfiles } from '@src/clientSideScene/sceneEntities'
|
||||
import type { Setting } from '@src/lib/settings/initialSettings'
|
||||
import type { CameraProjectionType } from '@rust/kcl-lib/bindings/CameraProjectionType'
|
||||
import { DRAFT_DASHED_LINE } from '@src/clientSideScene/sceneConstants'
|
||||
@ -2248,6 +2249,55 @@ export const modelingMachine = setup({
|
||||
return Promise.reject(new Error('Unexpected compilation error'))
|
||||
let parsed = pResult.program
|
||||
|
||||
// Apply sketch scaling if enabled
|
||||
if (data.scaleSketch && sketchDetails.sketchNodePaths) {
|
||||
const originalValue = parseFloat(data.currentValue.valueText)
|
||||
const newValue = parseFloat(
|
||||
typeof data.namedValue === 'object' &&
|
||||
'valueText' in data.namedValue
|
||||
? data.namedValue.valueText
|
||||
: String(data.namedValue)
|
||||
)
|
||||
|
||||
if (
|
||||
!Number.isNaN(originalValue) &&
|
||||
!Number.isNaN(newValue) &&
|
||||
originalValue !== 0
|
||||
) {
|
||||
const scaleFactor = newValue / originalValue
|
||||
|
||||
try {
|
||||
const scaleResult = scaleProfiles({
|
||||
ast: parsed,
|
||||
pathsToProfile: sketchDetails.sketchNodePaths,
|
||||
factor: scaleFactor,
|
||||
variables: kclManager.variables,
|
||||
})
|
||||
|
||||
if (!err(scaleResult)) {
|
||||
parsed = scaleResult.modifiedAst
|
||||
|
||||
// Reparse and recast to get fresh source ranges after scaling
|
||||
const reparseResult = parse(recast(parsed))
|
||||
if (!trap(reparseResult) && resultIsOk(reparseResult)) {
|
||||
parsed = reparseResult.program
|
||||
}
|
||||
} else {
|
||||
// Continue with constraint application even if scaling fails
|
||||
console.warn(
|
||||
'Failed to scale sketch, continuing with constraint application'
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue with constraint application even if scaling fails
|
||||
console.warn(
|
||||
'Error scaling sketch, continuing with constraint application:',
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result: {
|
||||
modifiedAst: Node<Program>
|
||||
pathToReplaced: PathToNode | null
|
||||
|
Reference in New Issue
Block a user