Skip to content

Commit

Permalink
Point-and-click Sweep (first PR) (#4989)
Browse files Browse the repository at this point in the history
* 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 214763c.

* 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>
  • Loading branch information
pierremtb and github-actions[bot] authored Jan 11, 2025
1 parent c6fad2e commit ac4a6c8
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 7 deletions.
2 changes: 2 additions & 0 deletions e2e/playwright/fixtures/toolbarFixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class ToolbarFixture {

extrudeButton!: Locator
loftButton!: Locator
sweepButton!: Locator
shellButton!: Locator
offsetPlaneButton!: Locator
startSketchBtn!: Locator
Expand All @@ -40,6 +41,7 @@ export class ToolbarFixture {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep')
this.shellButton = page.getByTestId('shell')
this.offsetPlaneButton = page.getByTestId('plane-offset')
this.startSketchBtn = page.getByTestId('sketch')
Expand Down
98 changes: 98 additions & 0 deletions e2e/playwright/point-click.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{ shouldPreselect: true },
{ shouldPreselect: false },
Expand Down
31 changes: 31 additions & 0 deletions src/lang/modifyAst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
node: Node<Program>,
pathToNode: PathToNode,
Expand Down
10 changes: 7 additions & 3 deletions src/lang/std/artifactGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,15 @@ interface SegmentArtifactRich extends BaseArtifact {
/** A Sweep is a more generic term for extrude, revolve, loft and sweep*/
interface SweepArtifact extends BaseArtifact {
type: 'sweep'
subType: 'extrusion' | 'revolve' | 'loft'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
pathId: string
surfaceIds: Array<string>
edgeIds: Array<string>
codeRef: CodeRef
}
interface SweepArtifactRich extends BaseArtifact {
type: 'sweep'
subType: 'extrusion' | 'revolve' | 'loft'
subType: 'extrusion' | 'revolve' | 'loft' | 'sweep'
path: PathArtifact
surfaces: Array<WallArtifact | CapArtifact>
edges: Array<SweepEdge>
Expand Down Expand Up @@ -377,7 +377,11 @@ export function getArtifactsToUpdate({
})
}
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
returnArr.push({
id,
Expand Down
31 changes: 31 additions & 0 deletions src/lib/commandBarConfigs/modelingCommandConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export type ModelingCommandSchema = {
// result: (typeof EXTRUSION_RESULTS)[number]
distance: KclCommandValue
}
Sweep: {
path: Selections
profile: Selections
}
Loft: {
selection: Selections
}
Expand Down Expand Up @@ -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: {
description: 'Create a 3D body by blending between two or more sketches',
icon: 'loft',
Expand Down
1 change: 1 addition & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
SKETCH: 'sketch',
EXTRUDE: 'extrude',
LOFT: 'loft',
SWEEP: 'sweep',
SHELL: 'shell',
SEGMENT: 'seg',
REVOLVE: 'revolve',
Expand Down
12 changes: 8 additions & 4 deletions src/lib/toolbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,21 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
},
{
id: 'sweep',
onClick: () => console.error('Sweep not yet implemented'),
onClick: ({ commandBarSend }) =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Sweep', groupId: 'modeling' },
}),
icon: 'sweep',
status: 'unavailable',
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
title: 'Sweep',
hotkey: 'W',
description:
'Create a 3D body by moving a sketch region along an arbitrary path.',
links: [
{
label: 'GitHub discussion',
url: 'https://github.com/KittyCAD/modeling-app/discussions/498',
label: 'KCL docs',
url: 'https://zoo.dev/docs/kcl/sweep',
},
],
},
Expand Down
80 changes: 80 additions & 0 deletions src/machines/modelingMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
import { revolveSketch } from 'lang/modifyAst/addRevolve'
import {
addOffsetPlane,
addSweep,
deleteFromSelection,
extrudeSketch,
loftSketches,
Expand Down Expand Up @@ -266,6 +267,7 @@ export type ModelingMachineEvent =
| { type: 'Export'; data: ModelingCommandSchema['Export'] }
| { type: 'Make'; data: ModelingCommandSchema['Make'] }
| { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
| { type: 'Sweep'; data?: ModelingCommandSchema['Sweep'] }
| { type: 'Loft'; data?: ModelingCommandSchema['Loft'] }
| { type: 'Shell'; data?: ModelingCommandSchema['Shell'] }
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
Expand Down Expand Up @@ -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(
async ({
input,
Expand Down Expand Up @@ -1739,6 +1801,11 @@ export const modelingMachine = setup({
reenter: false,
},

Sweep: {
target: 'Applying sweep',
reenter: true,
},

Loft: {
target: 'Applying loft',
reenter: true,
Expand Down Expand Up @@ -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': {
invoke: {
src: 'loftAstMod',
Expand Down

0 comments on commit ac4a6c8

Please sign in to comment.