Multi-profile sweeps and more robust edit flows in point-and-click (#6437)
This commit is contained in:
@ -10,6 +10,8 @@ test.describe('Command bar tests', () => {
|
||||
test('Extrude from command bar selects extrude line after', async ({
|
||||
page,
|
||||
homePage,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
}) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
@ -35,20 +37,35 @@ test.describe('Command bar tests', () => {
|
||||
|
||||
// Click the line of code for xLine.
|
||||
await page.getByText(`close()`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.getByRole('button', { name: 'Extrude' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(200)
|
||||
await page.keyboard.press('Enter')
|
||||
await page.waitForTimeout(200)
|
||||
await toolbar.extrudeButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
commandName: 'Extrude',
|
||||
currentArgKey: 'sketches',
|
||||
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(
|
||||
`extrude001 = extrude(sketch001, length = ${KCL_DEFAULT_LENGTH})`
|
||||
)
|
||||
})
|
||||
|
||||
// TODO: fix this test after the electron migration
|
||||
test('Fillet from command bar', async ({ page, homePage }) => {
|
||||
await page.addInitScript(async () => {
|
||||
localStorage.setItem(
|
||||
@ -269,21 +286,22 @@ test.describe('Command bar tests', () => {
|
||||
await cmdBar.cmdOptions.getByText('Extrude').click()
|
||||
|
||||
// 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
|
||||
await page.mouse.move(700, 200)
|
||||
await page.mouse.click(700, 200)
|
||||
await cmdBar.progressCmdBar()
|
||||
|
||||
// Assert that we're on the distance step
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'distance', exact: false })
|
||||
page.getByRole('button', { name: 'length', exact: false })
|
||||
).toBeDisabled()
|
||||
|
||||
// Assert that the an alternative variable name is chosen,
|
||||
// since the default variable name is already in use (distance)
|
||||
await page.getByRole('button', { name: 'Create new variable' }).click()
|
||||
await expect(page.getByPlaceholder('Variable name')).toHaveValue(
|
||||
'distance001'
|
||||
'length001'
|
||||
)
|
||||
|
||||
const continueButton = page.getByRole('button', { name: 'Continue' })
|
||||
@ -297,7 +315,7 @@ test.describe('Command bar tests', () => {
|
||||
|
||||
// Assert we're back on the distance step
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'distance', exact: false })
|
||||
page.getByRole('button', { name: 'length', exact: false })
|
||||
).toBeDisabled()
|
||||
|
||||
await continueButton.click()
|
||||
@ -306,7 +324,7 @@ test.describe('Command bar tests', () => {
|
||||
await u.waitForCmdReceive('extrude')
|
||||
|
||||
await expect(page.locator('.cm-content')).toContainText(
|
||||
'extrude001 = extrude(sketch001, length = distance001)'
|
||||
'extrude001 = extrude(sketch001, length = length001)'
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -1085,6 +1085,9 @@ sketch001 = startSketchOn(XZ)
|
||||
page,
|
||||
context,
|
||||
homePage,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
scene,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
await context.addInitScript(async () => {
|
||||
@ -1128,17 +1131,30 @@ sketch001 = startSketchOn(XZ)
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.getByText('startProfile(at = [4.61, -14.01])').click()
|
||||
await expect(page.getByRole('button', { name: 'Extrude' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Extrude' }).click()
|
||||
|
||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await page.getByRole('button', { name: 'arrow right Continue' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
||||
await page.getByRole('button', { name: 'checkmark Submit command' }).click()
|
||||
await page.waitForTimeout(100)
|
||||
await toolbar.extrudeButton.click()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'length',
|
||||
currentArgValue: '5',
|
||||
headerArguments: {
|
||||
Sketches: '1 face',
|
||||
Length: '',
|
||||
},
|
||||
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
|
||||
await expect(page.locator('.cm-content')).toHaveText(
|
||||
|
@ -229,7 +229,7 @@ test.describe('Feature Tree pane', () => {
|
||||
const initialCode = `sketch001 = startSketchOn(XZ)
|
||||
|> circle(center = [0, 0], radius = 5)
|
||||
renamedExtrude = extrude(sketch001, length = ${initialInput})`
|
||||
const newConstantName = 'distance001'
|
||||
const newConstantName = 'length001'
|
||||
const expectedCode = `${newConstantName} = 23
|
||||
sketch001 = startSketchOn(XZ)
|
||||
|> circle(center = [0, 0], radius = 5)
|
||||
@ -270,12 +270,12 @@ test.describe('Feature Tree pane', () => {
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Extrude',
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'distance',
|
||||
currentArgKey: 'length',
|
||||
currentArgValue: initialInput,
|
||||
headerArguments: {
|
||||
Distance: initialInput,
|
||||
Length: initialInput,
|
||||
},
|
||||
highlightedHeaderArg: 'distance',
|
||||
highlightedHeaderArg: 'length',
|
||||
})
|
||||
})
|
||||
|
||||
@ -290,7 +290,7 @@ test.describe('Feature Tree pane', () => {
|
||||
stage: 'review',
|
||||
headerArguments: {
|
||||
// The calculated value is shown in the argument summary
|
||||
Distance: initialInput,
|
||||
Length: initialInput,
|
||||
},
|
||||
commandName: 'Extrude',
|
||||
})
|
||||
|
@ -6,6 +6,7 @@ import type { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
|
||||
import type { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
|
||||
import type { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
|
||||
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
|
||||
|
||||
@ -75,10 +76,19 @@ test.describe('Point-and-click tests', () => {
|
||||
await toolbar.extrudeButton.click()
|
||||
await cmdBar.expectState({
|
||||
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',
|
||||
headerArguments: { Selection: '1 face', Distance: '' },
|
||||
highlightedHeaderArg: 'distance',
|
||||
headerArguments: { Sketches: '1 face', Length: '' },
|
||||
highlightedHeaderArg: 'length',
|
||||
commandName: 'Extrude',
|
||||
})
|
||||
await cmdBar.progressCmdBar()
|
||||
@ -88,7 +98,7 @@ test.describe('Point-and-click tests', () => {
|
||||
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: { Selection: '1 face', Distance: '5' },
|
||||
headerArguments: { Sketches: '1 face', Length: '5' },
|
||||
commandName: 'Extrude',
|
||||
})
|
||||
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', () => {
|
||||
const _sketchOnAChamfer =
|
||||
(
|
||||
@ -1483,11 +1589,11 @@ extrude001 = extrude(profile001, length = 100)
|
||||
cmdBar,
|
||||
}) => {
|
||||
const initialCode = `sketch001 = startSketchOn(XZ)
|
||||
|> circle(center = [0, 0], radius = 30)
|
||||
plane001 = offsetPlane(XZ, offset = 50)
|
||||
sketch002 = startSketchOn(plane001)
|
||||
|> circle(center = [0, 0], radius = 20)
|
||||
`
|
||||
|> circle(center = [0, 0], radius = 30)
|
||||
plane001 = offsetPlane(XZ, offset = 50)
|
||||
sketch002 = startSketchOn(plane001)
|
||||
|> circle(center = [0, 0], radius = 20)
|
||||
`
|
||||
await context.addInitScript((initialCode) => {
|
||||
localStorage.setItem('persistCode', initialCode)
|
||||
}, initialCode)
|
||||
@ -1523,14 +1629,20 @@ extrude001 = extrude(profile001, length = 100)
|
||||
.toBe(1)
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'selection',
|
||||
currentArgKey: 'sketches',
|
||||
currentArgValue: '',
|
||||
headerArguments: { Selection: '' },
|
||||
highlightedHeaderArg: 'selection',
|
||||
headerArguments: { Sketches: '' },
|
||||
highlightedHeaderArg: 'sketches',
|
||||
commandName: 'Loft',
|
||||
})
|
||||
await selectSketches()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'review',
|
||||
headerArguments: { Sketches: '2 faces' },
|
||||
commandName: 'Loft',
|
||||
})
|
||||
await cmdBar.submit()
|
||||
})
|
||||
} else {
|
||||
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 toolbar.loftButton.click()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'sketches',
|
||||
currentArgValue: '',
|
||||
headerArguments: { Sketches: '' },
|
||||
highlightedHeaderArg: 'sketches',
|
||||
commandName: 'Loft',
|
||||
})
|
||||
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.y
|
||||
)
|
||||
const sweepDeclaration =
|
||||
'sweep001 = sweep(profile001, path = sketch002, sectional = false)'
|
||||
const sweepDeclaration = 'sweep001 = sweep(profile001, path = sketch002)'
|
||||
const editedSweepDeclaration =
|
||||
'sweep001 = sweep(profile001, path = sketch002, sectional = true)'
|
||||
|
||||
@ -1698,37 +1823,49 @@ sketch002 = startSketchOn(XZ)
|
||||
.toBe(1)
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'target',
|
||||
currentArgKey: 'sketches',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Sectional: '',
|
||||
Target: '',
|
||||
Trajectory: '',
|
||||
Sketches: '',
|
||||
Path: '',
|
||||
},
|
||||
highlightedHeaderArg: 'target',
|
||||
highlightedHeaderArg: 'sketches',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch1()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'trajectory',
|
||||
currentArgKey: 'path',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Sectional: '',
|
||||
Target: '1 face',
|
||||
Trajectory: '',
|
||||
Sketches: '1 face',
|
||||
Path: '',
|
||||
},
|
||||
highlightedHeaderArg: 'trajectory',
|
||||
highlightedHeaderArg: 'path',
|
||||
stage: 'arguments',
|
||||
})
|
||||
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.expectState({
|
||||
commandName: 'Sweep',
|
||||
headerArguments: {
|
||||
Target: '1 face',
|
||||
Trajectory: '1 segment',
|
||||
Sketches: '1 face',
|
||||
Path: '1 segment',
|
||||
Sectional: '',
|
||||
},
|
||||
stage: 'review',
|
||||
@ -1837,31 +1974,31 @@ sketch002 = startSketchOn(XZ)
|
||||
.toBe(1)
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'target',
|
||||
currentArgKey: 'sketches',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Sectional: '',
|
||||
Target: '',
|
||||
Trajectory: '',
|
||||
Sketches: '',
|
||||
Path: '',
|
||||
},
|
||||
highlightedHeaderArg: 'target',
|
||||
highlightedHeaderArg: 'sketches',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch1()
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
commandName: 'Sweep',
|
||||
currentArgKey: 'trajectory',
|
||||
currentArgKey: 'path',
|
||||
currentArgValue: '',
|
||||
headerArguments: {
|
||||
Sectional: '',
|
||||
Target: '1 face',
|
||||
Trajectory: '',
|
||||
Sketches: '1 face',
|
||||
Path: '',
|
||||
},
|
||||
highlightedHeaderArg: 'trajectory',
|
||||
highlightedHeaderArg: 'path',
|
||||
stage: 'arguments',
|
||||
})
|
||||
await clickOnSketch2()
|
||||
await page.waitForTimeout(500)
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(
|
||||
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()
|
||||
|
||||
const newCodeToFind = `revolve001 = revolve(sketch002, angle = 360, axis = X)`
|
||||
expect(editor.expectEditor.toContain(newCodeToFind)).toBeTruthy()
|
||||
@ -3585,6 +3723,7 @@ sketch002 = startSketchOn(extrude001, face = rectangleSegmentA001)
|
||||
await editor.scrollToText(codeToSelection)
|
||||
await page.getByText(codeToSelection).click()
|
||||
await toolbar.revolveButton.click()
|
||||
await cmdBar.progressCmdBar()
|
||||
await page.getByText('Edge', { exact: true }).click()
|
||||
const lineCodeToSelection = `angledLine(angle = 0, length = 202.6, tag = $rectangleSegmentA001)`
|
||||
await page.getByText(lineCodeToSelection).click()
|
||||
@ -3677,6 +3816,7 @@ sketch002 = startSketchOn(extrude001, face = rectangleSegmentA001)
|
||||
await page.waitForTimeout(1000)
|
||||
await editor.scrollToText(codeToSelection)
|
||||
await page.getByText(codeToSelection).click()
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect.poll(() => page.getByText('AxisOrEdge').count()).toBe(2)
|
||||
await page.getByText('Edge', { exact: true }).click()
|
||||
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 }
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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 ({
|
||||
page,
|
||||
homePage,
|
||||
cmdBar,
|
||||
toolbar,
|
||||
}) => {
|
||||
// this was a regression https://github.com/KittyCAD/modeling-app/issues/2832
|
||||
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()
|
||||
|
||||
// expect extrude button to be enabled
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Extrude' })
|
||||
).not.toBeDisabled()
|
||||
await expect(toolbar.extrudeButton).not.toBeDisabled()
|
||||
|
||||
// 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.
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'selection : 1 segment', exact: false })
|
||||
).toBeVisible({
|
||||
timeout: 10_000,
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.expectState({
|
||||
stage: 'arguments',
|
||||
currentArgKey: 'length',
|
||||
currentArgValue: '5',
|
||||
headerArguments: { Sketches: '1 segment', Length: '' },
|
||||
highlightedHeaderArg: 'length',
|
||||
commandName: 'Extrude',
|
||||
})
|
||||
})
|
||||
test("Existing sketch with bad code delete user's code", async ({
|
||||
|
@ -573,6 +573,7 @@ profile001 = startProfile(sketch002, at = [-12.34, 12.34])
|
||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
await cmdBar.progressCmdBar()
|
||||
await cmdBar.progressCmdBar()
|
||||
await expect(page.getByText('Confirm Extrude')).toBeVisible()
|
||||
await cmdBar.progressCmdBar()
|
||||
|
1
package-lock.json
generated
1
package-lock.json
generated
@ -2492,7 +2492,6 @@
|
||||
},
|
||||
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"extraneous": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -31,8 +31,6 @@ import {
|
||||
UNLABELED_ARG,
|
||||
} from '@src/lang/queryAstConstants'
|
||||
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
||||
import type { Artifact } from '@src/lang/std/artifactGraph'
|
||||
import { getPathsFromArtifact } from '@src/lang/std/artifactGraph'
|
||||
import {
|
||||
addTagForSketchOnFace,
|
||||
getConstraintInfoKw,
|
||||
@ -46,7 +44,6 @@ import {
|
||||
import type { SimplifiedArgDetails } from '@src/lang/std/stdTypes'
|
||||
import type {
|
||||
ArrayExpression,
|
||||
ArtifactGraph,
|
||||
CallExpressionKw,
|
||||
Expr,
|
||||
Literal,
|
||||
@ -340,122 +337,6 @@ export function mutateObjExpProp(
|
||||
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({
|
||||
node,
|
||||
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(
|
||||
node: Node<Program>,
|
||||
sketchPathToNode: PathToNode,
|
||||
@ -1345,7 +1169,7 @@ export function createNodeFromExprSnippet(
|
||||
export function insertVariableAndOffsetPathToNode(
|
||||
variable: KclCommandValue,
|
||||
modifiedAst: Node<Program>,
|
||||
pathToNode: PathToNode
|
||||
pathToNode?: PathToNode
|
||||
) {
|
||||
if ('variableName' in variable && variable.variableName) {
|
||||
modifiedAst.body.splice(
|
||||
@ -1353,7 +1177,7 @@ export function insertVariableAndOffsetPathToNode(
|
||||
0,
|
||||
variable.variableDeclarationAst
|
||||
)
|
||||
if (typeof pathToNode[1][0] === 'number') {
|
||||
if (pathToNode && pathToNode[1] && typeof pathToNode[1][0] === 'number') {
|
||||
pathToNode[1][0]++
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
406
src/lang/modifyAst/addSweep.ts
Normal file
406
src/lang/modifyAst/addSweep.ts
Normal 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,
|
||||
}
|
||||
}
|
@ -2,12 +2,14 @@ import type { FunctionExpression } from '@rust/kcl-lib/bindings/FunctionExpressi
|
||||
import type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement'
|
||||
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
||||
import type { TypeDeclaration } from '@rust/kcl-lib/bindings/TypeDeclaration'
|
||||
|
||||
import { createLocalName } from '@src/lang/create'
|
||||
import { createLocalName, createPipeSubstitution } from '@src/lang/create'
|
||||
import type { ToolTip } from '@src/lang/langHelpers'
|
||||
import { splitPathAtLastIndex } from '@src/lang/modifyAst'
|
||||
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 { getSketchSegmentFromSourceRange } from '@src/lang/std/sketchConstraints'
|
||||
import {
|
||||
@ -53,6 +55,7 @@ import { getAngle, isArray } from '@src/lib/utils'
|
||||
import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
|
||||
import type { KclCommandValue } from '@src/lib/commandTypes'
|
||||
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.
|
||||
@ -1053,6 +1056,125 @@ export const valueOrVariable = (variable: KclCommandValue) => {
|
||||
: 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(
|
||||
ast: Node<Program>,
|
||||
pathToNode: PathToNode
|
||||
|
@ -64,27 +64,20 @@ export type ModelingCommandSchema = {
|
||||
Extrude: {
|
||||
// Enables editing workflow
|
||||
nodeToEdit?: PathToNode
|
||||
selection: Selections // & { type: 'face' } would be cool to lock that down
|
||||
// result: (typeof EXTRUSION_RESULTS)[number]
|
||||
distance: KclCommandValue
|
||||
// KCL stdlib arguments
|
||||
sketches: Selections
|
||||
length: KclCommandValue
|
||||
}
|
||||
Sweep: {
|
||||
// Enables editing workflow
|
||||
nodeToEdit?: PathToNode
|
||||
// Arguments
|
||||
target: Selections
|
||||
trajectory: Selections
|
||||
sectional: boolean
|
||||
// KCL stdlib arguments
|
||||
sketches: Selections
|
||||
path: Selections
|
||||
sectional?: boolean
|
||||
}
|
||||
Loft: {
|
||||
selection: Selections
|
||||
}
|
||||
Shell: {
|
||||
// Enables editing workflow
|
||||
nodeToEdit?: PathToNode
|
||||
// KCL stdlib arguments
|
||||
selection: Selections
|
||||
thickness: KclCommandValue
|
||||
sketches: Selections
|
||||
}
|
||||
Revolve: {
|
||||
// Enables editing workflow
|
||||
@ -92,11 +85,18 @@ export type ModelingCommandSchema = {
|
||||
// Flow arg
|
||||
axisOrEdge: 'Axis' | 'Edge'
|
||||
// KCL stdlib arguments
|
||||
selection: Selections
|
||||
sketches: Selections
|
||||
angle: KclCommandValue
|
||||
axis: string | undefined
|
||||
edge: Selections | undefined
|
||||
}
|
||||
Shell: {
|
||||
// Enables editing workflow
|
||||
nodeToEdit?: PathToNode
|
||||
// KCL stdlib arguments
|
||||
selection: Selections
|
||||
thickness: KclCommandValue
|
||||
}
|
||||
Fillet: {
|
||||
// Enables editing workflow
|
||||
nodeToEdit?: PathToNode
|
||||
@ -388,26 +388,14 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
selection: {
|
||||
sketches: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['solid2d', 'segment'],
|
||||
multiple: false, // TODO: multiple selection
|
||||
multiple: true,
|
||||
required: true,
|
||||
skip: true,
|
||||
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
|
||||
},
|
||||
// result: {
|
||||
// inputType: 'options',
|
||||
// defaultValue: 'add',
|
||||
// skip: true,
|
||||
// required: true,
|
||||
// options: EXTRUSION_RESULTS.map((r) => ({
|
||||
// name: r,
|
||||
// isCurrent: r === 'add',
|
||||
// value: r,
|
||||
// })),
|
||||
// },
|
||||
distance: {
|
||||
length: {
|
||||
inputType: 'kcl',
|
||||
defaultValue: KCL_DEFAULT_LENGTH,
|
||||
required: true,
|
||||
@ -426,26 +414,28 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
skip: true,
|
||||
inputType: 'text',
|
||||
required: false,
|
||||
hidden: true,
|
||||
},
|
||||
target: {
|
||||
sketches: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['solid2d'],
|
||||
selectionTypes: ['solid2d', 'segment'],
|
||||
multiple: true,
|
||||
required: true,
|
||||
skip: true,
|
||||
multiple: false,
|
||||
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
|
||||
},
|
||||
trajectory: {
|
||||
path: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['segment'],
|
||||
required: true,
|
||||
skip: true,
|
||||
multiple: false,
|
||||
validation: sweepValidator,
|
||||
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
|
||||
},
|
||||
sectional: {
|
||||
inputType: 'options',
|
||||
skip: true,
|
||||
defaultValue: false,
|
||||
hidden: false,
|
||||
required: true,
|
||||
options: [
|
||||
{ name: 'False', value: false },
|
||||
@ -458,45 +448,17 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
Loft: {
|
||||
description: 'Create a 3D body by blending between two or more sketches',
|
||||
icon: 'loft',
|
||||
needsReview: false,
|
||||
needsReview: true,
|
||||
args: {
|
||||
selection: {
|
||||
sketches: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['solid2d'],
|
||||
multiple: true,
|
||||
required: true,
|
||||
skip: false,
|
||||
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: {
|
||||
description: 'Create a 3D body by rotating a sketch region about an axis.',
|
||||
icon: 'revolve',
|
||||
@ -509,12 +471,11 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
inputType: 'text',
|
||||
required: false,
|
||||
},
|
||||
selection: {
|
||||
sketches: {
|
||||
inputType: 'selection',
|
||||
selectionTypes: ['solid2d', 'segment'],
|
||||
multiple: false, // TODO: multiple selection
|
||||
multiple: true,
|
||||
required: true,
|
||||
skip: true,
|
||||
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
|
||||
},
|
||||
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': {
|
||||
description: 'Subtract one solid from another.',
|
||||
icon: 'booleanSubtract',
|
||||
|
@ -94,11 +94,12 @@ export const revolveAxisValidator = async ({
|
||||
data: { [key: string]: Selections }
|
||||
context: CommandBarContext
|
||||
}): Promise<boolean | string> => {
|
||||
if (!isSelections(context.argumentsToSubmit.selection)) {
|
||||
if (!isSelections(context.argumentsToSubmit.sketches)) {
|
||||
return 'Unable to revolve, selections are missing'
|
||||
}
|
||||
// Gotcha: this validation only works for the first sketch
|
||||
const artifact =
|
||||
context.argumentsToSubmit.selection.graphSelections[0].artifact
|
||||
context.argumentsToSubmit.sketches.graphSelections[0].artifact
|
||||
|
||||
if (!artifact) {
|
||||
return 'Unable to revolve, sketch not found'
|
||||
@ -155,16 +156,16 @@ export const loftValidator = async ({
|
||||
data: { [key: string]: Selections }
|
||||
context: CommandBarContext
|
||||
}): Promise<boolean | string> => {
|
||||
if (!isSelections(data.selection)) {
|
||||
if (!isSelections(data.sketches)) {
|
||||
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'
|
||||
}
|
||||
|
||||
const sectionIds = data.selection.graphSelections.flatMap((s) =>
|
||||
const sectionIds = sketches.graphSelections.flatMap((s) =>
|
||||
s.artifact?.type === 'solid2d' ? s.artifact.pathId : []
|
||||
)
|
||||
|
||||
@ -258,15 +259,15 @@ export const sweepValidator = async ({
|
||||
data,
|
||||
}: {
|
||||
context: CommandBarContext
|
||||
data: { trajectory: Selections }
|
||||
data: { path: Selections }
|
||||
}): Promise<boolean | string> => {
|
||||
if (!isSelections(data.trajectory)) {
|
||||
if (!isSelections(data.path)) {
|
||||
console.log('Unable to sweep, selections are missing')
|
||||
return 'Unable to sweep, selections are missing'
|
||||
}
|
||||
|
||||
// Retrieve the parent path from the segment selection directly
|
||||
const trajectoryArtifact = data.trajectory.graphSelections[0].artifact
|
||||
const trajectoryArtifact = data.path.graphSelections[0].artifact
|
||||
if (!trajectoryArtifact) {
|
||||
return "Unable to sweep, couldn't find the trajectory artifact"
|
||||
}
|
||||
@ -276,7 +277,7 @@ export const sweepValidator = async ({
|
||||
const trajectory = trajectoryArtifact.pathId
|
||||
|
||||
// Get the former arg in the command bar flow, and retrieve the path from the solid2d directly
|
||||
const targetArg = context.argumentsToSubmit['target'] as Selections
|
||||
const targetArg = context.argumentsToSubmit['sketches'] as Selections
|
||||
const targetArtifact = targetArg.graphSelections[0].artifact
|
||||
if (!targetArtifact) {
|
||||
return "Unable to sweep, couldn't find the profile artifact"
|
||||
|
@ -1,7 +1,11 @@
|
||||
import type { OpKclValue, Operation } from '@rust/kcl-lib/bindings/Operation'
|
||||
|
||||
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 type { Artifact } from '@src/lang/std/artifactGraph'
|
||||
import {
|
||||
@ -57,81 +61,51 @@ interface StdLibCallInfo {
|
||||
* Gather up the argument values for the Extrude command
|
||||
* to be used in the command bar edit flow.
|
||||
*/
|
||||
const prepareToEditExtrude: PrepareToEditCallback =
|
||||
async function prepareToEditExtrude({ operation, artifact }) {
|
||||
const baseCommand = {
|
||||
name: 'Extrude',
|
||||
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,
|
||||
}
|
||||
const prepareToEditExtrude: PrepareToEditCallback = async ({ operation }) => {
|
||||
const baseCommand = {
|
||||
name: 'Extrude',
|
||||
groupId: 'modeling',
|
||||
}
|
||||
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
|
||||
@ -446,78 +420,32 @@ const prepareToEditOffsetPlane: PrepareToEditCallback = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const prepareToEditSweep: PrepareToEditCallback = async ({
|
||||
artifact,
|
||||
operation,
|
||||
}) => {
|
||||
/**
|
||||
* Gather up the argument values for the Revolve command
|
||||
* to be used in the command bar edit flow.
|
||||
*/
|
||||
const prepareToEditSweep: PrepareToEditCallback = async ({ operation }) => {
|
||||
const baseCommand = {
|
||||
name: 'Sweep',
|
||||
groupId: 'modeling',
|
||||
}
|
||||
if (
|
||||
(operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') ||
|
||||
!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
|
||||
if (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') {
|
||||
return { reason: 'Wrong operation type' }
|
||||
}
|
||||
|
||||
// We have to go a little roundabout to get from the original artifact
|
||||
// to the solid2DId that we need to pass to the Sweep command, just like Extrude.
|
||||
const pathArtifact = getArtifactOfTypes(
|
||||
{
|
||||
key: artifact.pathId,
|
||||
types: ['path'],
|
||||
},
|
||||
// 1. Map the unlabeled arguments to solid2d selections
|
||||
const sketches = getSketchSelectionsFromOperation(
|
||||
operation,
|
||||
kclManager.artifactGraph
|
||||
)
|
||||
|
||||
if (
|
||||
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: [],
|
||||
if (err(sketches)) {
|
||||
return { reason: "Couldn't retrieve sketches" }
|
||||
}
|
||||
|
||||
// 2. Prepare labeled arguments
|
||||
// Same roundabout but twice for 'path' aka trajectory: sketch -> path -> segment
|
||||
if (!('path' in operation.labeledArgs) || !operation.labeledArgs.path) {
|
||||
return baseCommand
|
||||
}
|
||||
|
||||
if (operation.labeledArgs.path.value.type !== 'Sketch') {
|
||||
return baseCommand
|
||||
if (operation.labeledArgs.path?.value.type !== 'Sketch') {
|
||||
return { reason: "Couldn't retrieve trajectory argument" }
|
||||
}
|
||||
|
||||
const trajectoryPathArtifact = getArtifactOfTypes(
|
||||
@ -529,7 +457,7 @@ const prepareToEditSweep: PrepareToEditCallback = async ({
|
||||
)
|
||||
|
||||
if (err(trajectoryPathArtifact) || trajectoryPathArtifact.type !== 'path') {
|
||||
return baseCommand
|
||||
return { reason: "Couldn't retrieve trajectory path artifact" }
|
||||
}
|
||||
|
||||
const trajectoryArtifact = getArtifactOfTypes(
|
||||
@ -541,10 +469,11 @@ const prepareToEditSweep: PrepareToEditCallback = async ({
|
||||
)
|
||||
|
||||
if (err(trajectoryArtifact) || trajectoryArtifact.type !== 'segment') {
|
||||
return baseCommand
|
||||
console.log(trajectoryArtifact)
|
||||
return { reason: "Couldn't retrieve trajectory artifact" }
|
||||
}
|
||||
|
||||
const trajectory = {
|
||||
const path = {
|
||||
graphSelections: [
|
||||
{
|
||||
artifact: trajectoryArtifact,
|
||||
@ -554,33 +483,28 @@ const prepareToEditSweep: PrepareToEditCallback = async ({
|
||||
otherSelections: [],
|
||||
}
|
||||
|
||||
// sectional options boolean arg
|
||||
if (
|
||||
!('sectional' in operation.labeledArgs) ||
|
||||
!operation.labeledArgs.sectional
|
||||
) {
|
||||
return baseCommand
|
||||
// sectional argument from a string to a KCL expression
|
||||
let sectional: boolean | undefined
|
||||
if ('sectional' in operation.labeledArgs && operation.labeledArgs.sectional) {
|
||||
sectional =
|
||||
codeManager.code.slice(
|
||||
operation.labeledArgs.sectional.sourceRange[0],
|
||||
operation.labeledArgs.sectional.sourceRange[1]
|
||||
) === 'true'
|
||||
}
|
||||
|
||||
const sectional =
|
||||
codeManager.code.slice(
|
||||
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
|
||||
// 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['Sweep'] = {
|
||||
target: target,
|
||||
trajectory,
|
||||
sketches,
|
||||
path,
|
||||
sectional,
|
||||
nodeToEdit: getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
sourceRangeFromRust(operation.sourceRange)
|
||||
),
|
||||
}
|
||||
|
||||
return {
|
||||
...baseCommand,
|
||||
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 ({
|
||||
operation,
|
||||
artifact,
|
||||
@ -851,51 +779,22 @@ const prepareToEditRevolve: PrepareToEditCallback = async ({
|
||||
}
|
||||
if (
|
||||
!artifact ||
|
||||
!('pathId' in artifact) ||
|
||||
operation.type !== 'KclStdLibCall' ||
|
||||
!operation.labeledArgs
|
||||
) {
|
||||
return { reason: 'Wrong operation type or artifact' }
|
||||
}
|
||||
|
||||
// We have to go a little roundabout to get from the original artifact
|
||||
// to the solid2DId that we need to pass to the command.
|
||||
const pathArtifact = getArtifactOfTypes(
|
||||
{
|
||||
key: artifact.pathId,
|
||||
types: ['path'],
|
||||
},
|
||||
// 1. Map the unlabeled arguments to solid2d selections
|
||||
const sketches = getSketchSelectionsFromOperation(
|
||||
operation,
|
||||
kclManager.artifactGraph
|
||||
)
|
||||
if (
|
||||
err(pathArtifact) ||
|
||||
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: [],
|
||||
if (err(sketches)) {
|
||||
return { reason: "Couldn't retrieve sketches" }
|
||||
}
|
||||
|
||||
// 2. Prepare labeled arguments
|
||||
// axis options string arg
|
||||
if (!('axis' in operation.labeledArgs) || !operation.labeledArgs.axis) {
|
||||
return { reason: "Couldn't find axis argument" }
|
||||
@ -988,14 +887,14 @@ const prepareToEditRevolve: PrepareToEditCallback = async ({
|
||||
return { reason: 'Error in angle argument retrieval' }
|
||||
}
|
||||
|
||||
// Assemble the default argument values for the Offset Plane command,
|
||||
// with `nodeToEdit` set, which will let the Offset Plane actor know
|
||||
// 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['Revolve'] = {
|
||||
sketches,
|
||||
axisOrEdge,
|
||||
axis,
|
||||
edge,
|
||||
selection,
|
||||
angle,
|
||||
nodeToEdit: getNodePathFromSourceRange(
|
||||
kclManager.ast,
|
||||
|
@ -49,11 +49,8 @@ import {
|
||||
addHelix,
|
||||
addOffsetPlane,
|
||||
addShell,
|
||||
addSweep,
|
||||
extrudeSketch,
|
||||
insertNamedConstant,
|
||||
insertVariableAndOffsetPathToNode,
|
||||
loftSketches,
|
||||
} from '@src/lang/modifyAst'
|
||||
import type {
|
||||
ChamferParameters,
|
||||
@ -66,9 +63,12 @@ import {
|
||||
mutateAstWithTagForSketchSegment,
|
||||
} from '@src/lang/modifyAst/addEdgeTreatment'
|
||||
import {
|
||||
addExtrude,
|
||||
addLoft,
|
||||
addRevolve,
|
||||
addSweep,
|
||||
getAxisExpressionAndIndex,
|
||||
revolveSketch,
|
||||
} from '@src/lang/modifyAst/addRevolve'
|
||||
} from '@src/lang/modifyAst/addSweep'
|
||||
import {
|
||||
applyIntersectFromTargetOperatorSelections,
|
||||
applySubtractFromTargetOperatorSelections,
|
||||
@ -94,7 +94,6 @@ import {
|
||||
updatePathToNodesAfterEdit,
|
||||
valueOrVariable,
|
||||
} from '@src/lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
||||
import {
|
||||
getFaceCodeRef,
|
||||
getPathsFromPlaneArtifact,
|
||||
@ -133,7 +132,7 @@ import {
|
||||
} from '@src/lib/singletons'
|
||||
import type { ToolbarModeName } from '@src/lib/toolbar'
|
||||
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 type { ImportStatement } from '@rust/kcl-lib/bindings/ImportStatement'
|
||||
|
||||
@ -1793,69 +1792,20 @@ export const modelingMachine = setup({
|
||||
unknown,
|
||||
ModelingCommandSchema['Extrude'] | undefined
|
||||
>(async ({ input }) => {
|
||||
if (!input) return new Error('No input provided')
|
||||
const { selection, distance, nodeToEdit } = input
|
||||
const isEditing =
|
||||
nodeToEdit !== undefined && typeof nodeToEdit[1][0] === 'number'
|
||||
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(
|
||||
if (!input) return Promise.reject(new Error('No input provided'))
|
||||
const { nodeToEdit, sketches, length } = input
|
||||
const { ast } = kclManager
|
||||
const astResult = addExtrude({
|
||||
ast,
|
||||
selection.graphSelections[0]?.codeRef.range
|
||||
)
|
||||
// Add an extrude statement to the AST
|
||||
const extrudeSketchRes = extrudeSketch({
|
||||
node: ast,
|
||||
pathToNode,
|
||||
artifact: selection.graphSelections[0].artifact,
|
||||
artifactGraph: kclManager.artifactGraph,
|
||||
distance:
|
||||
'variableName' in distance
|
||||
? distance.variableIdentifierAst
|
||||
: distance.valueAst,
|
||||
extrudeName,
|
||||
sketches,
|
||||
length,
|
||||
nodeToEdit,
|
||||
})
|
||||
if (err(extrudeSketchRes)) return extrudeSketchRes
|
||||
const { modifiedAst, pathToExtrudeArg } = extrudeSketchRes
|
||||
|
||||
// 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]++
|
||||
if (err(astResult)) {
|
||||
return Promise.reject(new Error("Couldn't add extrude statement"))
|
||||
}
|
||||
|
||||
const { modifiedAst, pathToNode } = astResult
|
||||
await updateModelingState(
|
||||
modifiedAst,
|
||||
EXECUTION_TYPE_REAL,
|
||||
@ -1865,73 +1815,92 @@ export const modelingMachine = setup({
|
||||
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<
|
||||
unknown,
|
||||
ModelingCommandSchema['Revolve'] | undefined
|
||||
>(async ({ input }) => {
|
||||
if (!input) return new Error('No input provided')
|
||||
const { nodeToEdit, selection, angle, axis, edge, axisOrEdge } = input
|
||||
let ast = kclManager.ast
|
||||
let variableName: string | undefined = undefined
|
||||
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(
|
||||
if (!input) return Promise.reject(new Error('No input provided'))
|
||||
const { nodeToEdit, sketches, angle, axis, edge, axisOrEdge } = input
|
||||
const { ast } = kclManager
|
||||
const astResult = addRevolve({
|
||||
ast,
|
||||
selection.graphSelections[0]?.codeRef.range
|
||||
)
|
||||
|
||||
const revolveSketchRes = revolveSketch(
|
||||
ast,
|
||||
pathToNode,
|
||||
'variableName' in angle ? angle.variableIdentifierAst : angle.valueAst,
|
||||
sketches,
|
||||
angle,
|
||||
axisOrEdge,
|
||||
axis,
|
||||
edge,
|
||||
variableName,
|
||||
insertIndex
|
||||
)
|
||||
if (trap(revolveSketchRes)) return
|
||||
const { modifiedAst, pathToRevolveArg } = revolveSketchRes
|
||||
nodeToEdit,
|
||||
})
|
||||
if (err(astResult)) {
|
||||
return Promise.reject(astResult)
|
||||
}
|
||||
|
||||
const { modifiedAst, pathToNode } = astResult
|
||||
await updateModelingState(
|
||||
modifiedAst,
|
||||
EXECUTION_TYPE_REAL,
|
||||
@ -1941,7 +1910,7 @@ export const modelingMachine = setup({
|
||||
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(
|
||||
async ({
|
||||
input,
|
||||
|
Reference in New Issue
Block a user