Point-and-click Loft (#4605)
* WIP: experimenting with Loft UI Relates to #4470 * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * Add selection guard * Working loft for two sketches in the right hardcoded order * First pass at handling more than 2 sketches * WIP selections * WIP selections * More checks * Appends the loft line after the 'last' sketch in the code * Clean up * Enable multiple selections after the button click * First point-click loft test (not working locally, loft gets inserted at the wrong place) * Lint * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * Clean up and working pw test * Add test for doesSceneHaveSweepableSketch with count = 2 * Clean up loftSketches function * Add pw test for preselected sketches * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * Trigger CI * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * Move to fromPromise-based Actor * Move error logic out of loftSketches, fix pw tests * Remove comments * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * Trigger CI * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * Trigger CI * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * Fix typo * Revert snapshots * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * Trigger CI * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) * Trigger CI --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@ -6,6 +6,7 @@ export class ToolbarFixture {
|
|||||||
public page: Page
|
public page: Page
|
||||||
|
|
||||||
extrudeButton!: Locator
|
extrudeButton!: Locator
|
||||||
|
loftButton!: Locator
|
||||||
offsetPlaneButton!: Locator
|
offsetPlaneButton!: Locator
|
||||||
startSketchBtn!: Locator
|
startSketchBtn!: Locator
|
||||||
lineBtn!: Locator
|
lineBtn!: Locator
|
||||||
@ -26,6 +27,7 @@ export class ToolbarFixture {
|
|||||||
reConstruct = (page: Page) => {
|
reConstruct = (page: Page) => {
|
||||||
this.page = page
|
this.page = page
|
||||||
this.extrudeButton = page.getByTestId('extrude')
|
this.extrudeButton = page.getByTestId('extrude')
|
||||||
|
this.loftButton = page.getByTestId('loft')
|
||||||
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
this.offsetPlaneButton = page.getByTestId('plane-offset')
|
||||||
this.startSketchBtn = page.getByTestId('sketch')
|
this.startSketchBtn = page.getByTestId('sketch')
|
||||||
this.lineBtn = page.getByTestId('line')
|
this.lineBtn = page.getByTestId('line')
|
||||||
|
@ -677,3 +677,94 @@ test(`Offset plane point-and-click`, async ({
|
|||||||
await scene.expectPixelColor([74, 74, 74], testPoint, 15)
|
await scene.expectPixelColor([74, 74, 74], testPoint, 15)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const loftPointAndClickCases = [
|
||||||
|
{ shouldPreselect: true },
|
||||||
|
{ shouldPreselect: false },
|
||||||
|
]
|
||||||
|
loftPointAndClickCases.forEach(({ shouldPreselect }) => {
|
||||||
|
test(`Loft point-and-click (preselected sketches: ${shouldPreselect})`, async ({
|
||||||
|
app,
|
||||||
|
page,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 30 }, %)
|
||||||
|
plane001 = offsetPlane('XZ', 50)
|
||||||
|
sketch002 = startSketchOn(plane001)
|
||||||
|
|> circle({ center = [0, 0], radius = 20 }, %)
|
||||||
|
`
|
||||||
|
await app.initialise(initialCode)
|
||||||
|
|
||||||
|
// One dumb hardcoded screen pixel value
|
||||||
|
const testPoint = { x: 575, y: 200 }
|
||||||
|
const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
|
||||||
|
const [clickOnSketch2] = scene.makeMouseHelpers(
|
||||||
|
testPoint.x,
|
||||||
|
testPoint.y + 80
|
||||||
|
)
|
||||||
|
const loftDeclaration = 'loft001 = loft([sketch001, sketch002])'
|
||||||
|
|
||||||
|
await test.step(`Look for the white of the sketch001 shape`, async () => {
|
||||||
|
await scene.expectPixelColor([254, 254, 254], testPoint, 15)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function selectSketches() {
|
||||||
|
await clickOnSketch1()
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await clickOnSketch2()
|
||||||
|
await app.page.waitForTimeout(500)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldPreselect) {
|
||||||
|
await test.step(`Go through the command bar flow without preselected sketches`, async () => {
|
||||||
|
await toolbar.loftButton.click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: { Selection: '' },
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
commandName: 'Loft',
|
||||||
|
})
|
||||||
|
await selectSketches()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: { Selection: '2 faces' },
|
||||||
|
commandName: 'Loft',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
await test.step(`Preselect the two sketches`, async () => {
|
||||||
|
await selectSketches()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step(`Go through the command bar flow with preselected sketches`, async () => {
|
||||||
|
await toolbar.loftButton.click()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: { Selection: '2 faces' },
|
||||||
|
commandName: 'Loft',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
|
||||||
|
await editor.expectEditor.toContain(loftDeclaration)
|
||||||
|
await editor.expectState({
|
||||||
|
diagnostics: [],
|
||||||
|
activeLines: [loftDeclaration],
|
||||||
|
highlightedCode: '',
|
||||||
|
})
|
||||||
|
await scene.expectPixelColor([89, 89, 89], testPoint, 15)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
@ -50,6 +50,7 @@ import {
|
|||||||
isSketchPipe,
|
isSketchPipe,
|
||||||
Selections,
|
Selections,
|
||||||
updateSelections,
|
updateSelections,
|
||||||
|
canLoftSelection,
|
||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||||
@ -569,6 +570,21 @@ export const ModelingMachineProvider = ({
|
|||||||
if (err(canSweep)) return false
|
if (err(canSweep)) return false
|
||||||
return canSweep
|
return canSweep
|
||||||
},
|
},
|
||||||
|
'has valid loft selection': ({ context: { selectionRanges } }) => {
|
||||||
|
const hasNoSelection =
|
||||||
|
selectionRanges.graphSelections.length === 0 ||
|
||||||
|
isRangeBetweenCharacters(selectionRanges) ||
|
||||||
|
isSelectionLastLine(selectionRanges, codeManager.code)
|
||||||
|
|
||||||
|
if (hasNoSelection) {
|
||||||
|
const count = 2
|
||||||
|
return doesSceneHaveSweepableSketch(kclManager.ast, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canLoft = canLoftSelection(selectionRanges)
|
||||||
|
if (err(canLoft)) return false
|
||||||
|
return canLoft
|
||||||
|
},
|
||||||
'has valid selection for deletion': ({
|
'has valid selection for deletion': ({
|
||||||
context: { selectionRanges },
|
context: { selectionRanges },
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -346,6 +346,37 @@ export function extrudeSketch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loftSketches(
|
||||||
|
node: Node<Program>,
|
||||||
|
declarators: VariableDeclarator[]
|
||||||
|
): {
|
||||||
|
modifiedAst: Node<Program>
|
||||||
|
pathToNode: PathToNode
|
||||||
|
} {
|
||||||
|
const modifiedAst = structuredClone(node)
|
||||||
|
const name = findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.LOFT)
|
||||||
|
const elements = declarators.map((d) => createIdentifier(d.id.name))
|
||||||
|
const loft = createCallExpressionStdLib('loft', [
|
||||||
|
createArrayExpression(elements),
|
||||||
|
])
|
||||||
|
const declaration = createVariableDeclaration(name, loft)
|
||||||
|
modifiedAst.body.push(declaration)
|
||||||
|
const pathToNode: PathToNode = [
|
||||||
|
['body', ''],
|
||||||
|
[modifiedAst.body.length - 1, 'index'],
|
||||||
|
['declarations', 'VariableDeclaration'],
|
||||||
|
['0', 'index'],
|
||||||
|
['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,
|
||||||
|
@ -628,6 +628,18 @@ sketch002 = startSketchOn(extrude001, $seg01)
|
|||||||
const extrudable = doesSceneHaveSweepableSketch(ast)
|
const extrudable = doesSceneHaveSweepableSketch(ast)
|
||||||
expect(extrudable).toBeTruthy()
|
expect(extrudable).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
it('finds sketch001 and sketch002 pipes to be lofted', async () => {
|
||||||
|
const exampleCode = `sketch001 = startSketchOn('XZ')
|
||||||
|
|> circle({ center = [0, 0], radius = 1 }, %)
|
||||||
|
plane001 = offsetPlane('XZ', 2)
|
||||||
|
sketch002 = startSketchOn(plane001)
|
||||||
|
|> circle({ center = [0, 0], radius = 3 }, %)
|
||||||
|
`
|
||||||
|
const ast = parse(exampleCode)
|
||||||
|
if (err(ast)) throw ast
|
||||||
|
const extrudable = doesSceneHaveSweepableSketch(ast, 2)
|
||||||
|
expect(extrudable).toBeTruthy()
|
||||||
|
})
|
||||||
it('find sketch002 NOT pipe to be extruded', async () => {
|
it('find sketch002 NOT pipe to be extruded', async () => {
|
||||||
const exampleCode = `sketch001 = startSketchOn('XZ')
|
const exampleCode = `sketch001 = startSketchOn('XZ')
|
||||||
|> startProfileAt([3.29, 7.86], %)
|
|> startProfileAt([3.29, 7.86], %)
|
||||||
|
@ -975,7 +975,9 @@ export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
|
|||||||
if (
|
if (
|
||||||
node.type === 'CallExpression' &&
|
node.type === 'CallExpression' &&
|
||||||
node.callee.type === 'Identifier' &&
|
node.callee.type === 'Identifier' &&
|
||||||
(node.callee.name === 'extrude' || node.callee.name === 'revolve') &&
|
(node.callee.name === 'extrude' ||
|
||||||
|
node.callee.name === 'revolve' ||
|
||||||
|
node.callee.name === 'loft') &&
|
||||||
node.arguments?.[1]?.type === 'Identifier' &&
|
node.arguments?.[1]?.type === 'Identifier' &&
|
||||||
node.arguments[1].name === varDec.id.name
|
node.arguments[1].name === varDec.id.name
|
||||||
) {
|
) {
|
||||||
@ -988,7 +990,7 @@ export function hasSketchPipeBeenExtruded(selection: Selection, ast: Program) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** File must contain at least one sketch that has not been extruded already */
|
/** File must contain at least one sketch that has not been extruded already */
|
||||||
export function doesSceneHaveSweepableSketch(ast: Node<Program>) {
|
export function doesSceneHaveSweepableSketch(ast: Node<Program>, count = 1) {
|
||||||
const theMap: any = {}
|
const theMap: any = {}
|
||||||
traverse(ast as any, {
|
traverse(ast as any, {
|
||||||
enter(node) {
|
enter(node) {
|
||||||
@ -1037,7 +1039,7 @@ export function doesSceneHaveSweepableSketch(ast: Node<Program>) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return Object.keys(theMap).length > 0
|
return Object.keys(theMap).length >= count
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getObjExprProperty(
|
export function getObjExprProperty(
|
||||||
|
@ -31,6 +31,9 @@ export type ModelingCommandSchema = {
|
|||||||
// result: (typeof EXTRUSION_RESULTS)[number]
|
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||||
distance: KclCommandValue
|
distance: KclCommandValue
|
||||||
}
|
}
|
||||||
|
Loft: {
|
||||||
|
selection: Selections
|
||||||
|
}
|
||||||
Revolve: {
|
Revolve: {
|
||||||
selection: Selections
|
selection: Selections
|
||||||
angle: KclCommandValue
|
angle: KclCommandValue
|
||||||
@ -260,6 +263,20 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loft: {
|
||||||
|
description: 'Create a 3D body by blending between two or more sketches',
|
||||||
|
icon: 'loft',
|
||||||
|
needsReview: true,
|
||||||
|
args: {
|
||||||
|
selection: {
|
||||||
|
inputType: 'selection',
|
||||||
|
selectionTypes: ['solid2D'],
|
||||||
|
multiple: true,
|
||||||
|
required: true,
|
||||||
|
skip: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
|
// TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
|
||||||
Revolve: {
|
Revolve: {
|
||||||
description: 'Create a 3D body by rotating a sketch region about an axis.',
|
description: 'Create a 3D body by rotating a sketch region about an axis.',
|
||||||
|
@ -52,6 +52,7 @@ export const ONBOARDING_PROJECT_NAME = 'Tutorial Project $nn'
|
|||||||
export const KCL_DEFAULT_CONSTANT_PREFIXES = {
|
export const KCL_DEFAULT_CONSTANT_PREFIXES = {
|
||||||
SKETCH: 'sketch',
|
SKETCH: 'sketch',
|
||||||
EXTRUDE: 'extrude',
|
EXTRUDE: 'extrude',
|
||||||
|
LOFT: 'loft',
|
||||||
SEGMENT: 'seg',
|
SEGMENT: 'seg',
|
||||||
REVOLVE: 'revolve',
|
REVOLVE: 'revolve',
|
||||||
PLANE: 'plane',
|
PLANE: 'plane',
|
||||||
|
@ -529,6 +529,10 @@ function nodeHasExtrude(node: CommonASTNode) {
|
|||||||
doesPipeHaveCallExp({
|
doesPipeHaveCallExp({
|
||||||
calleeName: 'revolve',
|
calleeName: 'revolve',
|
||||||
...node,
|
...node,
|
||||||
|
}) ||
|
||||||
|
doesPipeHaveCallExp({
|
||||||
|
calleeName: 'loft',
|
||||||
|
...node,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -559,6 +563,22 @@ export function canSweepSelection(selection: Selections) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canLoftSelection(selection: Selections) {
|
||||||
|
const commonNodes = selection.graphSelections.map((_, i) =>
|
||||||
|
buildCommonNodeFromSelection(selection, i)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
!!isCursorInSketchCommandRange(
|
||||||
|
engineCommandManager.artifactGraph,
|
||||||
|
selection
|
||||||
|
) &&
|
||||||
|
commonNodes.length > 1 &&
|
||||||
|
commonNodes.every((n) => !hasSketchPipeBeenExtruded(n.selection, n.ast)) &&
|
||||||
|
commonNodes.every((n) => nodeHasClose(n) || nodeHasCircle(n)) &&
|
||||||
|
commonNodes.every((n) => !nodeHasExtrude(n))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// This accounts for non-geometry selections under "other"
|
// This accounts for non-geometry selections under "other"
|
||||||
export type ResolvedSelectionType = Artifact['type'] | 'other'
|
export type ResolvedSelectionType = Artifact['type'] | 'other'
|
||||||
export type SelectionCountsByType = Map<ResolvedSelectionType, number>
|
export type SelectionCountsByType = Map<ResolvedSelectionType, number>
|
||||||
|
@ -139,9 +139,14 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'loft',
|
id: 'loft',
|
||||||
onClick: () => console.error('Loft not yet implemented'),
|
onClick: ({ commandBarSend }) =>
|
||||||
|
commandBarSend({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: { name: 'Loft', groupId: 'modeling' },
|
||||||
|
}),
|
||||||
|
disabled: (state) => !state.can({ type: 'Loft' }),
|
||||||
icon: 'loft',
|
icon: 'loft',
|
||||||
status: 'kcl-only',
|
status: 'available',
|
||||||
title: 'Loft',
|
title: 'Loft',
|
||||||
hotkey: 'L',
|
hotkey: 'L',
|
||||||
description:
|
description:
|
||||||
|
@ -44,6 +44,7 @@ import {
|
|||||||
addOffsetPlane,
|
addOffsetPlane,
|
||||||
deleteFromSelection,
|
deleteFromSelection,
|
||||||
extrudeSketch,
|
extrudeSketch,
|
||||||
|
loftSketches,
|
||||||
revolveSketch,
|
revolveSketch,
|
||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import {
|
import {
|
||||||
@ -256,6 +257,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: 'Loft'; data?: ModelingCommandSchema['Loft'] }
|
||||||
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
|
| { type: 'Revolve'; data?: ModelingCommandSchema['Revolve'] }
|
||||||
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
|
| { type: 'Fillet'; data?: ModelingCommandSchema['Fillet'] }
|
||||||
| { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] }
|
| { type: 'Offset plane'; data: ModelingCommandSchema['Offset plane'] }
|
||||||
@ -387,6 +389,7 @@ export const modelingMachine = setup({
|
|||||||
guards: {
|
guards: {
|
||||||
'Selection is on face': () => false,
|
'Selection is on face': () => false,
|
||||||
'has valid sweep selection': () => false,
|
'has valid sweep selection': () => false,
|
||||||
|
'has valid loft selection': () => false,
|
||||||
'has valid edge treatment selection': () => false,
|
'has valid edge treatment selection': () => false,
|
||||||
'Has exportable geometry': () => false,
|
'Has exportable geometry': () => false,
|
||||||
'has valid selection for deletion': () => false,
|
'has valid selection for deletion': () => false,
|
||||||
@ -1529,6 +1532,50 @@ export const modelingMachine = setup({
|
|||||||
updateAstResult.newAst
|
updateAstResult.newAst
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (updateAstResult?.selections) {
|
||||||
|
editorManager.selectRange(updateAstResult?.selections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
loftAstMod: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: ModelingCommandSchema['Loft'] | undefined
|
||||||
|
}) => {
|
||||||
|
if (!input) return new Error('No input provided')
|
||||||
|
// Extract inputs
|
||||||
|
const ast = kclManager.ast
|
||||||
|
const { selection } = input
|
||||||
|
const declarators = selection.graphSelections.flatMap((s) => {
|
||||||
|
const path = getNodePathFromSourceRange(ast, s?.codeRef.range)
|
||||||
|
const nodeFromPath = getNodeFromPath<VariableDeclarator>(
|
||||||
|
ast,
|
||||||
|
path,
|
||||||
|
'VariableDeclarator'
|
||||||
|
)
|
||||||
|
return err(nodeFromPath) ? [] : nodeFromPath.node
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: add better validation on selection
|
||||||
|
if (!(declarators && declarators.length > 1)) {
|
||||||
|
trap('Not enough sketches selected')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the loft
|
||||||
|
const loftSketchesRes = loftSketches(ast, declarators)
|
||||||
|
const updateAstResult = await kclManager.updateAst(
|
||||||
|
loftSketchesRes.modifiedAst,
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
focusPath: [loftSketchesRes.pathToNode],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
await codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
updateAstResult.newAst
|
||||||
|
)
|
||||||
|
|
||||||
if (updateAstResult?.selections) {
|
if (updateAstResult?.selections) {
|
||||||
editorManager.selectRange(updateAstResult?.selections)
|
editorManager.selectRange(updateAstResult?.selections)
|
||||||
}
|
}
|
||||||
@ -1570,6 +1617,11 @@ export const modelingMachine = setup({
|
|||||||
reenter: false,
|
reenter: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Loft: {
|
||||||
|
target: 'Applying loft',
|
||||||
|
reenter: true,
|
||||||
|
},
|
||||||
|
|
||||||
Fillet: {
|
Fillet: {
|
||||||
target: 'idle',
|
target: 'idle',
|
||||||
guard: 'has valid edge treatment selection',
|
guard: 'has valid edge treatment selection',
|
||||||
@ -2318,6 +2370,19 @@ export const modelingMachine = setup({
|
|||||||
onError: ['idle'],
|
onError: ['idle'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'Applying loft': {
|
||||||
|
invoke: {
|
||||||
|
src: 'loftAstMod',
|
||||||
|
id: 'loftAstMod',
|
||||||
|
input: ({ event }) => {
|
||||||
|
if (event.type !== 'Loft') return undefined
|
||||||
|
return event.data
|
||||||
|
},
|
||||||
|
onDone: ['idle'],
|
||||||
|
onError: ['idle'],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
initial: 'idle',
|
initial: 'idle',
|
||||||
|