Multi-profile sweeps and more robust edit flows in point-and-click (#6437)

This commit is contained in:
Pierre Jacquier
2025-05-06 17:57:27 -04:00
committed by GitHub
parent 8fb1563f2d
commit 996517f5c4
15 changed files with 1353 additions and 928 deletions

View File

@ -10,6 +10,8 @@ test.describe('Command bar tests', () => {
test('Extrude from command bar selects extrude line after', async ({ test('Extrude from command bar selects extrude line after', async ({
page, page,
homePage, homePage,
toolbar,
cmdBar,
}) => { }) => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -35,20 +37,35 @@ test.describe('Command bar tests', () => {
// Click the line of code for xLine. // Click the line of code for xLine.
await page.getByText(`close()`).click() // TODO remove this and reinstate // await topHorzSegmentClick() await page.getByText(`close()`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Extrude' }).click() await toolbar.extrudeButton.click()
await page.waitForTimeout(200) await cmdBar.expectState({
await page.keyboard.press('Enter') stage: 'arguments',
await page.waitForTimeout(200) commandName: 'Extrude',
await page.keyboard.press('Enter') currentArgKey: 'sketches',
await page.waitForTimeout(200) currentArgValue: '',
headerArguments: {
Sketches: '',
Length: '',
},
highlightedHeaderArg: 'sketches',
})
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
commandName: 'Extrude',
headerArguments: {
Sketches: '1 segment',
Length: '5',
},
})
await cmdBar.progressCmdBar()
await expect(page.locator('.cm-activeLine')).toHaveText( await expect(page.locator('.cm-activeLine')).toHaveText(
`extrude001 = extrude(sketch001, length = ${KCL_DEFAULT_LENGTH})` `extrude001 = extrude(sketch001, length = ${KCL_DEFAULT_LENGTH})`
) )
}) })
// TODO: fix this test after the electron migration
test('Fillet from command bar', async ({ page, homePage }) => { test('Fillet from command bar', async ({ page, homePage }) => {
await page.addInitScript(async () => { await page.addInitScript(async () => {
localStorage.setItem( localStorage.setItem(
@ -269,21 +286,22 @@ test.describe('Command bar tests', () => {
await cmdBar.cmdOptions.getByText('Extrude').click() await cmdBar.cmdOptions.getByText('Extrude').click()
// Assert that we're on the selection step // Assert that we're on the selection step
await expect(page.getByRole('button', { name: 'selection' })).toBeDisabled() await expect(page.getByRole('button', { name: 'sketches' })).toBeDisabled()
// Select a face // Select a face
await page.mouse.move(700, 200) await page.mouse.move(700, 200)
await page.mouse.click(700, 200) await page.mouse.click(700, 200)
await cmdBar.progressCmdBar()
// Assert that we're on the distance step // Assert that we're on the distance step
await expect( await expect(
page.getByRole('button', { name: 'distance', exact: false }) page.getByRole('button', { name: 'length', exact: false })
).toBeDisabled() ).toBeDisabled()
// Assert that the an alternative variable name is chosen, // Assert that the an alternative variable name is chosen,
// since the default variable name is already in use (distance) // since the default variable name is already in use (distance)
await page.getByRole('button', { name: 'Create new variable' }).click() await page.getByRole('button', { name: 'Create new variable' }).click()
await expect(page.getByPlaceholder('Variable name')).toHaveValue( await expect(page.getByPlaceholder('Variable name')).toHaveValue(
'distance001' 'length001'
) )
const continueButton = page.getByRole('button', { name: 'Continue' }) const continueButton = page.getByRole('button', { name: 'Continue' })
@ -297,7 +315,7 @@ test.describe('Command bar tests', () => {
// Assert we're back on the distance step // Assert we're back on the distance step
await expect( await expect(
page.getByRole('button', { name: 'distance', exact: false }) page.getByRole('button', { name: 'length', exact: false })
).toBeDisabled() ).toBeDisabled()
await continueButton.click() await continueButton.click()
@ -306,7 +324,7 @@ test.describe('Command bar tests', () => {
await u.waitForCmdReceive('extrude') await u.waitForCmdReceive('extrude')
await expect(page.locator('.cm-content')).toContainText( await expect(page.locator('.cm-content')).toContainText(
'extrude001 = extrude(sketch001, length = distance001)' 'extrude001 = extrude(sketch001, length = length001)'
) )
}) })

View File

@ -1085,6 +1085,9 @@ sketch001 = startSketchOn(XZ)
page, page,
context, context,
homePage, homePage,
toolbar,
cmdBar,
scene,
}) => { }) => {
const u = await getUtils(page) const u = await getUtils(page)
await context.addInitScript(async () => { await context.addInitScript(async () => {
@ -1128,17 +1131,30 @@ sketch001 = startSketchOn(XZ)
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.getByText('startProfile(at = [4.61, -14.01])').click() await page.getByText('startProfile(at = [4.61, -14.01])').click()
await expect(page.getByRole('button', { name: 'Extrude' })).toBeVisible() await toolbar.extrudeButton.click()
await page.getByRole('button', { name: 'Extrude' }).click() await cmdBar.progressCmdBar()
await cmdBar.expectState({
await expect(page.getByTestId('command-bar')).toBeVisible() stage: 'arguments',
await page.waitForTimeout(100) currentArgKey: 'length',
currentArgValue: '5',
await page.getByRole('button', { name: 'arrow right Continue' }).click() headerArguments: {
await page.waitForTimeout(100) Sketches: '1 face',
await expect(page.getByText('Confirm Extrude')).toBeVisible() Length: '',
await page.getByRole('button', { name: 'checkmark Submit command' }).click() },
await page.waitForTimeout(100) highlightedHeaderArg: 'length',
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Sketches: '1 face',
Length: '5',
},
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await scene.settled(cmdBar)
// expect the code to have changed // expect the code to have changed
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(

View File

@ -229,7 +229,7 @@ test.describe('Feature Tree pane', () => {
const initialCode = `sketch001 = startSketchOn(XZ) const initialCode = `sketch001 = startSketchOn(XZ)
|> circle(center = [0, 0], radius = 5) |> circle(center = [0, 0], radius = 5)
renamedExtrude = extrude(sketch001, length = ${initialInput})` renamedExtrude = extrude(sketch001, length = ${initialInput})`
const newConstantName = 'distance001' const newConstantName = 'length001'
const expectedCode = `${newConstantName} = 23 const expectedCode = `${newConstantName} = 23
sketch001 = startSketchOn(XZ) sketch001 = startSketchOn(XZ)
|> circle(center = [0, 0], radius = 5) |> circle(center = [0, 0], radius = 5)
@ -270,12 +270,12 @@ test.describe('Feature Tree pane', () => {
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Extrude', commandName: 'Extrude',
stage: 'arguments', stage: 'arguments',
currentArgKey: 'distance', currentArgKey: 'length',
currentArgValue: initialInput, currentArgValue: initialInput,
headerArguments: { headerArguments: {
Distance: initialInput, Length: initialInput,
}, },
highlightedHeaderArg: 'distance', highlightedHeaderArg: 'length',
}) })
}) })
@ -290,7 +290,7 @@ test.describe('Feature Tree pane', () => {
stage: 'review', stage: 'review',
headerArguments: { headerArguments: {
// The calculated value is shown in the argument summary // The calculated value is shown in the argument summary
Distance: initialInput, Length: initialInput,
}, },
commandName: 'Extrude', commandName: 'Extrude',
}) })

View File

@ -6,6 +6,7 @@ import type { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture' import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture' import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
import { expect, test } from '@e2e/playwright/zoo-test' import { expect, test } from '@e2e/playwright/zoo-test'
import { bracket } from '@e2e/playwright/fixtures/bracket'
// test file is for testing point an click code gen functionality that's not sketch mode related // test file is for testing point an click code gen functionality that's not sketch mode related
@ -75,10 +76,19 @@ test.describe('Point-and-click tests', () => {
await toolbar.extrudeButton.click() await toolbar.extrudeButton.click()
await cmdBar.expectState({ await cmdBar.expectState({
stage: 'arguments', stage: 'arguments',
currentArgKey: 'distance', currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: { Sketches: '', Length: '' },
highlightedHeaderArg: 'sketches',
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: '5', currentArgValue: '5',
headerArguments: { Selection: '1 face', Distance: '' }, headerArguments: { Sketches: '1 face', Length: '' },
highlightedHeaderArg: 'distance', highlightedHeaderArg: 'length',
commandName: 'Extrude', commandName: 'Extrude',
}) })
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
@ -88,7 +98,7 @@ test.describe('Point-and-click tests', () => {
await cmdBar.expectState({ await cmdBar.expectState({
stage: 'review', stage: 'review',
headerArguments: { Selection: '1 face', Distance: '5' }, headerArguments: { Sketches: '1 face', Length: '5' },
commandName: 'Extrude', commandName: 'Extrude',
}) })
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
@ -97,6 +107,102 @@ test.describe('Point-and-click tests', () => {
}) })
}) })
test('Verify in-pipe extrudes in bracket can be edited', async ({
tronApp,
context,
editor,
homePage,
page,
scene,
toolbar,
cmdBar,
}) => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, bracket)
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await test.step(`Edit first extrude via feature tree`, async () => {
await (await toolbar.getFeatureTreeOperation('Extrude', 0)).dblclick()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: 'width',
headerArguments: {
Length: '5',
},
highlightedHeaderArg: 'length',
commandName: 'Extrude',
})
await page.keyboard.insertText('width - 0.001')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Length: '4.999',
},
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await editor.expectEditor.toContain('extrude(length = width - 0.001)')
})
await test.step(`Edit second extrude via feature tree`, async () => {
await (await toolbar.getFeatureTreeOperation('Extrude', 1)).dblclick()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: '-thickness - .01',
headerArguments: {
Length: '-0.3949',
},
highlightedHeaderArg: 'length',
commandName: 'Extrude',
})
await page.keyboard.insertText('-thickness - .01 - 0.001')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Length: '-0.3959',
},
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await editor.expectEditor.toContain(
'extrude(length = -thickness - .01 - 0.001)'
)
})
await test.step(`Edit third extrude via feature tree`, async () => {
await (await toolbar.getFeatureTreeOperation('Extrude', 2)).dblclick()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: '-thickness - 0.1',
headerArguments: {
Length: '-0.4849',
},
highlightedHeaderArg: 'length',
commandName: 'Extrude',
})
await page.keyboard.insertText('-thickness - 0.1 - 0.001')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Length: '-0.4859',
},
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await editor.expectEditor.toContain(
'extrude(length = -thickness - 0.1 - 0.001)'
)
})
})
test.describe('verify sketch on chamfer works', () => { test.describe('verify sketch on chamfer works', () => {
const _sketchOnAChamfer = const _sketchOnAChamfer =
( (
@ -1483,11 +1589,11 @@ extrude001 = extrude(profile001, length = 100)
cmdBar, cmdBar,
}) => { }) => {
const initialCode = `sketch001 = startSketchOn(XZ) const initialCode = `sketch001 = startSketchOn(XZ)
|> circle(center = [0, 0], radius = 30) |> circle(center = [0, 0], radius = 30)
plane001 = offsetPlane(XZ, offset = 50) plane001 = offsetPlane(XZ, offset = 50)
sketch002 = startSketchOn(plane001) sketch002 = startSketchOn(plane001)
|> circle(center = [0, 0], radius = 20) |> circle(center = [0, 0], radius = 20)
` `
await context.addInitScript((initialCode) => { await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode) localStorage.setItem('persistCode', initialCode)
}, initialCode) }, initialCode)
@ -1523,14 +1629,20 @@ extrude001 = extrude(profile001, length = 100)
.toBe(1) .toBe(1)
await cmdBar.expectState({ await cmdBar.expectState({
stage: 'arguments', stage: 'arguments',
currentArgKey: 'selection', currentArgKey: 'sketches',
currentArgValue: '', currentArgValue: '',
headerArguments: { Selection: '' }, headerArguments: { Sketches: '' },
highlightedHeaderArg: 'selection', highlightedHeaderArg: 'sketches',
commandName: 'Loft', commandName: 'Loft',
}) })
await selectSketches() await selectSketches()
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Sketches: '2 faces' },
commandName: 'Loft',
})
await cmdBar.submit()
}) })
} else { } else {
await test.step(`Preselect the two sketches`, async () => { await test.step(`Preselect the two sketches`, async () => {
@ -1539,7 +1651,21 @@ extrude001 = extrude(profile001, length = 100)
await test.step(`Go through the command bar flow with preselected sketches`, async () => { await test.step(`Go through the command bar flow with preselected sketches`, async () => {
await toolbar.loftButton.click() await toolbar.loftButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: { Sketches: '' },
highlightedHeaderArg: 'sketches',
commandName: 'Loft',
})
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: { Sketches: '2 faces' },
commandName: 'Loft',
})
await cmdBar.submit()
}) })
} }
@ -1681,8 +1807,7 @@ sketch002 = startSketchOn(XZ)
testPoint.x - 50, testPoint.x - 50,
testPoint.y testPoint.y
) )
const sweepDeclaration = const sweepDeclaration = 'sweep001 = sweep(profile001, path = sketch002)'
'sweep001 = sweep(profile001, path = sketch002, sectional = false)'
const editedSweepDeclaration = const editedSweepDeclaration =
'sweep001 = sweep(profile001, path = sketch002, sectional = true)' 'sweep001 = sweep(profile001, path = sketch002, sectional = true)'
@ -1698,37 +1823,49 @@ sketch002 = startSketchOn(XZ)
.toBe(1) .toBe(1)
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Sweep', commandName: 'Sweep',
currentArgKey: 'target', currentArgKey: 'sketches',
currentArgValue: '', currentArgValue: '',
headerArguments: { headerArguments: {
Sectional: '', Sectional: '',
Target: '', Sketches: '',
Trajectory: '', Path: '',
}, },
highlightedHeaderArg: 'target', highlightedHeaderArg: 'sketches',
stage: 'arguments', stage: 'arguments',
}) })
await clickOnSketch1() await clickOnSketch1()
await cmdBar.progressCmdBar()
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Sweep', commandName: 'Sweep',
currentArgKey: 'trajectory', currentArgKey: 'path',
currentArgValue: '', currentArgValue: '',
headerArguments: { headerArguments: {
Sectional: '', Sectional: '',
Target: '1 face', Sketches: '1 face',
Trajectory: '', Path: '',
}, },
highlightedHeaderArg: 'trajectory', highlightedHeaderArg: 'path',
stage: 'arguments', stage: 'arguments',
}) })
await clickOnSketch2() await clickOnSketch2()
await page.waitForTimeout(500) await cmdBar.expectState({
commandName: 'Sweep',
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Sectional: '',
Sketches: '1 face',
Path: '',
},
highlightedHeaderArg: 'path',
stage: 'arguments',
})
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Sweep', commandName: 'Sweep',
headerArguments: { headerArguments: {
Target: '1 face', Sketches: '1 face',
Trajectory: '1 segment', Path: '1 segment',
Sectional: '', Sectional: '',
}, },
stage: 'review', stage: 'review',
@ -1837,31 +1974,31 @@ sketch002 = startSketchOn(XZ)
.toBe(1) .toBe(1)
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Sweep', commandName: 'Sweep',
currentArgKey: 'target', currentArgKey: 'sketches',
currentArgValue: '', currentArgValue: '',
headerArguments: { headerArguments: {
Sectional: '', Sectional: '',
Target: '', Sketches: '',
Trajectory: '', Path: '',
}, },
highlightedHeaderArg: 'target', highlightedHeaderArg: 'sketches',
stage: 'arguments', stage: 'arguments',
}) })
await clickOnSketch1() await clickOnSketch1()
await cmdBar.progressCmdBar()
await cmdBar.expectState({ await cmdBar.expectState({
commandName: 'Sweep', commandName: 'Sweep',
currentArgKey: 'trajectory', currentArgKey: 'path',
currentArgValue: '', currentArgValue: '',
headerArguments: { headerArguments: {
Sectional: '', Sectional: '',
Target: '1 face', Sketches: '1 face',
Trajectory: '', Path: '',
}, },
highlightedHeaderArg: 'trajectory', highlightedHeaderArg: 'path',
stage: 'arguments', stage: 'arguments',
}) })
await clickOnSketch2() await clickOnSketch2()
await page.waitForTimeout(500)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await expect( await expect(
page.getByText('Unable to sweep with the current selection. Reason:') page.getByText('Unable to sweep with the current selection. Reason:')
@ -3513,6 +3650,7 @@ tag=$rectangleSegmentC002,
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar()
const newCodeToFind = `revolve001 = revolve(sketch002, angle = 360, axis = X)` const newCodeToFind = `revolve001 = revolve(sketch002, angle = 360, axis = X)`
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy() expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
@ -3585,6 +3723,7 @@ sketch002 = startSketchOn(extrude001, face = rectangleSegmentA001)
await editor.scrollToText(codeToSelection) await editor.scrollToText(codeToSelection)
await page.getByText(codeToSelection).click() await page.getByText(codeToSelection).click()
await toolbar.revolveButton.click() await toolbar.revolveButton.click()
await cmdBar.progressCmdBar()
await page.getByText('Edge', { exact: true }).click() await page.getByText('Edge', { exact: true }).click()
const lineCodeToSelection = `angledLine(angle = 0, length = 202.6, tag = $rectangleSegmentA001)` const lineCodeToSelection = `angledLine(angle = 0, length = 202.6, tag = $rectangleSegmentA001)`
await page.getByText(lineCodeToSelection).click() await page.getByText(lineCodeToSelection).click()
@ -3677,6 +3816,7 @@ sketch002 = startSketchOn(extrude001, face = rectangleSegmentA001)
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
await editor.scrollToText(codeToSelection) await editor.scrollToText(codeToSelection)
await page.getByText(codeToSelection).click() await page.getByText(codeToSelection).click()
await cmdBar.progressCmdBar()
await expect.poll(() => page.getByText('AxisOrEdge').count()).toBe(2) await expect.poll(() => page.getByText('AxisOrEdge').count()).toBe(2)
await page.getByText('Edge', { exact: true }).click() await page.getByText('Edge', { exact: true }).click()
const lineCodeToSelection = `length = 2.6` const lineCodeToSelection = `length = 2.6`
@ -4393,4 +4533,319 @@ extrude001 = extrude(profile001, length = 1)
) )
}) })
}) })
const multiProfileSweepsCode = `sketch001 = startSketchOn(XY)
profile001 = circle(sketch001, center = [3, 0], radius = 1)
profile002 = circle(sketch001, center = [6, 0], radius = 1)
path001 = startProfile(sketch001, at = [0, 0])
|> yLine(length = 2)
`
const profile001Point = { x: 470, y: 270 }
const profile002Point = { x: 670, y: 270 }
test('Point-and-click multi-profile sweeps: extrude', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, multiProfileSweepsCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await test.step('Select through scene', async () => {
// Unfortunately can't select thru code for multi profile yet
const [clickProfile001Point] = scene.makeMouseHelpers(
profile001Point.x,
profile001Point.y
)
const [clickProfile002Point] = scene.makeMouseHelpers(
profile002Point.x,
profile002Point.y
)
await toolbar.closePane('code')
await clickProfile001Point()
await page.keyboard.down('Shift')
await clickProfile002Point()
await page.waitForTimeout(500)
await page.keyboard.up('Shift')
})
await test.step('Go through command bar flow', async () => {
await toolbar.extrudeButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sketches: '',
Length: '',
},
highlightedHeaderArg: 'sketches',
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'length',
currentArgValue: '5',
headerArguments: {
Sketches: '2 faces',
Length: '',
},
highlightedHeaderArg: 'length',
commandName: 'Extrude',
})
await page.keyboard.insertText('1')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Sketches: '2 faces',
Length: '1',
},
commandName: 'Extrude',
})
await cmdBar.progressCmdBar()
await scene.settled(cmdBar)
await toolbar.openPane('code')
await editor.expectEditor.toContain(
`extrude001 = extrude([profile001, profile002], length = 1)`,
{ shouldNormalise: true }
)
await editor.closePane()
})
await test.step('Delete extrude via feature tree selection', async () => {
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-delete').click()
await scene.settled(cmdBar)
await toolbar.closePane('feature-tree')
await toolbar.openPane('code')
await editor.expectEditor.not.toContain(
`extrude001 = extrude([profile001, profile002], length = 1)`,
{ shouldNormalise: true }
)
})
})
test('Point-and-click multi-profile sweeps: sweep', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, multiProfileSweepsCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await test.step('Select through scene', async () => {
// Unfortunately can't select thru code for multi profile yet
const [clickProfile001Point] = scene.makeMouseHelpers(
profile001Point.x,
profile001Point.y
)
const [clickProfile002Point] = scene.makeMouseHelpers(
profile002Point.x,
profile002Point.y
)
await toolbar.closePane('code')
await clickProfile001Point()
await page.keyboard.down('Shift')
await clickProfile002Point()
await page.waitForTimeout(500)
await page.keyboard.up('Shift')
})
await test.step('Go through command bar flow', async () => {
await toolbar.sweepButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sketches: '',
Path: '',
Sectional: '',
},
highlightedHeaderArg: 'sketches',
commandName: 'Sweep',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'path',
currentArgValue: '',
headerArguments: {
Sketches: '2 faces',
Path: '',
Sectional: '',
},
highlightedHeaderArg: 'path',
commandName: 'Sweep',
})
await toolbar.openPane('code')
await page.getByText('yLine(length = 2)').click()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Sketches: '2 faces',
Path: '1 segment',
Sectional: '',
},
commandName: 'Sweep',
})
await cmdBar.progressCmdBar()
await scene.settled(cmdBar)
await toolbar.openPane('code')
await editor.expectEditor.toContain(
`sweep001 = sweep([profile001, profile002], path = path001)`,
{ shouldNormalise: true }
)
})
await test.step('Delete sweep via feature tree selection', async () => {
await editor.closePane()
const op = await toolbar.getFeatureTreeOperation('Sweep', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-delete').click()
await scene.settled(cmdBar)
await editor.expectEditor.not.toContain(
`sweep001 = sweep([profile001, profile002], path = path001)`,
{ shouldNormalise: true }
)
})
})
test('Point-and-click multi-profile sweeps: revolve', async ({
context,
page,
homePage,
scene,
editor,
toolbar,
cmdBar,
}) => {
await context.addInitScript((initialCode) => {
localStorage.setItem('persistCode', initialCode)
}, multiProfileSweepsCode)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await scene.settled(cmdBar)
await test.step('Select through scene', async () => {
// Unfortunately can't select thru code for multi profile yet
const [clickProfile001Point] = scene.makeMouseHelpers(
profile001Point.x,
profile001Point.y
)
const [clickProfile002Point] = scene.makeMouseHelpers(
profile002Point.x,
profile002Point.y
)
await toolbar.closePane('code')
await clickProfile001Point()
await page.keyboard.down('Shift')
await clickProfile002Point()
await page.waitForTimeout(500)
await page.keyboard.up('Shift')
})
await test.step('Go through command bar flow', async () => {
await toolbar.closePane('code')
await toolbar.revolveButton.click()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'sketches',
currentArgValue: '',
headerArguments: {
Sketches: '',
AxisOrEdge: '',
Angle: '',
},
highlightedHeaderArg: 'sketches',
commandName: 'Revolve',
})
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'axisOrEdge',
currentArgValue: '',
headerArguments: {
Sketches: '2 faces',
AxisOrEdge: '',
Angle: '',
},
highlightedHeaderArg: 'axisOrEdge',
commandName: 'Revolve',
})
await cmdBar.selectOption({ name: 'Edge' }).click()
await toolbar.openPane('code')
await page.getByText('yLine(length = 2)').click()
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'arguments',
currentArgKey: 'angle',
currentArgValue: '360',
headerArguments: {
Sketches: '2 faces',
AxisOrEdge: 'Edge',
Edge: '1 segment',
Angle: '',
},
highlightedHeaderArg: 'angle',
commandName: 'Revolve',
})
await page.keyboard.insertText('180')
await cmdBar.progressCmdBar()
await cmdBar.expectState({
stage: 'review',
headerArguments: {
Sketches: '2 faces',
AxisOrEdge: 'Edge',
Edge: '1 segment',
Angle: '180',
},
commandName: 'Revolve',
})
await cmdBar.progressCmdBar()
await scene.settled(cmdBar)
await editor.expectEditor.toContain(`yLine(length = 2, tag = $seg01)`, {
shouldNormalise: true,
})
await editor.expectEditor.toContain(
`revolve001 = revolve([profile001, profile002], angle=180, axis=seg01)`,
{ shouldNormalise: true }
)
})
await test.step('Delete revolve via feature tree selection', async () => {
await editor.closePane()
const op = await toolbar.getFeatureTreeOperation('Revolve', 0)
await op.click({ button: 'right' })
await page.getByTestId('context-menu-delete').click()
await scene.settled(cmdBar)
await editor.expectEditor.not.toContain(
`revolve001 = revolve([profile001, profile002], axis = XY, angle = 180)`,
{ shouldNormalise: true }
)
})
})
}) })

