Remove old dry-run validations on Sweep, Loft, Revolve, and Shell (#6969)
This commit is contained in:
@ -1931,84 +1931,6 @@ sketch002 = startSketchOn(XZ)
|
||||
})
|
||||
})
|
||||
|
||||
test(`Sweep point-and-click failing validation`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `@settings(defaultLengthUnit = in)
|
||||
sketch001 = startSketchOn(YZ)
|
||||
|> circle(
|
||||
center = [0, 0],
|
||||
radius = 500
|
||||
)
|
||||
sketch002 = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> xLine(length = -500)
|
||||
|> line(endAbsolute = [-2000, 500])
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// 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 expect
|
||||
.poll(() => page.getByText('Please select one').count())
|
||||
.toBe(1)
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'sketches',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Sectional: '',
|
||||
Sketches: '',
|
||||
Path: '',
|
||||
},
|
||||
highlightedHeaderArg: 'sketches',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch1()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'path',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Sectional: '',
|
||||
Sketches: '1 face',
|
||||
Path: '',
|
||||
},
|
||||
highlightedHeaderArg: 'path',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch2()
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(
|
||||
page.getByText('Unable to sweep with the current selection. Reason:')
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test(`Fillet point-and-click`, async ({
|
||||
context,
|
||||
page,
|
||||
@ -3627,67 +3549,6 @@ profile001 = startProfile(sketch001, at = [-20, 20])
|
||||
})
|
||||
})
|
||||
|
||||
test(`Shell dry-run validation rejects sweeps`, async ({
|
||||
context,
|
||||
page,
|
||||
homePage,
|
||||
scene,
|
||||
editor,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn(YZ)
|
||||
|> circle(
|
||||
center = [0, 0],
|
||||
radius = 500
|
||||
)
|
||||
sketch002 = startSketchOn(XZ)
|
||||
|> startProfile(at = [0, 0])
|
||||
|> xLine(length = -2000)
|
||||
sweep001 = sweep(sketch001, path = sketch002)
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||
await homePage.goToModelingScene()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
// One dumb hardcoded screen pixel value
|
||||
const testPoint = { x: 500, y: 250 }
|
||||
const [clickOnSweep] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||
|
||||
await test.step(`Confirm sweep exists`, async () => {
|
||||
await toolbar.closePane('code')
|
||||
await scene.expectPixelColor([231, 231, 231], testPoint, 15)
|
||||
})
|
||||
|
||||
await test.step(`Go through the Shell flow and fail validation with a toast`, async () => {
|
||||
await toolbar.shellButton.click()
|
||||
await expect
|
||||
.poll(() => page.getByText('Please select one').count())
|
||||
.toBe(1)
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Selection: '',
|
||||
Thickness: '',
|
||||
},
|
||||
highlightedHeaderArg: 'selection',
|
||||
commandName: 'Shell',
|
||||
})
|
||||
await clickOnSweep()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(
|
||||
page.getByText('Unable to shell with the current selection. Reason:')
|
||||
).toBeVisible()
|
||||
await page.waitForTimeout(1000)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Revolve point and click workflows', () => {
|
||||
test('Base case workflow, auto spam continue in command bar', async ({
|
||||
context,
|
||||
|
@ -12,12 +12,6 @@ import type {
|
||||
VariableDeclarator,
|
||||
} from '@src/lang/wasm'
|
||||
import { isPathToNode } from '@src/lang/wasm'
|
||||
import {
|
||||
loftValidator,
|
||||
revolveAxisValidator,
|
||||
shellValidator,
|
||||
sweepValidator,
|
||||
} from '@src/lib/commandBarConfigs/validators'
|
||||
import type {
|
||||
KclCommandValue,
|
||||
StateMachineCommandSetConfig,
|
||||
@ -428,7 +422,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
selectionTypes: ['segment'],
|
||||
required: true,
|
||||
multiple: false,
|
||||
validation: sweepValidator,
|
||||
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
|
||||
},
|
||||
sectional: {
|
||||
@ -455,7 +448,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
selectionTypes: ['solid2d'],
|
||||
multiple: true,
|
||||
required: true,
|
||||
validation: loftValidator,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -508,7 +500,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
|
||||
multiple: false,
|
||||
validation: revolveAxisValidator,
|
||||
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
|
||||
},
|
||||
angle: {
|
||||
@ -535,7 +526,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
selectionTypes: ['cap', 'wall'],
|
||||
multiple: true,
|
||||
required: true,
|
||||
validation: shellValidator,
|
||||
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
|
||||
},
|
||||
thickness: {
|
||||
|
@ -1,13 +0,0 @@
|
||||
import { parseEngineErrorMessage } from '@src/lib/commandBarConfigs/validators'
|
||||
|
||||
describe('parseEngineErrorMessage', () => {
|
||||
it('takes an engine error string and parses its json message', () => {
|
||||
const engineError =
|
||||
'[{"success": false,"request_id": "e6c0104b-ec60-4779-8e98-722f0a5019ec","errors": [{"error_code": "internal_engine","message": "Trajectory curve must be G1 continuous (with continuous tangents)"}]}]'
|
||||
const parsedEngineError = JSON.parse(engineError)
|
||||
const message = parseEngineErrorMessage(parsedEngineError)
|
||||
expect(message).toEqual(
|
||||
'Trajectory curve must be G1 continuous (with continuous tangents)'
|
||||
)
|
||||
})
|
||||
})
|
@ -1,317 +0,0 @@
|
||||
import type { Models } from '@kittycad/lib'
|
||||
|
||||
import type { Selections } from '@src/lib/selections'
|
||||
import { engineCommandManager, kclManager } from '@src/lib/singletons'
|
||||
import { isArray, uuidv4 } from '@src/lib/utils'
|
||||
import type { CommandBarContext } from '@src/machines/commandBarMachine'
|
||||
|
||||
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
|
||||
for (let tries = 0; tries < numberOfRetries; tries++) {
|
||||
try {
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'disable_dry_run' },
|
||||
})
|
||||
// Exit out since the command was successful
|
||||
return
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error('disable_dry_run failed. This is bad!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Takes a callback function and wraps it around enable_dry_run and disable_dry_run
|
||||
export const dryRunWrapper = async (
|
||||
callback: () => Promise<
|
||||
| Models['WebSocketResponse_type']
|
||||
| [Models['WebSocketResponse_type']]
|
||||
| undefined
|
||||
| null
|
||||
>
|
||||
): Promise<[Models['WebSocketResponse_type']] | undefined> => {
|
||||
// Gotcha: What about race conditions?
|
||||
try {
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: { type: 'enable_dry_run' },
|
||||
})
|
||||
const result = await callback()
|
||||
if (!result) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (isArray(result)) {
|
||||
return result
|
||||
}
|
||||
|
||||
return [result]
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
await disableDryRunWithRetry(5)
|
||||
}
|
||||
}
|
||||
|
||||
function isSelections(selections: unknown): selections is Selections {
|
||||
return (
|
||||
(selections as Selections).graphSelections !== undefined &&
|
||||
(selections as Selections).otherSelections !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
export function parseEngineErrorMessage(
|
||||
engineErrors?: [Models['WebSocketResponse_type']]
|
||||
): string | undefined {
|
||||
if (!engineErrors) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!engineErrors[0]) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const engineError = engineErrors[0]
|
||||
|
||||
if (engineError.success) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const errors = engineError.errors
|
||||
if (!errors[0]) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return errors[0].message
|
||||
}
|
||||
|
||||
export const revolveAxisValidator = async ({
|
||||
data,
|
||||
context,
|
||||
}: {
|
||||
data: { [key: string]: Selections }
|
||||
context: CommandBarContext
|
||||
}): Promise<boolean | string> => {
|
||||
if (!isSelections(context.argumentsToSubmit.sketches)) {
|
||||
return 'Unable to revolve, selections are missing'
|
||||
}
|
||||
// Gotcha: this validation only works for the first sketch
|
||||
const artifact =
|
||||
context.argumentsToSubmit.sketches.graphSelections[0].artifact
|
||||
|
||||
if (!artifact) {
|
||||
return 'Unable to revolve, sketch not found'
|
||||
}
|
||||
|
||||
if (!('pathId' in artifact)) {
|
||||
return 'Unable to revolve, sketch has no path'
|
||||
}
|
||||
|
||||
const sketchSelection = artifact.pathId
|
||||
let edgeSelection = data.edge.graphSelections[0].artifact?.id
|
||||
|
||||
if (!sketchSelection) {
|
||||
return 'Unable to revolve, sketch is missing'
|
||||
}
|
||||
|
||||
if (!edgeSelection) {
|
||||
return 'Unable to revolve, edge is missing'
|
||||
}
|
||||
|
||||
const angleInDegrees: Models['Angle_type'] = {
|
||||
unit: 'degrees',
|
||||
value: 360,
|
||||
}
|
||||
|
||||
const command = async () => {
|
||||
return await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'revolve_about_edge',
|
||||
angle: angleInDegrees,
|
||||
edge_id: edgeSelection,
|
||||
target: sketchSelection,
|
||||
// Gotcha: Playwright will fail with larger tolerances, need to use a smaller one.
|
||||
tolerance: 1e-7,
|
||||
// WARNING: I'm not sure this is what it should be.
|
||||
opposite: 'None',
|
||||
},
|
||||
})
|
||||
}
|
||||
const result = await dryRunWrapper(command)
|
||||
if (result && result[0] && result[0].success) {
|
||||
return true
|
||||
}
|
||||
|
||||
const reason = parseEngineErrorMessage(result) || 'unknown'
|
||||
return `Unable to revolve with the current selection. Reason: ${reason}`
|
||||
}
|
||||
|
||||
export const loftValidator = async ({
|
||||
data,
|
||||
}: {
|
||||
data: { [key: string]: Selections }
|
||||
context: CommandBarContext
|
||||
}): Promise<boolean | string> => {
|
||||
if (!isSelections(data.sketches)) {
|
||||
return 'Unable to loft, selections are missing'
|
||||
}
|
||||
const { sketches } = data
|
||||
|
||||
if (sketches.graphSelections.some((s) => s.artifact?.type !== 'solid2d')) {
|
||||
return 'Unable to loft, some selection are not solid2ds'
|
||||
}
|
||||
|
||||
const sectionIds = sketches.graphSelections.flatMap((s) =>
|
||||
s.artifact?.type === 'solid2d' ? s.artifact.pathId : []
|
||||
)
|
||||
|
||||
if (sectionIds.length < 2) {
|
||||
return 'Unable to loft, selection contains less than two solid2ds'
|
||||
}
|
||||
|
||||
const command = async () => {
|
||||
// TODO: check what to do with these
|
||||
const DEFAULT_V_DEGREE = 2
|
||||
const DEFAULT_TOLERANCE = 2
|
||||
const DEFAULT_BEZ_APPROXIMATE_RATIONAL = false
|
||||
return await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
section_ids: sectionIds,
|
||||
type: 'loft',
|
||||
bez_approximate_rational: DEFAULT_BEZ_APPROXIMATE_RATIONAL,
|
||||
tolerance: DEFAULT_TOLERANCE,
|
||||
v_degree: DEFAULT_V_DEGREE,
|
||||
},
|
||||
})
|
||||
}
|
||||
const result = await dryRunWrapper(command)
|
||||
if (result && result[0] && result[0].success) {
|
||||
return true
|
||||
}
|
||||
|
||||
const reason = parseEngineErrorMessage(result) || 'unknown'
|
||||
return `Unable to loft with the current selection. Reason: ${reason}`
|
||||
}
|
||||
|
||||
export const shellValidator = async ({
|
||||
data,
|
||||
}: {
|
||||
data: { selection: Selections }
|
||||
}): Promise<boolean | string> => {
|
||||
if (!isSelections(data.selection)) {
|
||||
return 'Unable to shell, selections are missing'
|
||||
}
|
||||
|
||||
// No validation on the faces, filtering is done upstream and we have the dry run validation just below
|
||||
const face_ids = data.selection.graphSelections.flatMap((s) =>
|
||||
s.artifact ? s.artifact.id : []
|
||||
)
|
||||
|
||||
// We don't have the concept of solid3ds in TS yet.
|
||||
// So we're listing out the sweeps as if they were solids and taking the first one, just like in Rust for Shell:
|
||||
// https://github.com/KittyCAD/modeling-app/blob/e61fff115b9fa94aaace6307b1842cc15d41655e/src/wasm-lib/kcl/src/std/shell.rs#L237-L238
|
||||
// TODO: This is one cheap way to make sketch-on-face supported now but will likely fail multiple solids
|
||||
const object_id = kclManager.artifactGraph
|
||||
.values()
|
||||
.find((v) => v.type === 'sweep')?.pathId
|
||||
|
||||
if (!object_id) {
|
||||
return "Unable to shell, couldn't find the solid"
|
||||
}
|
||||
|
||||
const command = async () => {
|
||||
// TODO: figure out something better than an arbitrarily small value
|
||||
const DEFAULT_THICKNESS: Models['LengthUnit_type'] = 1e-9
|
||||
const DEFAULT_HOLLOW = false
|
||||
const cmdArgs = {
|
||||
face_ids,
|
||||
object_id,
|
||||
hollow: DEFAULT_HOLLOW,
|
||||
shell_thickness: DEFAULT_THICKNESS,
|
||||
}
|
||||
return await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
type: 'solid3d_shell_face',
|
||||
...cmdArgs,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const result = await dryRunWrapper(command)
|
||||
if (result && result[0] && result[0].success) {
|
||||
return true
|
||||
}
|
||||
|
||||
const reason = parseEngineErrorMessage(result) || 'unknown'
|
||||
return `Unable to shell with the current selection. Reason: ${reason}`
|
||||
}
|
||||
|
||||
export const sweepValidator = async ({
|
||||
context,
|
||||
data,
|
||||
}: {
|
||||
context: CommandBarContext
|
||||
data: { path: Selections }
|
||||
}): Promise<boolean | string> => {
|
||||
if (!isSelections(data.path)) {
|
||||
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.path.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['sketches'] 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 command = 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 result = await dryRunWrapper(command)
|
||||
if (result && result[0] && result[0].success) {
|
||||
return true
|
||||
}
|
||||
|
||||
const reason = parseEngineErrorMessage(result) || 'unknown'
|
||||
return `Unable to sweep with the current selection. Reason: ${reason}`
|
||||
}
|
Reference in New Issue
Block a user