Add dry-run validation for Sweep (#5097)

* Add dry-run validation for Sweep
Fixes #5095

* Add sweep test failing validation

* Make naming more consistent with engine

* Fix tests after big rename

* Fix tsc after main merge
This commit is contained in:
Pierre Jacquier
2025-01-22 15:59:47 +01:00
committed by GitHub
parent 10789d9c3c
commit 10da986649
4 changed files with 162 additions and 38 deletions

View File

@ -963,37 +963,31 @@ sketch002 = startSketchOn('XZ')
await toolbar.sweepButton.click() await toolbar.sweepButton.click()
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Sweep', commandName: 'Sweep',
currentArgKey: 'profile', currentArgKey: 'target',
currentArgValue: '', currentArgValue: '',
headerArguments: { headerArguments: {
Path: '', Target: '',
Profile: '', Trajectory: '',
}, },
highlightedHeaderArg: 'profile', highlightedHeaderArg: 'target',
stage: 'arguments', stage: 'arguments',
}) })
await clickOnSketch1() await clickOnSketch1()
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Sweep', commandName: 'Sweep',
currentArgKey: 'path', currentArgKey: 'trajectory',
currentArgValue: '', currentArgValue: '',
headerArguments: { headerArguments: {
Path: '', Target: '1 face',
Profile: '1 face', Trajectory: '',
}, },
highlightedHeaderArg: 'path', highlightedHeaderArg: 'trajectory',
stage: 'arguments', stage: 'arguments',
}) })
await clickOnSketch2() await clickOnSketch2()
await cmdBar.expectState({ await page.waitForTimeout(500)
commandName: 'Sweep',
headerArguments: {
Path: '1 face',
Profile: '1 face',
},
stage: 'review',
})
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await page.waitForTimeout(500)
}) })
await test.step(`Confirm code is added to the editor, scene has changed`, async () => { await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
@ -1020,6 +1014,75 @@ sketch002 = startSketchOn('XZ')
}) })
}) })
test(`Sweep point-and-click failing validation`, async ({
context,
page,
homePage,
scene,
toolbar,
cmdBar,
}) => {
const initialCode = `sketch001 = startSketchOn('YZ')
|> circle({
center = [0, 0],
radius = 500
}, %)
sketch002 = startSketchOn('XZ')
|> startProfileAt([0, 0], %)
|> xLine(-500, %)
|> lineTo([-2000, 500], %)
`
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, initialCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.waitForExecutionDone()
// One dumb hardcoded screen pixel value
const testPoint = { x: 700, y: 250 }
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x - 50, testPoint.y)
await test.step(`Look for sketch001`, async () => {
await toolbar.closePane('code')
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
})
await test.step(`Go through the command bar flow and fail validation with a toast`, async () => {
await toolbar.sweepButton.click()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'target',
currentArgValue: '',
headerArguments: {
Target: '',
Trajectory: '',
},
highlightedHeaderArg: 'target',
stage: 'arguments',
})
await clickOnSketch1()
await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'trajectory',
currentArgValue: '',
headerArguments: {
Target: '1 face',
Trajectory: '',
},
highlightedHeaderArg: 'trajectory',
stage: 'arguments',
})
await clickOnSketch2()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar()
await expect(
page.getByText('Unable to sweep with the provided selection')
).toBeVisible()
})
})
test(`Fillet point-and-click`, async ({ test(`Fillet point-and-click`, async ({
context, context,
page, page,

View File

@ -13,6 +13,7 @@ import {
loftValidator, loftValidator,
revolveAxisValidator, revolveAxisValidator,
shellValidator, shellValidator,
sweepValidator,
} from './validators' } from './validators'
type OutputFormat = Models['OutputFormat_type'] type OutputFormat = Models['OutputFormat_type']
@ -42,8 +43,8 @@ export type ModelingCommandSchema = {
distance: KclCommandValue distance: KclCommandValue
} }
Sweep: { Sweep: {
path: Selections target: Selections
profile: Selections trajectory: Selections
} }
Loft: { Loft: {
selection: Selections selection: Selections
@ -308,25 +309,24 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
'Create a 3D body by moving a sketch region along an arbitrary path.', 'Create a 3D body by moving a sketch region along an arbitrary path.',
icon: 'sweep', icon: 'sweep',
status: 'development', status: 'development',
needsReview: true, needsReview: false,
args: { args: {
profile: { target: {
inputType: 'selection', inputType: 'selection',
selectionTypes: ['solid2d'], selectionTypes: ['solid2d'],
required: true, required: true,
skip: true, skip: true,
multiple: false, multiple: false,
// TODO: add dry-run validation
warningMessage: warningMessage:
'The sweep workflow is new and under tested. Please break it and report issues.', 'The sweep workflow is new and under tested. Please break it and report issues.',
}, },
path: { trajectory: {
inputType: 'selection', inputType: 'selection',
selectionTypes: ['segment', 'path'], selectionTypes: ['segment', 'path'],
required: true, required: true,
skip: true, skip: false,
multiple: false, multiple: false,
// TODO: add dry-run validation validation: sweepValidator,
}, },
}, },
}, },

View File

@ -207,3 +207,64 @@ export const shellValidator = async ({
return 'Unable to shell with the provided selection' return 'Unable to shell with the provided selection'
} }
export const sweepValidator = async ({
context,
data,
}: {
context: CommandBarContext
data: { trajectory: Selections }
}): Promise<boolean | string> => {
if (!isSelections(data.trajectory)) {
console.log('Unable to sweep, selections are missing')
return 'Unable to sweep, selections are missing'
}
// Retrieve the parent path from the segment selection directly
const trajectoryArtifact = data.trajectory.graphSelections[0].artifact
if (!trajectoryArtifact) {
return "Unable to sweep, couldn't find the trajectory artifact"
}
if (trajectoryArtifact.type !== 'segment') {
return "Unable to sweep, couldn't find the target from a non-segment selection"
}
const trajectory = trajectoryArtifact.pathId
// Get the former arg in the command bar flow, and retrieve the path from the solid2d directly
const targetArg = context.argumentsToSubmit['target'] as Selections
const targetArtifact = targetArg.graphSelections[0].artifact
if (!targetArtifact) {
return "Unable to sweep, couldn't find the profile artifact"
}
if (targetArtifact.type !== 'solid2d') {
return "Unable to sweep, couldn't find the target from a non-solid2d selection"
}
const target = targetArtifact.pathId
const sweepCommand = async () => {
// TODO: second look on defaults here
const DEFAULT_TOLERANCE: Models['LengthUnit_type'] = 1e-7
const DEFAULT_SECTIONAL = false
const cmdArgs = {
target,
trajectory,
sectional: DEFAULT_SECTIONAL,
tolerance: DEFAULT_TOLERANCE,
}
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'sweep',
...cmdArgs,
},
})
}
const attemptSweep = await dryRunWrapper(sweepCommand)
if (attemptSweep?.success) {
return true
}
return 'Unable to sweep with the provided selection'
}

View File

@ -1561,40 +1561,40 @@ export const modelingMachine = setup({
if (!input) return new Error('No input provided') if (!input) return new Error('No input provided')
// Extract inputs // Extract inputs
const ast = kclManager.ast const ast = kclManager.ast
const { profile, path } = input const { target, trajectory } = input
// Find the profile declaration // Find the profile declaration
const profileNodePath = getNodePathFromSourceRange( const targetNodePath = getNodePathFromSourceRange(
ast, ast,
profile.graphSelections[0].codeRef.range target.graphSelections[0].codeRef.range
) )
const profileNode = getNodeFromPath<VariableDeclarator>( const targetNode = getNodeFromPath<VariableDeclarator>(
ast, ast,
profileNodePath, targetNodePath,
'VariableDeclarator' 'VariableDeclarator'
) )
if (err(profileNode)) { if (err(targetNode)) {
return new Error("Couldn't parse profile selection") return new Error("Couldn't parse profile selection")
} }
const profileDeclarator = profileNode.node const targetDeclarator = targetNode.node
// Find the path declaration // Find the path declaration
const pathNodePath = getNodePathFromSourceRange( const trajectoryNodePath = getNodePathFromSourceRange(
ast, ast,
path.graphSelections[0].codeRef.range trajectory.graphSelections[0].codeRef.range
) )
const pathNode = getNodeFromPath<VariableDeclarator>( const trajectoryNode = getNodeFromPath<VariableDeclarator>(
ast, ast,
pathNodePath, trajectoryNodePath,
'VariableDeclarator' 'VariableDeclarator'
) )
if (err(pathNode)) { if (err(trajectoryNode)) {
return new Error("Couldn't parse path selection") return new Error("Couldn't parse path selection")
} }
const pathDeclarator = pathNode.node const trajectoryDeclarator = trajectoryNode.node
// Perform the sweep // Perform the sweep
const sweepRes = addSweep(ast, profileDeclarator, pathDeclarator) const sweepRes = addSweep(ast, targetDeclarator, trajectoryDeclarator)
const updateAstResult = await kclManager.updateAst( const updateAstResult = await kclManager.updateAst(
sweepRes.modifiedAst, sweepRes.modifiedAst,
true, true,