Point-and-click Sweep (first PR) (#4989)
* Refactor 'Delete selection' as actor
Will fix #4662
* WIP logging
* WIP: working Solid3dGetExtrusionFaceInfo for loft
* Working wall deletion of loft
* Add offset plane deletion
* Add feature tree deletion of shell
* Clean up
* Revert "Clean up"
This reverts commit 214763cc2b
.
* Clean up rust changes, taking the sketch with the most paths
* Working cap selection and deletion
* Clean up
* Add test for loft and offset plane deletion via selection
* A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-16-cores)
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-macos-8-cores)
* Set reenter: false as it was originally
* Passing test
* Add shell deletion via feature tree test
* Revert the migration to promise actor
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* Trigger CI
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* Trigger CI
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* Trigger CI
* A snapshot a day keeps the bugs away! 📷🐛 (OS: namespace-profile-ubuntu-8-cores)
* Trigger CI
* Use cmd.id as solid_id after latest engine merge
* Add feature tree deletion of offset plane and fix lint
* Add feature tree deletion of loft
* Clean up
* Better comment
* Lint fix
* Remove sketch sorting
* WIP: sweep point-and-click
* Working sweep
* Add test
* Make sweep a development command
* Fix tsc error
* Clean up for review
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
@ -14,6 +14,7 @@ export class ToolbarFixture {
|
|||||||
|
|
||||||
extrudeButton!: Locator
|
extrudeButton!: Locator
|
||||||
loftButton!: Locator
|
loftButton!: Locator
|
||||||
|
sweepButton!: Locator
|
||||||
shellButton!: Locator
|
shellButton!: Locator
|
||||||
offsetPlaneButton!: Locator
|
offsetPlaneButton!: Locator
|
||||||
startSketchBtn!: Locator
|
startSketchBtn!: Locator
|
||||||
@ -40,6 +41,7 @@ export class ToolbarFixture {
|
|||||||
this.page = page
|
this.page = page
|
||||||
this.extrudeButton = page.getByTestId('extrude')
|
this.extrudeButton = page.getByTestId('extrude')
|
||||||
this.loftButton = page.getByTestId('loft')
|
this.loftButton = page.getByTestId('loft')
|
||||||
|
this.sweepButton = page.getByTestId('sweep')
|
||||||
this.shellButton = page.getByTestId('shell')
|
this.shellButton = page.getByTestId('shell')
|
||||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||||
this.startSketchBtn = page.getByTestId('sketch')
|
this.startSketchBtn = page.getByTestId('sketch')
|
||||||
|
@ -934,6 +934,104 @@ loft001 = loft([sketch001, sketch002])
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test(`Sweep point-and-click`, async ({
|
||||||
|
context,
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn('YZ')
|
||||||
|
|> circle({
|
||||||
|
center = [0, 0],
|
||||||
|
radius = 500
|
||||||
|
}, %)
|
||||||
|
sketch002 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> xLine(-500, %)
|
||||||
|
|> tangentialArcTo([-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)
|
||||||
|
const sweepDeclaration = 'sweep001 = sweep({ path = sketch002 }, sketch001)'
|
||||||
|
|
||||||
|
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`, async () => {
|
||||||
|
await toolbar.sweepButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Sweep',
|
||||||
|
currentArgKey: 'profile',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Path: '',
|
||||||
|
Profile: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'profile',
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await clickOnSketch1()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Sweep',
|
||||||
|
currentArgKey: 'path',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Path: '',
|
||||||
|
Profile: '1 face',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'path',
|
||||||
|
stage: 'arguments',
|
||||||
|
})
|
||||||
|
await clickOnSketch2()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
commandName: 'Sweep',
|
||||||
|
headerArguments: {
|
||||||
|
Path: '1 face',
|
||||||
|
Profile: '1 face',
|
||||||
|
},
|
||||||
|
stage: 'review',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
|
await scene.expectPixelColor([135, 64, 73], testPoint, 15)
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await editor.expectEditor.toContain(sweepDeclaration)
|
||||||
|
await editor.expectState({
|
||||||
|
diagnostics: [],
|
||||||
|
activeLines: [sweepDeclaration],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
await toolbar.closePane('code')
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Delete sweep via feature tree selection', async () => {
|
||||||
|
await toolbar.openPane('feature-tree')
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
const operationButton = await toolbar.getFeatureTreeOperation('Sweep', 0)
|
||||||
|
await operationButton.click({ button: 'left' })
|
||||||
|
await page.keyboard.press('Backspace')
|
||||||
|
await page.waitForTimeout(500)
|
||||||
|
await toolbar.closePane('feature-tree')
|
||||||
|
await scene.expectPixelColor([53, 53, 53], testPoint, 15)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const shellPointAndClickCapCases = [
|
const shellPointAndClickCapCases = [
|
||||||
{ shouldPreselect: true },
|
{ shouldPreselect: true },
|
||||||
{ shouldPreselect: false },
|
{ shouldPreselect: false },
|
||||||
|
@ -374,6 +374,37 @@ export function loftSketches(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addSweep(
|
||||||
|
node: Node<Program>,
|
||||||
|
profileDeclarator: VariableDeclarator,
|
||||||
|
pathDeclarator: VariableDeclarator
|
||||||
|
): {
|
||||||
|
modifiedAst: Node<Program>
|
||||||
|
pathToNode: PathToNode
|
||||||
|
} {
|
||||||
|
const modifiedAst = structuredClone(node)
|
||||||
|
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP)
|
||||||
|
const sweep = createCallExpressionStdLib('sweep', [
|
||||||
|
createObjectExpression({ path: createIdentifier(pathDeclarator.id.name) }),
|
||||||
|
createIdentifier(profileDeclarator.id.name),
|
||||||
|
])
|
||||||
|
const declaration = createVariableDeclaration(name, sweep)
|
||||||
|
modifiedAst.body.push(declaration)
|
||||||
|
const pathToNode: PathToNode = [
|
||||||
|
['body', ''],
|
||||||
|
[modifiedAst.body.length - 1, 'index'],
|
||||||
|
['declaration', 'VariableDeclaration'],
|
||||||
|
['init', 'VariableDeclarator'],
|
||||||
|
['arguments', 'CallExpression'],
|
||||||
|
[0, 'index'],
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedAst,
|
||||||
|
pathToNode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function revolveSketch(
|
export function revolveSketch(
|
||||||
node: Node<Program>,
|
node: Node<Program>,
|
||||||
pathToNode: PathToNode,
|
pathToNode: PathToNode,
|
||||||
|
@ -77,7 +77,7 @@ interface SegmentArtifactRich extends BaseArtifact {
|
|||||||
/** A Sweep is a more generic term for extrude, revolve, loft and sweep*/
|
/** A Sweep is a more generic term for extrude, revolve, loft and sweep*/
|
||||||
interface SweepArtifact extends BaseArtifact {
|
interface SweepArtifact extends BaseArtifact {
|
||||||
type: 'sweep'
|
type: 'sweep'
|
||||||
subType: 'extrusion' | 'revolve' | 'loft'
|
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
|
||||||
pathId: string
|
pathId: string
|
||||||
surfaceIds: Array<string>
|
surfaceIds: Array<string>
|
||||||
edgeIds: Array<string>
|
edgeIds: Array<string>
|
||||||
@ -85,7 +85,7 @@ interface SweepArtifact extends BaseArtifact {
|
|||||||
}
|
}
|
||||||
interface SweepArtifactRich extends BaseArtifact {
|
interface SweepArtifactRich extends BaseArtifact {
|
||||||
type: 'sweep'
|
type: 'sweep'
|
||||||
subType: 'extrusion' | 'revolve' | 'loft'
|
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
|
||||||
path: PathArtifact
|
path: PathArtifact
|
||||||
surfaces: Array<WallArtifact | CapArtifact>
|
surfaces: Array<WallArtifact | CapArtifact>
|
||||||
edges: Array<SweepEdge>
|
edges: Array<SweepEdge>
|
||||||
@ -377,7 +377,11 @@ export function getArtifactsToUpdate({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return returnArr
|
return returnArr
|
||||||
} else if (cmd.type === 'extrude' || cmd.type === 'revolve') {
|
} else if (
|
||||||
|
cmd.type === 'extrude' ||
|
||||||
|
cmd.type === 'revolve' ||
|
||||||
|
cmd.type === 'sweep'
|
||||||
|
) {
|
||||||
const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type
|
const subType = cmd.type === 'extrude' ? 'extrusion' : cmd.type
|
||||||
returnArr.push({
|
returnArr.push({
|
||||||
id,
|
id,
|
||||||
|
@ -37,6 +37,10 @@ export type ModelingCommandSchema = {
|
|||||||
// result: (typeof EXTRUSION_RESULTS)[number]
|
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||||
distance: KclCommandValue
|
distance: KclCommandValue
|
||||||
}
|
}
|
||||||
|
Sweep: {
|
||||||
|
path: Selections
|
||||||
|
profile: Selections
|
||||||
|
}
|
||||||
Loft: {
|
Loft: {
|
||||||
selection: Selections
|
selection: Selections
|
||||||
}
|
}
|
||||||
@ -292,6 +296,33 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Sweep: {
|
||||||
|
description:
|
||||||
|
'Create a 3D body by moving a sketch region along an arbitrary path.',
|
||||||
|
icon: 'sweep',
|
||||||
|
status: 'development',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
profile: {
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['solid2D'],
|
||||||
|
required: true,
|
||||||
|
skip: true,
|
||||||
|
multiple: false,
|
||||||
|
// TODO: add dry-run validation
|
||||||
|
warningMessage:
|
||||||
|
'The sweep workflow is new and under tested. Please break it and report issues.',
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['segment', 'path'],
|
||||||
|
required: true,
|
||||||
|
skip: true,
|
||||||
|
multiple: false,
|
||||||
|
// TODO: add dry-run validation
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
Loft: {
|
Loft: {
|
||||||
description: 'Create a 3D body by blending between two or more sketches',
|
description: 'Create a 3D body by blending between two or more sketches',
|
||||||
icon: 'loft',
|
icon: 'loft',
|
||||||
|
@ -53,6 +53,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
|
|||||||
SKETCH: 'sketch',
|
SKETCH: 'sketch',
|
||||||
EXTRUDE: 'extrude',
|
EXTRUDE: 'extrude',
|
||||||
LOFT: 'loft',
|
LOFT: 'loft',
|
||||||
|
SWEEP: 'sweep',
|
||||||
SHELL: 'shell',
|
SHELL: 'shell',
|
||||||
SEGMENT: 'seg',
|
SEGMENT: 'seg',
|
||||||
REVOLVE: 'revolve',
|
REVOLVE: 'revolve',
|
||||||
|
@ -119,17 +119,21 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'sweep',
|
id: 'sweep',
|
||||||
onClick: () => console.error('Sweep not yet implemented'),
|
onClick: ({ commandBarSend }) =>
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: { name: 'Sweep', groupId: 'modeling' },
|
||||||
|
}),
|
||||||
icon: 'sweep',
|
icon: 'sweep',
|
||||||
status: 'unavailable',
|
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
|
||||||
title: 'Sweep',
|
title: 'Sweep',
|
||||||
hotkey: 'W',
|
hotkey: 'W',
|
||||||
description:
|
description:
|
||||||
'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.',
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
label: 'GitHub discussion',
|
label: 'KCL docs',
|
||||||
url: 'https://github.com/KittyCAD/modeling-app/discussions/498',
|
url: 'https://zoo.dev/docs/kcl/sweep',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -45,6 +45,7 @@ import {
|
|||||||
import { revolveSketch } from 'lang/modifyAst/addRevolve'
|
import { revolveSketch } from 'lang/modifyAst/addRevolve'
|
||||||
import {
|
import {
|
||||||
addOffsetPlane,
|
addOffsetPlane,
|
||||||
|
addSweep,
|
||||||
deleteFromSelection,
|
deleteFromSelection,
|
||||||
extrudeSketch,
|
extrudeSketch,
|
||||||
loftSketches,
|
loftSketches,
|
||||||
@ -266,6 +267,7 @@ export type ModelingMachineEvent =
|
|||||||
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
|
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
|
||||||
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
|
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
|
||||||
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
|
||||||
|
| { type: 'Sweep'; data?: ModelingCommandSchema['Sweep'] }
|
||||||
| { type: 'Loft'; data?: ModelingCommandSchema['Loft'] }
|
| { type: 'Loft'; data?: ModelingCommandSchema['Loft'] }
|
||||||
| { type: 'Shell'; data?: ModelingCommandSchema['Shell'] }
|
| { type: 'Shell'; data?: ModelingCommandSchema['Shell'] }
|
||||||
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
|
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
|
||||||
@ -1544,6 +1546,66 @@ export const modelingMachine = setup({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
sweepAstMod: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: ModelingCommandSchema['Sweep'] | undefined
|
||||||
|
}) => {
|
||||||
|
if (!input) return new Error('No input provided')
|
||||||
|
// Extract inputs
|
||||||
|
const ast = kclManager.ast
|
||||||
|
const { profile, path } = input
|
||||||
|
|
||||||
|
// Find the profile declaration
|
||||||
|
const profileNodePath = getNodePathFromSourceRange(
|
||||||
|
ast,
|
||||||
|
profile.graphSelections[0].codeRef.range
|
||||||
|
)
|
||||||
|
const profileNode = getNodeFromPath<VariableDeclarator>(
|
||||||
|
ast,
|
||||||
|
profileNodePath,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
if (err(profileNode)) {
|
||||||
|
return new Error("Couldn't parse profile selection")
|
||||||
|
}
|
||||||
|
const profileDeclarator = profileNode.node
|
||||||
|
|
||||||
|
// Find the path declaration
|
||||||
|
const pathNodePath = getNodePathFromSourceRange(
|
||||||
|
ast,
|
||||||
|
path.graphSelections[0].codeRef.range
|
||||||
|
)
|
||||||
|
const pathNode = getNodeFromPath<VariableDeclarator>(
|
||||||
|
ast,
|
||||||
|
pathNodePath,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
if (err(pathNode)) {
|
||||||
|
return new Error("Couldn't parse path selection")
|
||||||
|
}
|
||||||
|
const pathDeclarator = pathNode.node
|
||||||
|
|
||||||
|
// Perform the sweep
|
||||||
|
const sweepRes = addSweep(ast, profileDeclarator, pathDeclarator)
|
||||||
|
const updateAstResult = await kclManager.updateAst(
|
||||||
|
sweepRes.modifiedAst,
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
focusPath: [sweepRes.pathToNode],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updateAstResult.newAst
|
||||||
|
)
|
||||||
|
|
||||||
|
if (updateAstResult?.selections) {
|
||||||
|
editorManager.selectRange(updateAstResult?.selections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
loftAstMod: fromPromise(
|
loftAstMod: fromPromise(
|
||||||
async ({
|
async ({
|
||||||
input,
|
input,
|
||||||
@ -1739,6 +1801,11 @@ export const modelingMachine = setup({
|
|||||||
reenter: false,
|
reenter: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Sweep: {
|
||||||
|
target: 'Applying sweep',
|
||||||
|
reenter: true,
|
||||||
|
},
|
||||||
|
|
||||||
Loft: {
|
Loft: {
|
||||||
target: 'Applying loft',
|
target: 'Applying loft',
|
||||||
reenter: true,
|
reenter: true,
|
||||||
@ -2531,6 +2598,19 @@ export const modelingMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'Applying sweep': {
|
||||||
|
invoke: {
|
||||||
|
src: 'sweepAstMod',
|
||||||
|
id: 'sweepAstMod',
|
||||||
|
input: ({ event }) => {
|
||||||
|
if (event.type !== 'Sweep') return undefined
|
||||||
|
return event.data
|
||||||
|
},
|
||||||
|
onDone: ['idle'],
|
||||||
|
onError: ['idle'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
'Applying loft': {
|
'Applying loft': {
|
||||||
invoke: {
|
invoke: {
|
||||||
src: 'loftAstMod',
|
src: 'loftAstMod',
|
||||||
|
Reference in New Issue
Block a user