View File

@ -962,6 +962,8 @@ profile001 = startProfile(sketch001, at = [${roundOff(scale * 69.6)}, ${roundOff
test('exiting a close extrude, has the extrude button enabled ready to go', async ({ test('exiting a close extrude, has the extrude button enabled ready to go', async ({
page, page,
homePage, homePage,
cmdBar,
toolbar,
}) => { }) => {
// this was a regression https://github.com/KittyCAD/modeling-app/issues/2832 // this was a regression https://github.com/KittyCAD/modeling-app/issues/2832
await page.addInitScript(async () => { await page.addInitScript(async () => {
@ -1002,19 +1004,21 @@ profile001 = startProfile(sketch001, at = [${roundOff(scale * 69.6)}, ${roundOff
await page.getByRole('button', { name: 'Exit Sketch' }).click() await page.getByRole('button', { name: 'Exit Sketch' }).click()
// expect extrude button to be enabled // expect extrude button to be enabled
await expect( await expect(toolbar.extrudeButton).not.toBeDisabled()
page.getByRole('button', { name: 'Extrude' })
).not.toBeDisabled()
// click extrude // click extrude
await page.getByRole('button', { name: 'Extrude' }).click() await toolbar.extrudeButton.click()
// sketch selection should already have been made. "Selection: 1 face" only show up when the selection has been made already // sketch selection should already have been made. "Sketches: 1 face" only show up when the selection has been made already
// otherwise the cmdbar would be waiting for a selection. // otherwise the cmdbar would be waiting for a selection.
await expect( await cmdBar.progressCmdBar()
page.getByRole('button', { name: 'selection : 1 segment', exact: false }) await cmdBar.expectState({
).toBeVisible({ stage: 'arguments',
timeout: 10_000, currentArgKey: 'length',
currentArgValue: '5',
headerArguments: { Sketches: '1 segment', Length: '' },
highlightedHeaderArg: 'length',
commandName: 'Extrude',
}) })
}) })
test("Existing sketch with bad code delete user's code", async ({ test("Existing sketch with bad code delete user's code", async ({

View File

@ -573,6 +573,7 @@ profile001 = startProfile(sketch002, at = [-12.34, 12.34])
await expect(page.getByTestId('command-bar')).toBeVisible() await expect(page.getByTestId('command-bar')).toBeVisible()
await page.waitForTimeout(100) await page.waitForTimeout(100)
await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await expect(page.getByText('Confirm Extrude')).toBeVisible() await expect(page.getByText('Confirm Extrude')).toBeVisible()
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()

1
package-lock.json generated
View File

@ -2492,7 +2492,6 @@
}, },
"node_modules/@clack/prompts/node_modules/is-unicode-supported": { "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0", "version": "1.3.0",
"extraneous": true,
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {

View File

@ -31,8 +31,6 @@ import {
UNLABELED_ARG, UNLABELED_ARG,
} from '@src/lang/queryAstConstants' } from '@src/lang/queryAstConstants'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import type { Artifact } from '@src/lang/std/artifactGraph'
import { getPathsFromArtifact } from '@src/lang/std/artifactGraph'
import { import {
addTagForSketchOnFace, addTagForSketchOnFace,
getConstraintInfoKw, getConstraintInfoKw,
@ -46,7 +44,6 @@ import {
import type { SimplifiedArgDetails } from '@src/lang/std/stdTypes' import type { SimplifiedArgDetails } from '@src/lang/std/stdTypes'
import type { import type {
ArrayExpression, ArrayExpression,
ArtifactGraph,
CallExpressionKw, CallExpressionKw,
Expr, Expr,
Literal, Literal,
@ -340,122 +337,6 @@ export function mutateObjExpProp(
return false return false
} }
export function extrudeSketch({
node,
pathToNode,
distance = createLiteral(4),
extrudeName,
artifact,
artifactGraph,
}: {
node: Node<Program>
pathToNode: PathToNode
distance: Expr
extrudeName?: string
artifactGraph: ArtifactGraph
artifact?: Artifact
}):
| {
modifiedAst: Node<Program>
pathToNode: PathToNode
pathToExtrudeArg: PathToNode
}
| Error {
const orderedSketchNodePaths = getPathsFromArtifact({
artifact: artifact,
sketchPathToNode: pathToNode,
artifactGraph,
ast: node,
})
if (err(orderedSketchNodePaths)) return orderedSketchNodePaths
const _node = structuredClone(node)
const _node1 = getNodeFromPath(_node, pathToNode)
if (err(_node1)) return _node1
// determine if sketchExpression is in a pipeExpression or not
const _node2 = getNodeFromPath<PipeExpression>(
_node,
pathToNode,
'PipeExpression'
)
if (err(_node2)) return _node2
const _node3 = getNodeFromPath<VariableDeclarator>(
_node,
pathToNode,
'VariableDeclarator'
)
if (err(_node3)) return _node3
const { node: variableDeclarator } = _node3
const extrudeCall = createCallExpressionStdLibKw(
'extrude',
createLocalName(variableDeclarator.id.name),
[createLabeledArg('length', distance)]
)
// index of the 'length' arg above. If you reorder the labeled args above,
// make sure to update this too.
const argIndex = 0
// We're not creating a pipe expression,
// but rather a separate constant for the extrusion
const name =
extrudeName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE)
const VariableDeclaration = createVariableDeclaration(name, extrudeCall)
const lastSketchNodePath =
orderedSketchNodePaths[orderedSketchNodePaths.length - 1]
const sketchIndexInBody = Number(lastSketchNodePath[1][0])
_node.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
const pathToExtrudeArg: PathToNode = [
['body', ''],
[sketchIndexInBody + 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpressionKw'],
[argIndex, ARG_INDEX_FIELD],
['arg', LABELED_ARG_FIELD],
]
return {
modifiedAst: _node,
pathToNode: [...pathToNode.slice(0, -1), [-1, 'index']],
pathToExtrudeArg,
}
}
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) => createLocalName(d.id.name))
const loft = createCallExpressionStdLibKw(
'loft',
createArrayExpression(elements),
[]
)
const declaration = createVariableDeclaration(name, loft)
modifiedAst.body.push(declaration)
const pathToNode: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['unlabeled', UNLABELED_ARG],
]
return {
modifiedAst,
pathToNode,
}
}
export function addShell({ export function addShell({
node, node,
sweepName, sweepName,
@ -514,63 +395,6 @@ export function addShell({
} }
} }
export function addSweep({
node,
targetDeclarator,
trajectoryDeclarator,
sectional,
variableName,
insertIndex,
}: {
node: Node<Program>
targetDeclarator: VariableDeclarator
trajectoryDeclarator: VariableDeclarator
sectional: boolean
variableName?: string
insertIndex?: number
}): {
modifiedAst: Node<Program>
pathToNode: PathToNode
} {
const modifiedAst = structuredClone(node)
const name =
variableName ?? findUniqueName(node, KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP)
const call = createCallExpressionStdLibKw(
'sweep',
createLocalName(targetDeclarator.id.name),
[
createLabeledArg('path', createLocalName(trajectoryDeclarator.id.name)),
createLabeledArg('sectional', createLiteral(sectional)),
]
)
const variable = createVariableDeclaration(name, call)
const insertAt =
insertIndex !== undefined
? insertIndex
: modifiedAst.body.length
? modifiedAst.body.length
: 0
modifiedAst.body.length
? modifiedAst.body.splice(insertAt, 0, variable)
: modifiedAst.body.push(variable)
const argIndex = 0
const pathToNode: PathToNode = [
['body', ''],
[insertAt, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpressionKw'],
[argIndex, ARG_INDEX_FIELD],
['arg', LABELED_ARG_FIELD],
]
return {
modifiedAst,
pathToNode,
}
}
export function sketchOnExtrudedFace( export function sketchOnExtrudedFace(
node: Node<Program>, node: Node<Program>,
sketchPathToNode: PathToNode, sketchPathToNode: PathToNode,
@ -1345,7 +1169,7 @@ export function createNodeFromExprSnippet(
export function insertVariableAndOffsetPathToNode( export function insertVariableAndOffsetPathToNode(
variable: KclCommandValue, variable: KclCommandValue,
modifiedAst: Node<Program>, modifiedAst: Node<Program>,
pathToNode: PathToNode pathToNode?: PathToNode
) { ) {
if ('variableName' in variable && variable.variableName) { if ('variableName' in variable && variable.variableName) {
modifiedAst.body.splice( modifiedAst.body.splice(
@ -1353,7 +1177,7 @@ export function insertVariableAndOffsetPathToNode(
0, 0,
variable.variableDeclarationAst variable.variableDeclarationAst
) )
if (typeof pathToNode[1][0] === 'number') { if (pathToNode && pathToNode[1] && typeof pathToNode[1][0] === 'number') {
pathToNode[1][0]++ pathToNode[1][0]++
} }
} }

View File

@ -1,142 +0,0 @@
import type { Node } from '@rust/kcl-lib/bindings/Node'
import {
createCallExpressionStdLibKw,
createLabeledArg,
createLocalName,
createVariableDeclaration,
findUniqueName,
} from '@src/lang/create'
import {
getEdgeTagCall,
mutateAstWithTagForSketchSegment,
} from '@src/lang/modifyAst/addEdgeTreatment'
import { getNodeFromPath } from '@src/lang/queryAst'
import { getSafeInsertIndex } from '@src/lang/queryAst/getSafeInsertIndex'
import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import type {
Expr,
PathToNode,
Program,
VariableDeclarator,
} from '@src/lang/wasm'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from '@src/lib/constants'
import type { Selections } from '@src/lib/selections'
import { err } from '@src/lib/trap'
export function getAxisExpressionAndIndex(
axisOrEdge: 'Axis' | 'Edge',
axis: string | undefined,
edge: Selections | undefined,
ast: Node<Program>
) {
let generatedAxis
let axisDeclaration: PathToNode | null = null
let axisIndexIfAxis: number | undefined = undefined
if (axisOrEdge === 'Edge' && edge) {
const pathToAxisSelection = getNodePathFromSourceRange(
ast,
edge.graphSelections[0]?.codeRef.range
)
const tagResult = mutateAstWithTagForSketchSegment(ast, pathToAxisSelection)
// Have the tag whether it is already created or a new one is generated
if (err(tagResult)) return tagResult
const { tag } = tagResult
const axisSelection = edge?.graphSelections[0]?.artifact
if (!axisSelection) return new Error('Generated axis selection is missing.')
generatedAxis = getEdgeTagCall(tag, axisSelection)
if (
axisSelection.type === 'segment' ||
axisSelection.type === 'path' ||
axisSelection.type === 'edgeCut'
) {
axisDeclaration = axisSelection.codeRef.pathToNode
if (!axisDeclaration)
return new Error('Expected to fine axis declaration')
const axisIndexInPathToNode =
axisDeclaration.findIndex((a) => a[0] === 'body') + 1
const value = axisDeclaration[axisIndexInPathToNode][0]
if (typeof value !== 'number')
return new Error('expected axis index value to be a number')
axisIndexIfAxis = value
}
} else if (axisOrEdge === 'Axis' && axis) {
generatedAxis = createLocalName(axis)
}
return {
generatedAxis,
axisIndexIfAxis,
}
}
export function revolveSketch(
ast: Node<Program>,
pathToSketchNode: PathToNode,
angle: Expr,
axisOrEdge: 'Axis' | 'Edge',
axis: string | undefined,
edge: Selections | undefined,
variableName?: string,
insertIndex?: number
):
| {
modifiedAst: Node<Program>
pathToSketchNode: PathToNode
pathToRevolveArg: PathToNode
}
| Error {
const clonedAst = structuredClone(ast)
const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>(
clonedAst,
pathToSketchNode,
'VariableDeclarator'
)
if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode
const { node: sketchVariableDeclarator } = sketchVariableDeclaratorNode
const getAxisResult = getAxisExpressionAndIndex(
axisOrEdge,
axis,
edge,
clonedAst
)
if (err(getAxisResult)) return getAxisResult
const { generatedAxis } = getAxisResult
if (!generatedAxis) return new Error('Generated axis selection is missing.')
const revolveCall = createCallExpressionStdLibKw(
'revolve',
createLocalName(sketchVariableDeclarator.id.name),
[createLabeledArg('angle', angle), createLabeledArg('axis', generatedAxis)]
)
// We're not creating a pipe expression,
// but rather a separate constant for the extrusion
const name =
variableName ??
findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
const variableDeclaration = createVariableDeclaration(name, revolveCall)
const bodyInsertIndex =
insertIndex ?? getSafeInsertIndex(revolveCall, clonedAst)
clonedAst.body.splice(bodyInsertIndex, 0, variableDeclaration)
const argIndex = 0
const pathToRevolveArg: PathToNode = [
['body', ''],
[bodyInsertIndex, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpressionKw'],
[argIndex, ARG_INDEX_FIELD],
['arg', LABELED_ARG_FIELD],
]
return {
modifiedAst: clonedAst,
pathToSketchNode: [...pathToSketchNode.slice(0, -1), [-1, 'index']],
pathToRevolveArg,
}
}

View File

@ -0,0 +1,406 @@
import type { Node } from '@rust/kcl-lib/bindings/Node'
import {
createArrayExpression,
createCallExpressionStdLibKw,
createLabeledArg,
createLiteral,
createLocalName,
createVariableDeclaration,
findUniqueName,
} from '@src/lang/create'
import { insertVariableAndOffsetPathToNode } from '@src/lang/modifyAst'
import {
getEdgeTagCall,
mutateAstWithTagForSketchSegment,
} from '@src/lang/modifyAst/addEdgeTreatment'
import {
getNodeFromPath,
getSketchExprsFromSelection,
valueOrVariable,
} from '@src/lang/queryAst'
import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import type {
CallExpressionKw,
Expr,
PathToNode,
Program,
VariableDeclaration,
} from '@src/lang/wasm'
import type { KclCommandValue } from '@src/lib/commandTypes'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from '@src/lib/constants'
import type { Selections } from '@src/lib/selections'
import { err } from '@src/lib/trap'
export function addExtrude({
ast,
sketches,
length,
nodeToEdit,
}: {
ast: Node<Program>
sketches: Selections
length: KclCommandValue
nodeToEdit?: PathToNode
}):
| {
modifiedAst: Node<Program>
pathToNode: PathToNode
}
| Error {
// 1. Clone the ast so we can edit it
const modifiedAst = structuredClone(ast)
// 2. Prepare unlabeled and labeled arguments
// Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument
const sketchesExprList = getSketchExprsFromSelection(
sketches,
modifiedAst,
nodeToEdit
)
if (err(sketchesExprList)) {
return sketchesExprList
}
const sketchesExpr = createSketchExpression(sketchesExprList)
const call = createCallExpressionStdLibKw('extrude', sketchesExpr, [
createLabeledArg('length', valueOrVariable(length)),
])
// Insert variables for labeled arguments if provided
if ('variableName' in length && length.variableName) {
insertVariableAndOffsetPathToNode(length, modifiedAst, nodeToEdit)
}
// 3. If edit, we assign the new function call declaration to the existing node,
// otherwise just push to the end
let pathToNode: PathToNode | undefined
if (nodeToEdit) {
const result = getNodeFromPath<CallExpressionKw>(
modifiedAst,
nodeToEdit,
'CallExpressionKw'
)
if (err(result)) {
return result
}
Object.assign(result.node, call)
pathToNode = nodeToEdit
} else {
const name = findUniqueName(
modifiedAst,
KCL_DEFAULT_CONSTANT_PREFIXES.EXTRUDE
)
const declaration = createVariableDeclaration(name, call)
modifiedAst.body.push(declaration)
pathToNode = createPathToNode(modifiedAst)
}
return {
modifiedAst,
pathToNode,
}
}
export function addSweep({
ast,
sketches,
path,
sectional,
nodeToEdit,
}: {
ast: Node<Program>
sketches: Selections
path: Selections
sectional?: boolean
nodeToEdit?: PathToNode
}):
| {
modifiedAst: Node<Program>
pathToNode: PathToNode
}
| Error {
// 1. Clone the ast so we can edit it
const modifiedAst = structuredClone(ast)
// 2. Prepare unlabeled and labeled arguments
// Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument
const sketchesExprList = getSketchExprsFromSelection(
sketches,
modifiedAst,
nodeToEdit
)
if (err(sketchesExprList)) {
return sketchesExprList
}
// Extra roundabout to find the trajectory (or path) declaration for the labeled argument
const pathNodePath = getNodePathFromSourceRange(
ast,
path.graphSelections[0].codeRef.range
)
const pathDeclaration = getNodeFromPath<VariableDeclaration>(
ast,
pathNodePath,
'VariableDeclaration'
)
if (err(pathDeclaration)) {
return pathDeclaration
}
// Extra labeled args expressions
const pathExpr = createLocalName(pathDeclaration.node.declaration.id.name)
const sectionalExpr = sectional
? [createLabeledArg('sectional', createLiteral(sectional))]
: []
const sketchesExpr = createSketchExpression(sketchesExprList)
const call = createCallExpressionStdLibKw('sweep', sketchesExpr, [
createLabeledArg('path', pathExpr),
...sectionalExpr,
])
// 3. If edit, we assign the new function call declaration to the existing node,
// otherwise just push to the end
let pathToNode: PathToNode | undefined
if (nodeToEdit) {
const result = getNodeFromPath<CallExpressionKw>(
modifiedAst,
nodeToEdit,
'CallExpressionKw'
)
if (err(result)) {
return result
}
Object.assign(result.node, call)
pathToNode = nodeToEdit
} else {
const name = findUniqueName(
modifiedAst,
KCL_DEFAULT_CONSTANT_PREFIXES.SWEEP
)
const declaration = createVariableDeclaration(name, call)
modifiedAst.body.push(declaration)
pathToNode = createPathToNode(modifiedAst)
}
return {
modifiedAst,
pathToNode,
}
}
export function addLoft({
ast,
sketches,
}: {
ast: Node<Program>
sketches: Selections
}):
| {
modifiedAst: Node<Program>
pathToNode: PathToNode
}
| Error {
// 1. Clone the ast so we can edit it
const modifiedAst = structuredClone(ast)
// 2. Prepare unlabeled and labeled arguments
// Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument
const sketchesExprList = getSketchExprsFromSelection(sketches, modifiedAst)
if (err(sketchesExprList)) {
return sketchesExprList
}
const sketchesExpr = createSketchExpression(sketchesExprList)
const call = createCallExpressionStdLibKw('loft', sketchesExpr, [])
// 3. Just push the declaration to the end
// Note that Loft doesn't support edit flows yet since it's selection only atm
const name = findUniqueName(modifiedAst, KCL_DEFAULT_CONSTANT_PREFIXES.LOFT)
const declaration = createVariableDeclaration(name, call)
modifiedAst.body.push(declaration)
const toFirstKwarg = false
const pathToNode = createPathToNode(modifiedAst, toFirstKwarg)
return {
modifiedAst,
pathToNode,
}
}
export function addRevolve({
ast,
sketches,
angle,
axisOrEdge,
axis,
edge,
nodeToEdit,
}: {
ast: Node<Program>
sketches: Selections
angle: KclCommandValue
axisOrEdge: 'Axis' | 'Edge'
axis: string | undefined
edge: Selections | undefined
nodeToEdit?: PathToNode
}):
| {
modifiedAst: Node<Program>
pathToNode: PathToNode
}
| Error {
// 1. Clone the ast so we can edit it
const modifiedAst = structuredClone(ast)
// 2. Prepare unlabeled and labeled arguments
// Map the sketches selection into a list of kcl expressions to be passed as unlabelled argument
const sketchesExprList = getSketchExprsFromSelection(
sketches,
modifiedAst,
nodeToEdit
)
if (err(sketchesExprList)) {
return sketchesExprList
}
// Retrieve axis expression depending on mode
const getAxisResult = getAxisExpressionAndIndex(
axisOrEdge,
axis,
edge,
modifiedAst
)
if (err(getAxisResult) || !getAxisResult.generatedAxis) {
return new Error('Generated axis selection is missing.')
}
const sketchesExpr = createSketchExpression(sketchesExprList)
const call = createCallExpressionStdLibKw('revolve', sketchesExpr, [
createLabeledArg('angle', valueOrVariable(angle)),
createLabeledArg('axis', getAxisResult.generatedAxis),
])
// Insert variables for labeled arguments if provided
if ('variableName' in angle && angle.variableName) {
insertVariableAndOffsetPathToNode(angle, modifiedAst, nodeToEdit)
}
// 3. If edit, we assign the new function call declaration to the existing node,
// otherwise just push to the end
let pathToNode: PathToNode | undefined
if (nodeToEdit) {
const result = getNodeFromPath<CallExpressionKw>(
modifiedAst,
nodeToEdit,
'CallExpressionKw'
)
if (err(result)) {
return result
}
Object.assign(result.node, call)
pathToNode = nodeToEdit
} else {
const name = findUniqueName(
modifiedAst,
KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE
)
const declaration = createVariableDeclaration(name, call)
modifiedAst.body.push(declaration)
pathToNode = createPathToNode(modifiedAst)
}
return {
modifiedAst,
pathToNode,
}
}
// Utilities
function createSketchExpression(sketches: Expr[]) {
let sketchesExpr: Expr | null = null
if (sketches.every((s) => s.type === 'PipeSubstitution')) {
// Keeping null so we don't even put it the % sign
} else if (sketches.length === 1) {
sketchesExpr = sketches[0]
} else {
sketchesExpr = createArrayExpression(sketches)
}
return sketchesExpr
}
function createPathToNode(
modifiedAst: Node<Program>,
toFirstKwarg = true
): PathToNode {
const argIndex = 0 // first kwarg for all sweeps here
const pathToCall: PathToNode = [
['body', ''],
[modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
]
if (toFirstKwarg) {
pathToCall.push(
['arguments', 'CallExpressionKw'],
[argIndex, ARG_INDEX_FIELD],
['arg', LABELED_ARG_FIELD]
)
}
return pathToCall
}
export function getAxisExpressionAndIndex(
axisOrEdge: 'Axis' | 'Edge',
axis: string | undefined,
edge: Selections | undefined,
ast: Node<Program>
) {
let generatedAxis
let axisDeclaration: PathToNode | null = null
let axisIndexIfAxis: number | undefined = undefined
if (axisOrEdge === 'Edge' && edge) {
const pathToAxisSelection = getNodePathFromSourceRange(
ast,
edge.graphSelections[0]?.codeRef.range
)
const tagResult = mutateAstWithTagForSketchSegment(ast, pathToAxisSelection)
// Have the tag whether it is already created or a new one is generated
if (err(tagResult)) return tagResult
const { tag } = tagResult
const axisSelection = edge?.graphSelections[0]?.artifact
if (!axisSelection) return new Error('Generated axis selection is missing.')
generatedAxis = getEdgeTagCall(tag, axisSelection)
if (
axisSelection.type === 'segment' ||
axisSelection.type === 'path' ||
axisSelection.type === 'edgeCut'
) {
axisDeclaration = axisSelection.codeRef.pathToNode
if (!axisDeclaration)
return new Error('Expected to find axis declaration')
const axisIndexInPathToNode =
axisDeclaration.findIndex((a) => a[0] === 'body') + 1
const value = axisDeclaration[axisIndexInPathToNode][0]
if (typeof value !== 'number')
return new Error('expected axis index value to be a number')
axisIndexIfAxis = value
}
} else if (axisOrEdge === 'Axis' && axis) {
generatedAxis = createLocalName(axis)
}
return {
generatedAxis,
axisIndexIfAxis,
}
}

View File

@ -2,12 +2,14 @@ import type { FunctionExpression } from '@rust/kcl-lib/bindings/FunctionExpressi
import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement' import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement'
import type { Node } from '@rust/kcl-lib/bindings/Node' import type { Node } from '@rust/kcl-lib/bindings/Node'
import type { TypeDeclaration } from '@rust/kcl-lib/bindings/TypeDeclaration' import type { TypeDeclaration } from '@rust/kcl-lib/bindings/TypeDeclaration'
import { createLocalName, createPipeSubstitution } from '@src/lang/create'
import { createLocalName } from '@src/lang/create'
import type { ToolTip } from '@src/lang/langHelpers' import type { ToolTip } from '@src/lang/langHelpers'
import { splitPathAtLastIndex } from '@src/lang/modifyAst' import { splitPathAtLastIndex } from '@src/lang/modifyAst'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import { codeRefFromRange } from '@src/lang/std/artifactGraph' import {
codeRefFromRange,
getArtifactOfTypes,
} from '@src/lang/std/artifactGraph'
import { getArgForEnd } from '@src/lang/std/sketch' import { getArgForEnd } from '@src/lang/std/sketch'
import { getSketchSegmentFromSourceRange } from '@src/lang/std/sketchConstraints' import { getSketchSegmentFromSourceRange } from '@src/lang/std/sketchConstraints'
import { import {
@ -53,6 +55,7 @@ import { getAngle, isArray } from '@src/lib/utils'
import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants' import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
import type { KclCommandValue } from '@src/lib/commandTypes' import type { KclCommandValue } from '@src/lib/commandTypes'
import type { UnaryExpression } from 'typescript' import type { UnaryExpression } from 'typescript'
import type { Operation, OpKclValue } from '@rust/kcl-lib/bindings/Operation'
/** /**
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type. * Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
@ -1053,6 +1056,125 @@ export const valueOrVariable = (variable: KclCommandValue) => {
: variable.valueAst : variable.valueAst
} }
// Go from a selection of sketches to a list of KCL expressions that
// can be used to create KCL sweep call declarations.
export function getSketchExprsFromSelection(
selection: Selections,
ast: Node<Program>,
nodeToEdit?: PathToNode
): Error | Expr[] {
const sketches: Expr[] = selection.graphSelections.flatMap((s) => {
const path = getNodePathFromSourceRange(ast, s?.codeRef.range)
const sketchVariable = getNodeFromPath<VariableDeclarator>(
ast,
path,
'VariableDeclarator'
)
if (err(sketchVariable)) {
return []
}
if (sketchVariable.node.id) {
const name = sketchVariable.node?.id.name
if (nodeToEdit) {
const result = getNodeFromPath<VariableDeclarator>(
ast,
nodeToEdit,
'VariableDeclarator'
)
if (
!err(result) &&
result.node.type === 'VariableDeclarator' &&
name === result.node.id.name
) {
// Pointing to same variable case
return createPipeSubstitution()
}
}
// Pointing to different variable case
return createLocalName(name)
} else {
// No variable case
return createPipeSubstitution()
}
})
if (sketches.length === 0) {
return new Error("Couldn't map selections to program references")
}
return sketches
}
// Go from the sketches argument in a KCL sweep call declaration
// to a list of graph selections, useful for edit flows.
// Somewhat of an inverse of getSketchExprsFromSelection.
export function getSketchSelectionsFromOperation(
operation: Operation,
artifactGraph: ArtifactGraph
): Error | Selections {
const error = new Error("Couldn't retrieve sketches from operation")
if (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') {
return error
}
let sketches: OpKclValue[] = []
if (operation.unlabeledArg?.value.type === 'Sketch') {
sketches = [operation.unlabeledArg.value]
} else if (operation.unlabeledArg?.value.type === 'Array') {
sketches = operation.unlabeledArg.value.value
} else {
return error
}
const graphSelections: Selection[] = sketches.flatMap((sketch) => {
// We have to go a little roundabout to get from the original artifact
// to the solid2DId that we need to pass to the Extrude command.
if (sketch.type !== 'Sketch') {
return []
}
const pathArtifact = getArtifactOfTypes(
{
key: sketch.value.artifactId,
types: ['path'],
},
artifactGraph
)
if (
err(pathArtifact) ||
pathArtifact.type !== 'path' ||
!pathArtifact.solid2dId
) {
return []
}
const solid2DArtifact = getArtifactOfTypes(
{
key: pathArtifact.solid2dId,
types: ['solid2d'],
},
artifactGraph
)
if (err(solid2DArtifact) || solid2DArtifact.type !== 'solid2d') {
return []
}
return {
artifact: solid2DArtifact,
codeRef: pathArtifact.codeRef,
}
})
if (graphSelections.length === 0) {
return error
}
return {
graphSelections,
otherSelections: [],
}
}
export function findImportNodeAndAlias( export function findImportNodeAndAlias(
ast: Node<Program>, ast: Node<Program>,
pathToNode: PathToNode pathToNode: PathToNode

View File

@ -64,27 +64,20 @@ export type ModelingCommandSchema = {
Extrude: { Extrude: {
// Enables editing workflow // Enables editing workflow
nodeToEdit?: PathToNode nodeToEdit?: PathToNode
selection: Selections // & { type: 'face' } would be cool to lock that down // KCL stdlib arguments
// result: (typeof EXTRUSION_RESULTS)[number] sketches: Selections
distance: KclCommandValue length: KclCommandValue
} }
Sweep: { Sweep: {
// Enables editing workflow // Enables editing workflow
nodeToEdit?: PathToNode nodeToEdit?: PathToNode
// Arguments // KCL stdlib arguments
target: Selections sketches: Selections
trajectory: Selections path: Selections
sectional: boolean sectional?: boolean
} }
Loft: { Loft: {
selection: Selections sketches: Selections
}
Shell: {
// Enables editing workflow
nodeToEdit?: PathToNode
// KCL stdlib arguments
selection: Selections
thickness: KclCommandValue
} }
Revolve: { Revolve: {
// Enables editing workflow // Enables editing workflow
@ -92,11 +85,18 @@ export type ModelingCommandSchema = {
// Flow arg // Flow arg
axisOrEdge: 'Axis' | 'Edge' axisOrEdge: 'Axis' | 'Edge'
// KCL stdlib arguments // KCL stdlib arguments
selection: Selections sketches: Selections
angle: KclCommandValue angle: KclCommandValue
axis: string | undefined axis: string | undefined
edge: Selections | undefined edge: Selections | undefined
} }
Shell: {
// Enables editing workflow
nodeToEdit?: PathToNode
// KCL stdlib arguments
selection: Selections
thickness: KclCommandValue
}
Fillet: { Fillet: {
// Enables editing workflow // Enables editing workflow
nodeToEdit?: PathToNode nodeToEdit?: PathToNode
@ -388,26 +388,14 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
required: false, required: false,
hidden: true, hidden: true,
}, },
selection: { sketches: {
inputType: 'selection', inputType: 'selection',
selectionTypes: ['solid2d', 'segment'], selectionTypes: ['solid2d', 'segment'],
multiple: false, // TODO: multiple selection multiple: true,
required: true, required: true,
skip: true,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
}, },
// result: { length: {
// inputType: 'options',
// defaultValue: 'add',
// skip: true,
// required: true,
// options: EXTRUSION_RESULTS.map((r) => ({
// name: r,
// isCurrent: r === 'add',
// value: r,
// })),
// },
distance: {
inputType: 'kcl', inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH, defaultValue: KCL_DEFAULT_LENGTH,
required: true, required: true,
@ -426,26 +414,28 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
skip: true, skip: true,
inputType: 'text', inputType: 'text',
required: false, required: false,
hidden: true,
}, },
target: { sketches: {
inputType: 'selection', inputType: 'selection',
selectionTypes: ['solid2d'], selectionTypes: ['solid2d', 'segment'],
multiple: true,
required: true, required: true,
skip: true,
multiple: false,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
}, },
trajectory: { path: {
inputType: 'selection', inputType: 'selection',
selectionTypes: ['segment'], selectionTypes: ['segment'],
required: true, required: true,
skip: true,
multiple: false, multiple: false,
validation: sweepValidator, validation: sweepValidator,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
}, },
sectional: { sectional: {
inputType: 'options', inputType: 'options',
skip: true,
defaultValue: false,
hidden: false,
required: true, required: true,
options: [ options: [
{ name: 'False', value: false }, { name: 'False', value: false },
@ -458,45 +448,17 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
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',
needsReview: false, needsReview: true,
args: { args: {
selection: { sketches: {
inputType: 'selection', inputType: 'selection',
selectionTypes: ['solid2d'], selectionTypes: ['solid2d'],
multiple: true, multiple: true,
required: true, required: true,
skip: false,
validation: loftValidator, validation: loftValidator,
}, },
}, },
}, },
Shell: {
description: 'Hollow out a 3D solid.',
icon: 'shell',
needsReview: true,
args: {
nodeToEdit: {
description:
'Path to the node in the AST to edit. Never shown to the user.',
skip: true,
inputType: 'text',
required: false,
},
selection: {
inputType: 'selection',
selectionTypes: ['cap', 'wall'],
multiple: true,
required: true,
validation: shellValidator,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
thickness: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
},
},
},
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.',
icon: 'revolve', icon: 'revolve',
@ -509,12 +471,11 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
inputType: 'text', inputType: 'text',
required: false, required: false,
}, },
selection: { sketches: {
inputType: 'selection', inputType: 'selection',
selectionTypes: ['solid2d', 'segment'], selectionTypes: ['solid2d', 'segment'],
multiple: false, // TODO: multiple selection multiple: true,
required: true, required: true,
skip: true,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit), hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
}, },
axisOrEdge: { axisOrEdge: {
@ -557,6 +518,33 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
Shell: {
description: 'Hollow out a 3D solid.',
icon: 'shell',
needsReview: true,
args: {
nodeToEdit: {
description:
'Path to the node in the AST to edit. Never shown to the user.',
skip: true,
inputType: 'text',
required: false,
},
selection: {
inputType: 'selection',
selectionTypes: ['cap', 'wall'],
multiple: true,
required: true,
validation: shellValidator,
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
},
thickness: {
inputType: 'kcl',
defaultValue: KCL_DEFAULT_LENGTH,
required: true,
},
},
},
'Boolean Subtract': { 'Boolean Subtract': {
description: 'Subtract one solid from another.', description: 'Subtract one solid from another.',
icon: 'booleanSubtract', icon: 'booleanSubtract',

View File

@ -94,11 +94,12 @@ export const revolveAxisValidator = async ({
data: { [key: string]: Selections } data: { [key: string]: Selections }
context: CommandBarContext context: CommandBarContext
}): Promise<boolean | string> => { }): Promise<boolean | string> => {
if (!isSelections(context.argumentsToSubmit.selection)) { if (!isSelections(context.argumentsToSubmit.sketches)) {
return 'Unable to revolve, selections are missing' return 'Unable to revolve, selections are missing'
} }
// Gotcha: this validation only works for the first sketch
const artifact = const artifact =
context.argumentsToSubmit.selection.graphSelections[0].artifact context.argumentsToSubmit.sketches.graphSelections[0].artifact
if (!artifact) { if (!artifact) {
return 'Unable to revolve, sketch not found' return 'Unable to revolve, sketch not found'
@ -155,16 +156,16 @@ export const loftValidator = async ({
data: { [key: string]: Selections } data: { [key: string]: Selections }
context: CommandBarContext context: CommandBarContext
}): Promise<boolean | string> => { }): Promise<boolean | string> => {
if (!isSelections(data.selection)) { if (!isSelections(data.sketches)) {
return 'Unable to loft, selections are missing' return 'Unable to loft, selections are missing'
} }
const { selection } = data const { sketches } = data
if (selection.graphSelections.some((s) => s.artifact?.type !== 'solid2d')) { if (sketches.graphSelections.some((s) => s.artifact?.type !== 'solid2d')) {
return 'Unable to loft, some selection are not solid2ds' return 'Unable to loft, some selection are not solid2ds'
} }
const sectionIds = data.selection.graphSelections.flatMap((s) => const sectionIds = sketches.graphSelections.flatMap((s) =>
s.artifact?.type === 'solid2d' ? s.artifact.pathId : [] s.artifact?.type === 'solid2d' ? s.artifact.pathId : []
) )
@ -258,15 +259,15 @@ export const sweepValidator = async ({
data, data,
}: { }: {
context: CommandBarContext context: CommandBarContext
data: { trajectory: Selections } data: { path: Selections }
}): Promise<boolean | string> => { }): Promise<boolean | string> => {
if (!isSelections(data.trajectory)) { if (!isSelections(data.path)) {
console.log('Unable to sweep, selections are missing') console.log('Unable to sweep, selections are missing')
return 'Unable to sweep, selections are missing' return 'Unable to sweep, selections are missing'
} }
// Retrieve the parent path from the segment selection directly // Retrieve the parent path from the segment selection directly
const trajectoryArtifact = data.trajectory.graphSelections[0].artifact const trajectoryArtifact = data.path.graphSelections[0].artifact
if (!trajectoryArtifact) { if (!trajectoryArtifact) {
return "Unable to sweep, couldn't find the trajectory artifact" return "Unable to sweep, couldn't find the trajectory artifact"
} }
@ -276,7 +277,7 @@ export const sweepValidator = async ({
const trajectory = trajectoryArtifact.pathId const trajectory = trajectoryArtifact.pathId
// Get the former arg in the command bar flow, and retrieve the path from the solid2d directly // 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 targetArg = context.argumentsToSubmit['sketches'] as Selections
const targetArtifact = targetArg.graphSelections[0].artifact const targetArtifact = targetArg.graphSelections[0].artifact
if (!targetArtifact) { if (!targetArtifact) {
return "Unable to sweep, couldn't find the profile artifact" return "Unable to sweep, couldn't find the profile artifact"

View File

@ -1,7 +1,11 @@
import type { OpKclValue, Operation } from '@rust/kcl-lib/bindings/Operation' import type { OpKclValue, Operation } from '@rust/kcl-lib/bindings/Operation'
import type { CustomIconName } from '@src/components/CustomIcon' import type { CustomIconName } from '@src/components/CustomIcon'
import { getNodeFromPath, findPipesWithImportAlias } from '@src/lang/queryAst' import {
getNodeFromPath,
findPipesWithImportAlias,
getSketchSelectionsFromOperation,
} from '@src/lang/queryAst'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import type { Artifact } from '@src/lang/std/artifactGraph' import type { Artifact } from '@src/lang/std/artifactGraph'
import { import {
@ -57,81 +61,51 @@ interface StdLibCallInfo {
* Gather up the argument values for the Extrude command * Gather up the argument values for the Extrude command
* to be used in the command bar edit flow. * to be used in the command bar edit flow.
*/ */
const prepareToEditExtrude: PrepareToEditCallback = const prepareToEditExtrude: PrepareToEditCallback = async ({ operation }) => {
async function prepareToEditExtrude({ operation, artifact }) { const baseCommand = {
const baseCommand = { name: 'Extrude',
name: 'Extrude', groupId: 'modeling',
groupId: 'modeling',
}
if (
!artifact ||
!('pathId' in artifact) ||
(operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall')
) {
return baseCommand
}
// We have to go a little roundabout to get from the original artifact
// to the solid2DId that we need to pass to the Extrude command.
const pathArtifact = getArtifactOfTypes(
{
key: artifact.pathId,
types: ['path'],
},
kclManager.artifactGraph
)
if (
err(pathArtifact) ||
pathArtifact.type !== 'path' ||
!pathArtifact.solid2dId
)
return baseCommand
const solid2DArtifact = getArtifactOfTypes(
{
key: pathArtifact.solid2dId,
types: ['solid2d'],
},
kclManager.artifactGraph
)
if (err(solid2DArtifact) || solid2DArtifact.type !== 'solid2d') {
return baseCommand
}
// Convert the length argument from a string to a KCL expression
const distanceResult = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs?.['length']?.sourceRange[0],
operation.labeledArgs?.['length']?.sourceRange[1]
)
)
if (err(distanceResult) || 'errors' in distanceResult) {
return baseCommand
}
// Assemble the default argument values for the Extrude command,
// with `nodeToEdit` set, which will let the Extrude actor know
// to edit the node that corresponds to the StdLibCall.
const argDefaultValues: ModelingCommandSchema['Extrude'] = {
selection: {
graphSelections: [
{
artifact: solid2DArtifact,
codeRef: pathArtifact.codeRef,
},
],
otherSelections: [],
},
distance: distanceResult,
nodeToEdit: getNodePathFromSourceRange(
kclManager.ast,
sourceRangeFromRust(operation.sourceRange)
),
}
return {
...baseCommand,
argDefaultValues,
}
} }
if (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') {
return { reason: 'Wrong operation type' }
}
// 1. Map the unlabeled arguments to solid2d selections
const sketches = getSketchSelectionsFromOperation(
operation,
kclManager.artifactGraph
)
if (err(sketches)) {
return { reason: "Couldn't retrieve sketches" }
}
// 2. Convert the length argument from a string to a KCL expression
const length = await stringToKclExpression(
codeManager.code.slice(
operation.labeledArgs?.['length']?.sourceRange[0],
operation.labeledArgs?.['length']?.sourceRange[1]
)
)
if (err(length) || 'errors' in length) {
return { reason: "Couldn't retrieve length argument" }
}
// 3. Assemble the default argument values for the command,
// with `nodeToEdit` set, which will let the actor know
// to edit the node that corresponds to the StdLibCall.
const argDefaultValues: ModelingCommandSchema['Extrude'] = {
sketches,
length,
nodeToEdit: getNodePathFromSourceRange(
kclManager.ast,
sourceRangeFromRust(operation.sourceRange)
),
}
return {
...baseCommand,
argDefaultValues,
}
}
/** /**
* Gather up the argument values for the Chamfer or Fillet command * Gather up the argument values for the Chamfer or Fillet command
@ -446,78 +420,32 @@ const prepareToEditOffsetPlane: PrepareToEditCallback = async ({
} }
} }
const prepareToEditSweep: PrepareToEditCallback = async ({ /**
artifact, * Gather up the argument values for the Revolve command
operation, * to be used in the command bar edit flow.
}) => { */
const prepareToEditSweep: PrepareToEditCallback = async ({ operation }) => {
const baseCommand = { const baseCommand = {
name: 'Sweep', name: 'Sweep',
groupId: 'modeling', groupId: 'modeling',
} }
if ( if (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') {
(operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') || return { reason: 'Wrong operation type' }
!operation.labeledArgs ||
!operation.unlabeledArg ||
!('sectional' in operation.labeledArgs) ||
!operation.labeledArgs.sectional
) {
return baseCommand
}
if (
!artifact ||
!('pathId' in artifact) ||
(operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall')
) {
return baseCommand
} }
// We have to go a little roundabout to get from the original artifact // 1. Map the unlabeled arguments to solid2d selections
// to the solid2DId that we need to pass to the Sweep command, just like Extrude. const sketches = getSketchSelectionsFromOperation(
const pathArtifact = getArtifactOfTypes( operation,
{
key: artifact.pathId,
types: ['path'],
},
kclManager.artifactGraph kclManager.artifactGraph
) )
if (err(sketches)) {
if ( return { reason: "Couldn't retrieve sketches" }
err(pathArtifact) ||
pathArtifact.type !== 'path' ||
!pathArtifact.solid2dId
) {
return baseCommand
}
const targetArtifact = getArtifactOfTypes(
{
key: pathArtifact.solid2dId,
types: ['solid2d'],
},
kclManager.artifactGraph
)
if (err(targetArtifact) || targetArtifact.type !== 'solid2d') {
return baseCommand
}
const target = {
graphSelections: [
{
artifact: targetArtifact,
codeRef: pathArtifact.codeRef,
},
],
otherSelections: [],
} }
// 2. Prepare labeled arguments
// Same roundabout but twice for 'path' aka trajectory: sketch -> path -> segment // Same roundabout but twice for 'path' aka trajectory: sketch -> path -> segment
if (!('path' in operation.labeledArgs) || !operation.labeledArgs.path) { if (operation.labeledArgs.path?.value.type !== 'Sketch') {
return baseCommand return { reason: "Couldn't retrieve trajectory argument" }
}
if (operation.labeledArgs.path.value.type !== 'Sketch') {
return baseCommand
} }
const trajectoryPathArtifact = getArtifactOfTypes( const trajectoryPathArtifact = getArtifactOfTypes(
@ -529,7 +457,7 @@ const prepareToEditSweep: PrepareToEditCallback = async ({
) )
if (err(trajectoryPathArtifact) || trajectoryPathArtifact.type !== 'path') { if (err(trajectoryPathArtifact) || trajectoryPathArtifact.type !== 'path') {
return baseCommand return { reason: "Couldn't retrieve trajectory path artifact" }
} }
const trajectoryArtifact = getArtifactOfTypes( const trajectoryArtifact = getArtifactOfTypes(
@ -541,10 +469,11 @@ const prepareToEditSweep: PrepareToEditCallback = async ({
) )
if (err(trajectoryArtifact) || trajectoryArtifact.type !== 'segment') { if (err(trajectoryArtifact) || trajectoryArtifact.type !== 'segment') {
return baseCommand console.log(trajectoryArtifact)
return { reason: "Couldn't retrieve trajectory artifact" }
} }
const trajectory = { const path = {
graphSelections: [ graphSelections: [
{ {
artifact: trajectoryArtifact, artifact: trajectoryArtifact,
@ -554,33 +483,28 @@ const prepareToEditSweep: PrepareToEditCallback = async ({
otherSelections: [], otherSelections: [],
} }
// sectional options boolean arg // sectional argument from a string to a KCL expression
if ( let sectional: boolean | undefined
!('sectional' in operation.labeledArgs) || if ('sectional' in operation.labeledArgs && operation.labeledArgs.sectional) {
!operation.labeledArgs.sectional sectional =
) { codeManager.code.slice(
return baseCommand operation.labeledArgs.sectional.sourceRange[0],
operation.labeledArgs.sectional.sourceRange[1]
) === 'true'
} }
const sectional = // 3. Assemble the default argument values for the command,
codeManager.code.slice( // with `nodeToEdit` set, which will let the actor know
operation.labeledArgs.sectional.sourceRange[0],
operation.labeledArgs.sectional.sourceRange[1]
) === 'true'
// Assemble the default argument values for the Offset Plane command,
// with `nodeToEdit` set, which will let the Offset Plane actor know
// to edit the node that corresponds to the StdLibCall. // to edit the node that corresponds to the StdLibCall.
const argDefaultValues: ModelingCommandSchema['Sweep'] = { const argDefaultValues: ModelingCommandSchema['Sweep'] = {
target: target, sketches,
trajectory, path,
sectional, sectional,
nodeToEdit: getNodePathFromSourceRange( nodeToEdit: getNodePathFromSourceRange(
kclManager.ast, kclManager.ast,
sourceRangeFromRust(operation.sourceRange) sourceRangeFromRust(operation.sourceRange)
), ),
} }
return { return {
...baseCommand, ...baseCommand,
argDefaultValues, argDefaultValues,
@ -841,6 +765,10 @@ const prepareToEditHelix: PrepareToEditCallback = async ({ operation }) => {
} }
} }
/**
* Gather up the argument values for the Revolve command
* to be used in the command bar edit flow.
*/
const prepareToEditRevolve: PrepareToEditCallback = async ({ const prepareToEditRevolve: PrepareToEditCallback = async ({
operation, operation,
artifact, artifact,
@ -851,51 +779,22 @@ const prepareToEditRevolve: PrepareToEditCallback = async ({
} }
if ( if (
!artifact || !artifact ||
!('pathId' in artifact) ||
operation.type !== 'KclStdLibCall' || operation.type !== 'KclStdLibCall' ||
!operation.labeledArgs !operation.labeledArgs
) { ) {
return { reason: 'Wrong operation type or artifact' } return { reason: 'Wrong operation type or artifact' }
} }
// We have to go a little roundabout to get from the original artifact // 1. Map the unlabeled arguments to solid2d selections
// to the solid2DId that we need to pass to the command. const sketches = getSketchSelectionsFromOperation(
const pathArtifact = getArtifactOfTypes( operation,
{
key: artifact.pathId,
types: ['path'],
},
kclManager.artifactGraph kclManager.artifactGraph
) )
if ( if (err(sketches)) {
err(pathArtifact) || return { reason: "Couldn't retrieve sketches" }
pathArtifact.type !== 'path' ||
!pathArtifact.solid2dId
) {
return { reason: "Couldn't find related path artifact" }
}
const solid2DArtifact = getArtifactOfTypes(
{
key: pathArtifact.solid2dId,
types: ['solid2d'],
},
kclManager.artifactGraph
)
if (err(solid2DArtifact) || solid2DArtifact.type !== 'solid2d') {
return { reason: "Couldn't find related solid2d artifact" }
}
const selection = {
graphSelections: [
{
artifact: solid2DArtifact,
codeRef: pathArtifact.codeRef,
},
],
otherSelections: [],
} }
// 2. Prepare labeled arguments
// axis options string arg // axis options string arg
if (!('axis' in operation.labeledArgs) || !operation.labeledArgs.axis) { if (!('axis' in operation.labeledArgs) || !operation.labeledArgs.axis) {
return { reason: "Couldn't find axis argument" } return { reason: "Couldn't find axis argument" }
@ -988,14 +887,14 @@ const prepareToEditRevolve: PrepareToEditCallback = async ({
return { reason: 'Error in angle argument retrieval' } return { reason: 'Error in angle argument retrieval' }
} }
// Assemble the default argument values for the Offset Plane command, // 3. Assemble the default argument values for the command,
// with `nodeToEdit` set, which will let the Offset Plane actor know // with `nodeToEdit` set, which will let the actor know
// to edit the node that corresponds to the StdLibCall. // to edit the node that corresponds to the StdLibCall.
const argDefaultValues: ModelingCommandSchema['Revolve'] = { const argDefaultValues: ModelingCommandSchema['Revolve'] = {
sketches,
axisOrEdge, axisOrEdge,
axis, axis,
edge, edge,
selection,
angle, angle,
nodeToEdit: getNodePathFromSourceRange( nodeToEdit: getNodePathFromSourceRange(
kclManager.ast, kclManager.ast,

View File

@ -49,11 +49,8 @@ import {
addHelix, addHelix,
addOffsetPlane, addOffsetPlane,
addShell, addShell,
addSweep,
extrudeSketch,
insertNamedConstant, insertNamedConstant,
insertVariableAndOffsetPathToNode, insertVariableAndOffsetPathToNode,
loftSketches,
} from '@src/lang/modifyAst' } from '@src/lang/modifyAst'
import type { import type {
ChamferParameters, ChamferParameters,
@ -66,9 +63,12 @@ import {
mutateAstWithTagForSketchSegment, mutateAstWithTagForSketchSegment,
} from '@src/lang/modifyAst/addEdgeTreatment' } from '@src/lang/modifyAst/addEdgeTreatment'
import { import {
addExtrude,
addLoft,
addRevolve,
addSweep,
getAxisExpressionAndIndex, getAxisExpressionAndIndex,
revolveSketch, } from '@src/lang/modifyAst/addSweep'
} from '@src/lang/modifyAst/addRevolve'
import { import {
applyIntersectFromTargetOperatorSelections, applyIntersectFromTargetOperatorSelections,
applySubtractFromTargetOperatorSelections, applySubtractFromTargetOperatorSelections,
@ -94,7 +94,6 @@ import {
updatePathToNodesAfterEdit, updatePathToNodesAfterEdit,
valueOrVariable, valueOrVariable,
} from '@src/lang/queryAst' } from '@src/lang/queryAst'
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
import { import {
getFaceCodeRef, getFaceCodeRef,
getPathsFromPlaneArtifact, getPathsFromPlaneArtifact,
@ -133,7 +132,7 @@ import {
} from '@src/lib/singletons' } from '@src/lib/singletons'
import type { ToolbarModeName } from '@src/lib/toolbar' import type { ToolbarModeName } from '@src/lib/toolbar'
import { err, reportRejection, trap } from '@src/lib/trap' import { err, reportRejection, trap } from '@src/lib/trap'
import { isArray, uuidv4 } from '@src/lib/utils' import { uuidv4 } from '@src/lib/utils'
import { deleteNodeInExtrudePipe } from '@src/lang/modifyAst/deleteNodeInExtrudePipe' import { deleteNodeInExtrudePipe } from '@src/lang/modifyAst/deleteNodeInExtrudePipe'
import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement' import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement'
@ -1793,69 +1792,20 @@ export const modelingMachine = setup({
unknown, unknown,
ModelingCommandSchema['Extrude'] | undefined ModelingCommandSchema['Extrude'] | undefined
>(async ({ input }) => { >(async ({ input }) => {
if (!input) return new Error('No input provided') if (!input) return Promise.reject(new Error('No input provided'))
const { selection, distance, nodeToEdit } = input const { nodeToEdit, sketches, length } = input
const isEditing = const { ast } = kclManager
nodeToEdit !== undefined && typeof nodeToEdit[1][0] === 'number' const astResult = addExtrude({
let ast = structuredClone(kclManager.ast)
let extrudeName: string | undefined = undefined
// If this is an edit flow, first we're going to remove the old extrusion
if (isEditing) {
// Extract the plane name from the node to edit
const extrudeNameNode = getNodeFromPath<VariableDeclaration>(
ast,
nodeToEdit,
'VariableDeclaration'
)
if (err(extrudeNameNode)) {
console.error('Error extracting plane name')
} else {
extrudeName = extrudeNameNode.node.declaration.id.name
}
// Removing the old extrusion statement
const newBody = [...ast.body]
newBody.splice(nodeToEdit[1][0] as number, 1)
ast.body = newBody
}
const pathToNode = getNodePathFromSourceRange(
ast, ast,
selection.graphSelections[0]?.codeRef.range sketches,
) length,
// Add an extrude statement to the AST nodeToEdit,
const extrudeSketchRes = extrudeSketch({
node: ast,
pathToNode,
artifact: selection.graphSelections[0].artifact,
artifactGraph: kclManager.artifactGraph,
distance:
'variableName' in distance
? distance.variableIdentifierAst
: distance.valueAst,
extrudeName,
}) })
if (err(extrudeSketchRes)) return extrudeSketchRes if (err(astResult)) {
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes return Promise.reject(new Error("Couldn't add extrude statement"))
// Insert the distance variable if the user has provided a variable name
if (
'variableName' in distance &&
distance.variableName &&
typeof pathToExtrudeArg[1][0] === 'number'
) {
const insertIndex = Math.min(
pathToExtrudeArg[1][0],
distance.insertIndex
)
const newBody = [...modifiedAst.body]
newBody.splice(insertIndex, 0, distance.variableDeclarationAst)
modifiedAst.body = newBody
// Since we inserted a new variable, we need to update the path to the extrude argument
pathToExtrudeArg[1][0]++
} }
const { modifiedAst, pathToNode } = astResult
await updateModelingState( await updateModelingState(
modifiedAst, modifiedAst,
EXECUTION_TYPE_REAL, EXECUTION_TYPE_REAL,
@ -1865,73 +1815,92 @@ export const modelingMachine = setup({
codeManager, codeManager,
}, },
{ {
focusPath: [pathToExtrudeArg], focusPath: [pathToNode],
} }
) )
}), }),
sweepAstMod: fromPromise<
unknown,
ModelingCommandSchema['Sweep'] | undefined
>(async ({ input }) => {
if (!input) return Promise.reject(new Error('No input provided'))
const { nodeToEdit, sketches, path, sectional } = input
const { ast } = kclManager
const astResult = addSweep({
ast,
sketches,
path,
sectional,
nodeToEdit,
})
if (err(astResult)) {
return Promise.reject(astResult)
}
const { modifiedAst, pathToNode } = astResult
await updateModelingState(
modifiedAst,
EXECUTION_TYPE_REAL,
{
kclManager,
editorManager,
codeManager,
},
{
focusPath: [pathToNode],
}
)
}),
loftAstMod: fromPromise(
async ({
input,
}: {
input: ModelingCommandSchema['Loft'] | undefined
}) => {
if (!input) return Promise.reject(new Error('No input provided'))
const { sketches } = input
const { ast } = kclManager
const astResult = addLoft({ ast, sketches })
if (err(astResult)) {
return Promise.reject(astResult)
}
const { modifiedAst, pathToNode } = astResult
await updateModelingState(
modifiedAst,
EXECUTION_TYPE_REAL,
{
kclManager,
editorManager,
codeManager,
},
{
focusPath: [pathToNode],
}
)
}
),
revolveAstMod: fromPromise< revolveAstMod: fromPromise<
unknown, unknown,
ModelingCommandSchema['Revolve'] | undefined ModelingCommandSchema['Revolve'] | undefined
>(async ({ input }) => { >(async ({ input }) => {
if (!input) return new Error('No input provided') if (!input) return Promise.reject(new Error('No input provided'))
const { nodeToEdit, selection, angle, axis, edge, axisOrEdge } = input const { nodeToEdit, sketches, angle, axis, edge, axisOrEdge } = input
let ast = kclManager.ast const { ast } = kclManager
let variableName: string | undefined = undefined const astResult = addRevolve({
let insertIndex: number | undefined = undefined
// If this is an edit flow, first we're going to remove the old extrusion
if (nodeToEdit && typeof nodeToEdit[1][0] === 'number') {
// Extract the plane name from the node to edit
const nameNode = getNodeFromPath<VariableDeclaration>(
ast,
nodeToEdit,
'VariableDeclaration'
)
if (err(nameNode)) {
console.error('Error extracting plane name')
} else {
variableName = nameNode.node.declaration.id.name
}
// Removing the old extrusion statement
const newBody = [...ast.body]
newBody.splice(nodeToEdit[1][0], 1)
ast.body = newBody
insertIndex = nodeToEdit[1][0]
}
if (
'variableName' in angle &&
angle.variableName &&
angle.insertIndex !== undefined
) {
const newBody = [...ast.body]
newBody.splice(angle.insertIndex, 0, angle.variableDeclarationAst)
ast.body = newBody
if (insertIndex) {
// if editing need to offset that new var
insertIndex += 1
}
}
// This is the selection of the sketch that will be revolved
const pathToNode = getNodePathFromSourceRange(
ast, ast,
selection.graphSelections[0]?.codeRef.range sketches,
) angle,
const revolveSketchRes = revolveSketch(
ast,
pathToNode,
'variableName' in angle ? angle.variableIdentifierAst : angle.valueAst,
axisOrEdge, axisOrEdge,
axis, axis,
edge, edge,
variableName, nodeToEdit,
insertIndex })
) if (err(astResult)) {
if (trap(revolveSketchRes)) return return Promise.reject(astResult)
const { modifiedAst, pathToRevolveArg } = revolveSketchRes }
const { modifiedAst, pathToNode } = astResult
await updateModelingState( await updateModelingState(
modifiedAst, modifiedAst,
EXECUTION_TYPE_REAL, EXECUTION_TYPE_REAL,
@ -1941,7 +1910,7 @@ export const modelingMachine = setup({
codeManager, codeManager,
}, },
{ {
focusPath: [pathToRevolveArg], focusPath: [pathToNode],
} }
) )
}), }),
@ -2169,141 +2138,6 @@ 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 { target, trajectory, sectional, nodeToEdit } = input
let variableName: string | undefined = undefined
let insertIndex: number | undefined = undefined
// If this is an edit flow, first we're going to remove the old one
if (nodeToEdit !== undefined && typeof nodeToEdit[1][0] === 'number') {
// Extract the plane name from the node to edit
const variableNode = getNodeFromPath<VariableDeclaration>(
ast,
nodeToEdit,
'VariableDeclaration'
)
if (err(variableNode)) {
console.error('Error extracting name')
} else {
variableName = variableNode.node.declaration.id.name
}
// Removing the old statement
const newBody = [...ast.body]
newBody.splice(nodeToEdit[1][0], 1)
ast.body = newBody
insertIndex = nodeToEdit[1][0]
}
// Find the target declaration
const targetNodePath = getNodePathFromSourceRange(
ast,
target.graphSelections[0].codeRef.range
)
// Gotchas, not sure why
// - it seems like in some cases we get a list on edit, especially the state that e2e hits
// - looking for a VariableDeclaration seems more robust than VariableDeclarator
const targetNode = getNodeFromPath<
VariableDeclaration | VariableDeclaration[]
>(ast, targetNodePath, 'VariableDeclaration')
if (err(targetNode)) {
return new Error("Couldn't parse profile selection")
}
const targetDeclarator = isArray(targetNode.node)
? targetNode.node[0].declaration
: targetNode.node.declaration
// Find the trajectory (or path) declaration
const trajectoryNodePath = getNodePathFromSourceRange(
ast,
trajectory.graphSelections[0].codeRef.range
)
// Also looking for VariableDeclaration for consistency here
const trajectoryNode = getNodeFromPath<VariableDeclaration>(
ast,
trajectoryNodePath,
'VariableDeclaration'
)
if (err(trajectoryNode)) {
return new Error("Couldn't parse path selection")
}
const trajectoryDeclarator = trajectoryNode.node.declaration
// Perform the sweep
const { modifiedAst, pathToNode } = addSweep({
node: ast,
targetDeclarator,
trajectoryDeclarator,
sectional,
variableName,
insertIndex,
})
await updateModelingState(
modifiedAst,
EXECUTION_TYPE_REAL,
{
kclManager,
editorManager,
codeManager,
},
{
focusPath: [pathToNode],
}
)
}
),
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)
await updateModelingState(
loftSketchesRes.modifiedAst,
EXECUTION_TYPE_REAL,
{
kclManager,
editorManager,
codeManager,
},
{
focusPath: [loftSketchesRes.pathToNode],
}
)
}
),
shellAstMod: fromPromise( shellAstMod: fromPromise(
async ({ async ({
input, input,