Assemblies: Set translate and rotate via point-and-click (#6167)
* WIP: Add point-and-click Import for geometry Will eventually fix #6120 Right now the whole loop is there but the codemod doesn't work yet * Better pathToNOde, log on non-working cm dispatch call * Add workaround to updateModelingState not working * Back to updateModelingState with a skip flag * Better todo * Change working from Import to Insert, cleanups * Sister command in kclCommands to populate file options * Improve path selector * Unsure: move importAstMod to kclCommands onSubmit 😶 * Add e2e test * Clean up for review * Add native file menu entry and test * No await yo lint said so * WIP: UX improvements around foreign file imports Fixes #6152 * WIP: Set translate and rotate via point-and-click on imports. Boilerplate code Will eventually close #6020 * Full working loop of rotate and translate pipe mutation, including edits, only on module imports. VERY VERBOSE * Add first e2e test for set transform. Bunch of caveats listed as TODOs * @lrev-Dev's suggestion to remove a comment Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch> * Update to scene.settled(cmdBar) * Add partNNN default name for alias * Lint * Lint * Fix unit tests * Add sad path insert test Thanks @Irev-Dev for the suggestion * Add step insert test * Lint * Add test for second foreign import thru file tree click * WIP: Add point-and-click Load to copy files from outside the project into the project Towards #6210 * Move Insert button to modeling toolbar, update menus and toolbars * Add default value for local name alias * Aligning tests * Fix tests * Add padding for filenames starting with a digit * Lint * Lint * Update snapshots * Merge branch 'main' into pierremtb/issue6210-Add-point-and-click-Load-to-copy-files-from-outside-the-project-into-the-project * Add disabled transform subbutton * Allow start of Transform flow from toolbar with selection * Merge kcl-samples and local disk load into one 'Load external model' command * Fix em tests * Fix test * Add test for file pick import, better input * Fix non .kcl loading * Lint * Update snapshots * Fix issue leading to test failure * Fix clone test * Add note * Fix nested clone issue * Clean up for review * Add valueSummary for path * Fix test after path change * Clean up for review * Support much wider range for transform * Set display names * Bug fixed itself moment... * Add test for extrude tranform * Oops missed a thing * Clean up selection arg * More tests incl for variable stuff * Fix imports * Add supportsTransform: true on all solids returning operations * Fix edit flow on variables, add test * Split transform command into translate and rotate * Clean up and comment * Clean up operations.ts * Add comment * Improve assemblies test * Support more things * Typo * Fix test after unit change on import * Last clean up for review * Fix remaining test --------- Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
This commit is contained in:
@ -178,6 +178,13 @@ export class CmdBarFixture {
|
|||||||
return this.page.getByRole('option', options)
|
return this.page.getByRole('option', options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clicks the Create new variable button for kcl input
|
||||||
|
*/
|
||||||
|
createNewVariable = async () => {
|
||||||
|
await this.page.getByRole('button', { name: 'Create new variable' }).click()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Captures a snapshot of the request sent to the text-to-cad API endpoint
|
* Captures a snapshot of the request sent to the text-to-cad API endpoint
|
||||||
* and saves it to a file named after the current test.
|
* and saves it to a file named after the current test.
|
||||||
|
@ -169,6 +169,180 @@ test.describe('Point-and-click assemblies tests', () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
test(
|
||||||
|
`Insert the bracket part into an assembly and transform it`,
|
||||||
|
{ tag: ['@electron'] },
|
||||||
|
async ({
|
||||||
|
context,
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
tronApp,
|
||||||
|
}) => {
|
||||||
|
if (!tronApp) {
|
||||||
|
fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
const midPoint = { x: 500, y: 250 }
|
||||||
|
const moreToTheRightPoint = { x: 900, y: 250 }
|
||||||
|
const bgColor: [number, number, number] = [30, 30, 30]
|
||||||
|
const partColor: [number, number, number] = [100, 100, 100]
|
||||||
|
const tolerance = 30
|
||||||
|
const u = await getUtils(page)
|
||||||
|
const gizmo = page.locator('[aria-label*=gizmo]')
|
||||||
|
const resetCameraButton = page.getByRole('button', { name: 'Reset view' })
|
||||||
|
|
||||||
|
await test.step('Setup parts and expect empty assembly scene', async () => {
|
||||||
|
const projectName = 'assembly'
|
||||||
|
await context.folderSetupFn(async (dir) => {
|
||||||
|
const bracketDir = path.join(dir, projectName)
|
||||||
|
await fsp.mkdir(bracketDir, { recursive: true })
|
||||||
|
await Promise.all([
|
||||||
|
fsp.copyFile(
|
||||||
|
path.join('public', 'kcl-samples', 'bracket', 'main.kcl'),
|
||||||
|
path.join(bracketDir, 'bracket.kcl')
|
||||||
|
),
|
||||||
|
fsp.writeFile(path.join(bracketDir, 'main.kcl'), ''),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
|
await homePage.openProject(projectName)
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
await toolbar.closePane('code')
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Insert kcl as module', async () => {
|
||||||
|
await insertPartIntoAssembly(
|
||||||
|
'bracket.kcl',
|
||||||
|
'bracket',
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
page
|
||||||
|
)
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
import "bracket.kcl" as bracket
|
||||||
|
bracket
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
// Check scene for changes
|
||||||
|
await toolbar.closePane('code')
|
||||||
|
await u.doAndWaitForCmd(async () => {
|
||||||
|
await gizmo.click({ button: 'right' })
|
||||||
|
await resetCameraButton.click()
|
||||||
|
}, 'zoom_to_fit')
|
||||||
|
await toolbar.closePane('debug')
|
||||||
|
await scene.expectPixelColor(partColor, midPoint, tolerance)
|
||||||
|
await scene.expectPixelColor(bgColor, moreToTheRightPoint, tolerance)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Set translate on module', async () => {
|
||||||
|
await toolbar.openPane('feature-tree')
|
||||||
|
|
||||||
|
const op = await toolbar.getFeatureTreeOperation('bracket', 0)
|
||||||
|
await op.click({ button: 'right' })
|
||||||
|
await page.getByTestId('context-menu-set-translate').click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'x',
|
||||||
|
currentArgValue: '0',
|
||||||
|
headerArguments: {
|
||||||
|
X: '',
|
||||||
|
Y: '',
|
||||||
|
Z: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'x',
|
||||||
|
commandName: 'Translate',
|
||||||
|
})
|
||||||
|
await page.keyboard.insertText('5')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.keyboard.insertText('0.1')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.keyboard.insertText('0.2')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
X: '5',
|
||||||
|
Y: '0.1',
|
||||||
|
Z: '0.2',
|
||||||
|
},
|
||||||
|
commandName: 'Translate',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await toolbar.closePane('feature-tree')
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
bracket
|
||||||
|
|> translate(x = 5, y = 0.1, z = 0.2)
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
// Expect translated part in the scene
|
||||||
|
await scene.expectPixelColor(bgColor, midPoint, tolerance)
|
||||||
|
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Set rotate on module', async () => {
|
||||||
|
await toolbar.closePane('code')
|
||||||
|
await toolbar.openPane('feature-tree')
|
||||||
|
|
||||||
|
const op = await toolbar.getFeatureTreeOperation('bracket', 0)
|
||||||
|
await op.click({ button: 'right' })
|
||||||
|
await page.getByTestId('context-menu-set-rotate').click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'roll',
|
||||||
|
currentArgValue: '0',
|
||||||
|
headerArguments: {
|
||||||
|
Roll: '',
|
||||||
|
Pitch: '',
|
||||||
|
Yaw: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'roll',
|
||||||
|
commandName: 'Rotate',
|
||||||
|
})
|
||||||
|
await page.keyboard.insertText('0.1')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.keyboard.insertText('0.2')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.keyboard.insertText('0.3')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Roll: '0.1',
|
||||||
|
Pitch: '0.2',
|
||||||
|
Yaw: '0.3',
|
||||||
|
},
|
||||||
|
commandName: 'Rotate',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await toolbar.closePane('feature-tree')
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
bracket
|
||||||
|
|> translate(x = 5, y = 0.1, z = 0.2)
|
||||||
|
|> rotate(roll = 0.1, pitch = 0.2, yaw = 0.3)
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
// Expect no change in the scene as the rotations are tiny
|
||||||
|
await scene.expectPixelColor(bgColor, midPoint, tolerance)
|
||||||
|
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
`Insert foreign parts into assembly as whole module import`,
|
`Insert foreign parts into assembly as whole module import`,
|
||||||
{ tag: ['@electron'] },
|
{ tag: ['@electron'] },
|
||||||
|
@ -3835,4 +3835,469 @@ extrude001 = extrude(profile001, length = 100)
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const translateExtrudeCases: { variables: boolean }[] = [
|
||||||
|
{
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
translateExtrudeCases.map(({ variables }) => {
|
||||||
|
test(`Set translate on extrude through right-click menu (variables: ${variables})`, async ({
|
||||||
|
context,
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||||
|
extrude001 = extrude(profile001, length = 1)
|
||||||
|
`
|
||||||
|
await context.addInitScript((initialCode) => {
|
||||||
|
localStorage.setItem('persistCode', initialCode)
|
||||||
|
}, initialCode)
|
||||||
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
// One dumb hardcoded screen pixel value
|
||||||
|
const midPoint = { x: 500, y: 250 }
|
||||||
|
const moreToTheRightPoint = { x: 800, y: 250 }
|
||||||
|
const bgColor: [number, number, number] = [50, 50, 50]
|
||||||
|
const partColor: [number, number, number] = [150, 150, 150]
|
||||||
|
const tolerance = 50
|
||||||
|
|
||||||
|
await test.step('Confirm extrude exists with default appearance', async () => {
|
||||||
|
await toolbar.closePane('code')
|
||||||
|
await scene.expectPixelColor(partColor, midPoint, tolerance)
|
||||||
|
await scene.expectPixelColor(bgColor, moreToTheRightPoint, tolerance)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Set translate through command bar flow', async () => {
|
||||||
|
await toolbar.openPane('feature-tree')
|
||||||
|
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
|
||||||
|
await op.click({ button: 'right' })
|
||||||
|
await page.getByTestId('context-menu-set-translate').click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'x',
|
||||||
|
currentArgValue: '0',
|
||||||
|
headerArguments: {
|
||||||
|
X: '',
|
||||||
|
Y: '',
|
||||||
|
Z: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'x',
|
||||||
|
commandName: 'Translate',
|
||||||
|
})
|
||||||
|
await page.keyboard.insertText('3')
|
||||||
|
if (variables) {
|
||||||
|
await cmdBar.createNewVariable()
|
||||||
|
}
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.keyboard.insertText('0.1')
|
||||||
|
if (variables) {
|
||||||
|
await cmdBar.createNewVariable()
|
||||||
|
}
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.keyboard.insertText('0.2')
|
||||||
|
if (variables) {
|
||||||
|
await cmdBar.createNewVariable()
|
||||||
|
}
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
X: '3',
|
||||||
|
Y: '0.1',
|
||||||
|
Z: '0.2',
|
||||||
|
},
|
||||||
|
commandName: 'Translate',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await toolbar.closePane('feature-tree')
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Confirm code and scene have changed', async () => {
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
if (variables) {
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
z001 = 0.2
|
||||||
|
y001 = 0.1
|
||||||
|
x001 = 3
|
||||||
|
sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||||
|
extrude001 = extrude(profile001, length = 1)
|
||||||
|
|> translate(x = x001, y = y001, z = z001)
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||||
|
extrude001 = extrude(profile001, length = 1)
|
||||||
|
|> translate(x = 3, y = 0.1, z = 0.2)
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await scene.expectPixelColor(bgColor, midPoint, tolerance)
|
||||||
|
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Edit translate', async () => {
|
||||||
|
await toolbar.openPane('feature-tree')
|
||||||
|
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
|
||||||
|
await op.click({ button: 'right' })
|
||||||
|
await page.getByTestId('context-menu-set-translate').click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'z',
|
||||||
|
currentArgValue: variables ? 'z001' : '0.2',
|
||||||
|
headerArguments: {
|
||||||
|
X: '3',
|
||||||
|
Y: '0.1',
|
||||||
|
Z: '0.2',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'z',
|
||||||
|
commandName: 'Translate',
|
||||||
|
})
|
||||||
|
await page.keyboard.insertText('0.3')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
X: '3',
|
||||||
|
Y: '0.1',
|
||||||
|
Z: '0.3',
|
||||||
|
},
|
||||||
|
commandName: 'Translate',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await toolbar.closePane('feature-tree')
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await editor.expectEditor.toContain(`z = 0.3`)
|
||||||
|
// Expect almost no change in scene
|
||||||
|
await scene.expectPixelColor(bgColor, midPoint, tolerance)
|
||||||
|
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const rotateExtrudeCases: { variables: boolean }[] = [
|
||||||
|
{
|
||||||
|
variables: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
variables: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
rotateExtrudeCases.map(({ variables }) => {
|
||||||
|
test(`Set rotate on extrude through right-click menu (variables: ${variables})`, async ({
|
||||||
|
context,
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||||
|
extrude001 = extrude(profile001, length = 1)
|
||||||
|
`
|
||||||
|
await context.addInitScript((initialCode) => {
|
||||||
|
localStorage.setItem('persistCode', initialCode)
|
||||||
|
}, initialCode)
|
||||||
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
await test.step('Set rotate through command bar flow', async () => {
|
||||||
|
await toolbar.openPane('feature-tree')
|
||||||
|
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
|
||||||
|
await op.click({ button: 'right' })
|
||||||
|
await page.getByTestId('context-menu-set-rotate').click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'roll',
|
||||||
|
currentArgValue: '0',
|
||||||
|
headerArguments: {
|
||||||
|
Roll: '',
|
||||||
|
Pitch: '',
|
||||||
|
Yaw: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'roll',
|
||||||
|
commandName: 'Rotate',
|
||||||
|
})
|
||||||
|
await page.keyboard.insertText('1.1')
|
||||||
|
if (variables) {
|
||||||
|
await cmdBar.createNewVariable()
|
||||||
|
}
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.keyboard.insertText('1.2')
|
||||||
|
if (variables) {
|
||||||
|
await cmdBar.createNewVariable()
|
||||||
|
}
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.keyboard.insertText('1.3')
|
||||||
|
if (variables) {
|
||||||
|
await cmdBar.createNewVariable()
|
||||||
|
}
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Roll: '1.1',
|
||||||
|
Pitch: '1.2',
|
||||||
|
Yaw: '1.3',
|
||||||
|
},
|
||||||
|
commandName: 'Rotate',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await toolbar.closePane('feature-tree')
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Confirm code and scene have changed', async () => {
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
if (variables) {
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
yaw001 = 1.3
|
||||||
|
pitch001 = 1.2
|
||||||
|
roll001 = 1.1
|
||||||
|
sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||||
|
extrude001 = extrude(profile001, length = 1)
|
||||||
|
|> rotate(roll = roll001, pitch = pitch001, yaw = yaw001)
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||||
|
extrude001 = extrude(profile001, length = 1)
|
||||||
|
|> rotate(roll = 1.1, pitch = 1.2, yaw = 1.3)
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Edit rotate', async () => {
|
||||||
|
await toolbar.openPane('feature-tree')
|
||||||
|
const op = await toolbar.getFeatureTreeOperation('Extrude', 0)
|
||||||
|
await op.click({ button: 'right' })
|
||||||
|
await page.getByTestId('context-menu-set-rotate').click()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'yaw',
|
||||||
|
currentArgValue: variables ? 'yaw001' : '1.3',
|
||||||
|
headerArguments: {
|
||||||
|
Roll: '1.1',
|
||||||
|
Pitch: '1.2',
|
||||||
|
Yaw: '1.3',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'yaw',
|
||||||
|
commandName: 'Rotate',
|
||||||
|
})
|
||||||
|
await page.keyboard.insertText('13')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Roll: '1.1',
|
||||||
|
Pitch: '1.2',
|
||||||
|
Yaw: '13',
|
||||||
|
},
|
||||||
|
commandName: 'Rotate',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await toolbar.closePane('feature-tree')
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await editor.expectEditor.toContain(`yaw = 13`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test(`Set translate and rotate on extrude through selection`, async ({
|
||||||
|
context,
|
||||||
|
page,
|
||||||
|
homePage,
|
||||||
|
scene,
|
||||||
|
editor,
|
||||||
|
toolbar,
|
||||||
|
cmdBar,
|
||||||
|
}) => {
|
||||||
|
const initialCode = `sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||||
|
extrude001 = extrude(profile001, length = 1)
|
||||||
|
`
|
||||||
|
await context.addInitScript((initialCode) => {
|
||||||
|
localStorage.setItem('persistCode', initialCode)
|
||||||
|
}, initialCode)
|
||||||
|
await page.setBodyDimensions({ width: 1000, height: 500 })
|
||||||
|
await homePage.goToModelingScene()
|
||||||
|
await scene.settled(cmdBar)
|
||||||
|
|
||||||
|
// One dumb hardcoded screen pixel value
|
||||||
|
const midPoint = { x: 500, y: 250 }
|
||||||
|
const moreToTheRightPoint = { x: 800, y: 250 }
|
||||||
|
const bgColor: [number, number, number] = [50, 50, 50]
|
||||||
|
const partColor: [number, number, number] = [150, 150, 150]
|
||||||
|
const tolerance = 50
|
||||||
|
const [clickMidPoint] = scene.makeMouseHelpers(midPoint.x, midPoint.y)
|
||||||
|
const [clickMoreToTheRightPoint] = scene.makeMouseHelpers(
|
||||||
|
moreToTheRightPoint.x,
|
||||||
|
moreToTheRightPoint.y
|
||||||
|
)
|
||||||
|
|
||||||
|
await test.step('Confirm extrude exists with default appearance', async () => {
|
||||||
|
await toolbar.closePane('code')
|
||||||
|
await scene.expectPixelColor(partColor, midPoint, tolerance)
|
||||||
|
await scene.expectPixelColor(bgColor, moreToTheRightPoint, tolerance)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Set translate through command bar flow', async () => {
|
||||||
|
await cmdBar.openCmdBar()
|
||||||
|
await cmdBar.chooseCommand('Translate')
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
X: '',
|
||||||
|
Y: '',
|
||||||
|
Z: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
commandName: 'Translate',
|
||||||
|
})
|
||||||
|
await clickMidPoint()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'x',
|
||||||
|
currentArgValue: '0',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 path',
|
||||||
|
X: '',
|
||||||
|
Y: '',
|
||||||
|
Z: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'x',
|
||||||
|
commandName: 'Translate',
|
||||||
|
})
|
||||||
|
await page.keyboard.insertText('2')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 path',
|
||||||
|
X: '2',
|
||||||
|
Y: '0',
|
||||||
|
Z: '0',
|
||||||
|
},
|
||||||
|
commandName: 'Translate',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Confirm code and scene have changed', async () => {
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||||
|
extrude001 = extrude(profile001, length = 1)
|
||||||
|
|> translate(x = 2, y = 0, z = 0)
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
await scene.expectPixelColor(bgColor, midPoint, tolerance)
|
||||||
|
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Set rotate through command bar flow', async () => {
|
||||||
|
// clear selection
|
||||||
|
await clickMidPoint()
|
||||||
|
await cmdBar.openCmdBar()
|
||||||
|
await cmdBar.chooseCommand('Rotate')
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'selection',
|
||||||
|
currentArgValue: '',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '',
|
||||||
|
Roll: '',
|
||||||
|
Pitch: '',
|
||||||
|
Yaw: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'selection',
|
||||||
|
commandName: 'Rotate',
|
||||||
|
})
|
||||||
|
await clickMoreToTheRightPoint()
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'arguments',
|
||||||
|
currentArgKey: 'roll',
|
||||||
|
currentArgValue: '0',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 path',
|
||||||
|
Roll: '',
|
||||||
|
Pitch: '',
|
||||||
|
Yaw: '',
|
||||||
|
},
|
||||||
|
highlightedHeaderArg: 'roll',
|
||||||
|
commandName: 'Rotate',
|
||||||
|
})
|
||||||
|
await page.keyboard.insertText('0.1')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.keyboard.insertText('0.2')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await page.keyboard.insertText('0.3')
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
await cmdBar.expectState({
|
||||||
|
stage: 'review',
|
||||||
|
headerArguments: {
|
||||||
|
Selection: '1 path',
|
||||||
|
Roll: '0.1',
|
||||||
|
Pitch: '0.2',
|
||||||
|
Yaw: '0.3',
|
||||||
|
},
|
||||||
|
commandName: 'Rotate',
|
||||||
|
})
|
||||||
|
await cmdBar.progressCmdBar()
|
||||||
|
})
|
||||||
|
|
||||||
|
await test.step('Confirm code has changed', async () => {
|
||||||
|
await toolbar.openPane('code')
|
||||||
|
await editor.expectEditor.toContain(
|
||||||
|
`
|
||||||
|
sketch001 = startSketchOn(XZ)
|
||||||
|
profile001 = circle(sketch001, center = [0, 0], radius = 1)
|
||||||
|
extrude001 = extrude(profile001, length = 1)
|
||||||
|
|> translate(x = 2, y = 0, z = 0)
|
||||||
|
|> rotate(roll = 0.1, pitch = 0.2, yaw = 0.3)
|
||||||
|
`,
|
||||||
|
{ shouldNormalise: true }
|
||||||
|
)
|
||||||
|
// No change here since the angles are super small
|
||||||
|
await scene.expectPixelColor(bgColor, midPoint, tolerance)
|
||||||
|
await scene.expectPixelColor(partColor, moreToTheRightPoint, tolerance)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -117,7 +117,6 @@ export default function CommandBarSelectionMixedInput({
|
|||||||
Continue without selection
|
Continue without selection
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span data-testid="cmd-bar-arg-name" className="sr-only">
|
<span data-testid="cmd-bar-arg-name" className="sr-only">
|
||||||
{arg.name}
|
{arg.name}
|
||||||
</span>
|
</span>
|
||||||
|
@ -355,6 +355,38 @@ const OperationItem = (props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enterTranslateFlow() {
|
||||||
|
if (
|
||||||
|
props.item.type === 'StdLibCall' ||
|
||||||
|
props.item.type === 'KclStdLibCall' ||
|
||||||
|
props.item.type === 'GroupBegin'
|
||||||
|
) {
|
||||||
|
props.send({
|
||||||
|
type: 'enterTranslateFlow',
|
||||||
|
data: {
|
||||||
|
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
|
||||||
|
currentOperation: props.item,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterRotateFlow() {
|
||||||
|
if (
|
||||||
|
props.item.type === 'StdLibCall' ||
|
||||||
|
props.item.type === 'KclStdLibCall' ||
|
||||||
|
props.item.type === 'GroupBegin'
|
||||||
|
) {
|
||||||
|
props.send({
|
||||||
|
type: 'enterRotateFlow',
|
||||||
|
data: {
|
||||||
|
targetSourceRange: sourceRangeFromRust(props.item.sourceRange),
|
||||||
|
currentOperation: props.item,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function deleteOperation() {
|
function deleteOperation() {
|
||||||
if (
|
if (
|
||||||
props.item.type === 'StdLibCall' ||
|
props.item.type === 'StdLibCall' ||
|
||||||
@ -418,13 +450,6 @@ const OperationItem = (props: {
|
|||||||
...(props.item.type === 'StdLibCall' ||
|
...(props.item.type === 'StdLibCall' ||
|
||||||
props.item.type === 'KclStdLibCall'
|
props.item.type === 'KclStdLibCall'
|
||||||
? [
|
? [
|
||||||
<ContextMenuItem
|
|
||||||
disabled={!stdLibMap[props.item.name]?.supportsAppearance}
|
|
||||||
onClick={enterAppearanceFlow}
|
|
||||||
data-testid="context-menu-set-appearance"
|
|
||||||
>
|
|
||||||
Set appearance
|
|
||||||
</ContextMenuItem>,
|
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
disabled={!stdLibMap[props.item.name]?.prepareToEdit}
|
disabled={!stdLibMap[props.item.name]?.prepareToEdit}
|
||||||
onClick={enterEditFlow}
|
onClick={enterEditFlow}
|
||||||
@ -432,8 +457,39 @@ const OperationItem = (props: {
|
|||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</ContextMenuItem>,
|
</ContextMenuItem>,
|
||||||
|
<ContextMenuItem
|
||||||
|
disabled={!stdLibMap[props.item.name]?.supportsAppearance}
|
||||||
|
onClick={enterAppearanceFlow}
|
||||||
|
data-testid="context-menu-set-appearance"
|
||||||
|
>
|
||||||
|
Set appearance
|
||||||
|
</ContextMenuItem>,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
...(props.item.type === 'StdLibCall' ||
|
||||||
|
props.item.type === 'KclStdLibCall' ||
|
||||||
|
props.item.type === 'GroupBegin'
|
||||||
|
? [
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={enterTranslateFlow}
|
||||||
|
data-testid="context-menu-set-translate"
|
||||||
|
disabled={
|
||||||
|
props.item.type !== 'GroupBegin' &&
|
||||||
|
!stdLibMap[props.item.name]?.supportsTransform
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Set translate
|
||||||
|
</ContextMenuItem>,
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={enterRotateFlow}
|
||||||
|
data-testid="context-menu-set-rotate"
|
||||||
|
disabled={
|
||||||
|
props.item.type !== 'GroupBegin' &&
|
||||||
|
!stdLibMap[props.item.name]?.supportsTransform
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Set rotate
|
||||||
|
</ContextMenuItem>,
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onClick={deleteOperation}
|
onClick={deleteOperation}
|
||||||
hotkey="Delete"
|
hotkey="Delete"
|
||||||
@ -441,6 +497,8 @@ const OperationItem = (props: {
|
|||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</ContextMenuItem>,
|
</ContextMenuItem>,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
[props.item, props.send]
|
[props.item, props.send]
|
||||||
)
|
)
|
||||||
|
@ -54,23 +54,18 @@ export async function updateModelingState(
|
|||||||
},
|
},
|
||||||
options?: {
|
options?: {
|
||||||
focusPath?: Array<PathToNode>
|
focusPath?: Array<PathToNode>
|
||||||
skipUpdateAst?: boolean
|
|
||||||
}
|
}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let updatedAst: {
|
let updatedAst: {
|
||||||
newAst: Node<Program>
|
newAst: Node<Program>
|
||||||
selections?: Selections
|
selections?: Selections
|
||||||
} = { newAst: ast }
|
} = { newAst: ast }
|
||||||
// TODO: understand why this skip flag is needed for insertAstMod.
|
|
||||||
// It's unclear why we double casts the AST
|
|
||||||
if (!options?.skipUpdateAst) {
|
|
||||||
// Step 1: Update AST without executing (prepare selections)
|
// Step 1: Update AST without executing (prepare selections)
|
||||||
updatedAst = await dependencies.kclManager.updateAst(
|
updatedAst = await dependencies.kclManager.updateAst(
|
||||||
ast,
|
ast,
|
||||||
false, // Execution handled separately for error resilience
|
false, // Execution handled separately for error resilience
|
||||||
options
|
options
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Update the code editor and save file
|
// Step 2: Update the code editor and save file
|
||||||
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(
|
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(
|
||||||
|
@ -81,7 +81,10 @@ import type {
|
|||||||
VariableMap,
|
VariableMap,
|
||||||
} from '@src/lang/wasm'
|
} from '@src/lang/wasm'
|
||||||
import { isPathToNodeNumber, parse } from '@src/lang/wasm'
|
import { isPathToNodeNumber, parse } from '@src/lang/wasm'
|
||||||
import type { KclExpressionWithVariable } from '@src/lib/commandTypes'
|
import type {
|
||||||
|
KclCommandValue,
|
||||||
|
KclExpressionWithVariable,
|
||||||
|
} from '@src/lib/commandTypes'
|
||||||
import { KCL_DEFAULT_CONSTANT_PREFIXES } from '@src/lib/constants'
|
import { KCL_DEFAULT_CONSTANT_PREFIXES } from '@src/lib/constants'
|
||||||
import type { DefaultPlaneStr } from '@src/lib/planes'
|
import type { DefaultPlaneStr } from '@src/lib/planes'
|
||||||
import type { Selection } from '@src/lib/selections'
|
import type { Selection } from '@src/lib/selections'
|
||||||
@ -1828,3 +1831,20 @@ export function createNodeFromExprSnippet(
|
|||||||
if (!node) return new Error('No node found')
|
if (!node) return new Error('No node found')
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function insertVariableAndOffsetPathToNode(
|
||||||
|
variable: KclCommandValue,
|
||||||
|
modifiedAst: Node<Program>,
|
||||||
|
pathToNode: PathToNode
|
||||||
|
) {
|
||||||
|
if ('variableName' in variable && variable.variableName) {
|
||||||
|
modifiedAst.body.splice(
|
||||||
|
variable.insertIndex,
|
||||||
|
0,
|
||||||
|
variable.variableDeclarationAst
|
||||||
|
)
|
||||||
|
if (typeof pathToNode[1][0] === 'number') {
|
||||||
|
pathToNode[1][0]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
142
src/lang/modifyAst/setTransform.ts
Normal file
142
src/lang/modifyAst/setTransform.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import type { Node } from '@rust/kcl-lib/bindings/Node'
|
||||||
|
|
||||||
|
import {
|
||||||
|
createCallExpressionStdLibKw,
|
||||||
|
createLabeledArg,
|
||||||
|
createPipeExpression,
|
||||||
|
} from '@src/lang/create'
|
||||||
|
import { getNodeFromPath } from '@src/lang/queryAst'
|
||||||
|
import type {
|
||||||
|
CallExpressionKw,
|
||||||
|
Expr,
|
||||||
|
ExpressionStatement,
|
||||||
|
PathToNode,
|
||||||
|
PipeExpression,
|
||||||
|
Program,
|
||||||
|
VariableDeclarator,
|
||||||
|
} from '@src/lang/wasm'
|
||||||
|
import { err } from '@src/lib/trap'
|
||||||
|
|
||||||
|
export function setTranslate({
|
||||||
|
modifiedAst,
|
||||||
|
pathToNode,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
z,
|
||||||
|
}: {
|
||||||
|
modifiedAst: Node<Program>
|
||||||
|
pathToNode: PathToNode
|
||||||
|
x: Expr
|
||||||
|
y: Expr
|
||||||
|
z: Expr
|
||||||
|
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
|
||||||
|
const noPercentSign = null
|
||||||
|
const call = createCallExpressionStdLibKw('translate', noPercentSign, [
|
||||||
|
createLabeledArg('x', x),
|
||||||
|
createLabeledArg('y', y),
|
||||||
|
createLabeledArg('z', z),
|
||||||
|
])
|
||||||
|
|
||||||
|
const potentialPipe = getNodeFromPath<PipeExpression>(
|
||||||
|
modifiedAst,
|
||||||
|
pathToNode,
|
||||||
|
['PipeExpression']
|
||||||
|
)
|
||||||
|
if (!err(potentialPipe) && potentialPipe.node.type === 'PipeExpression') {
|
||||||
|
setTransformInPipe(potentialPipe.node, call)
|
||||||
|
} else {
|
||||||
|
const error = createPipeWithTransform(modifiedAst, pathToNode, call)
|
||||||
|
if (err(error)) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedAst,
|
||||||
|
pathToNode, // TODO: check if this should be updated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setRotate({
|
||||||
|
modifiedAst,
|
||||||
|
pathToNode,
|
||||||
|
roll,
|
||||||
|
pitch,
|
||||||
|
yaw,
|
||||||
|
}: {
|
||||||
|
modifiedAst: Node<Program>
|
||||||
|
pathToNode: PathToNode
|
||||||
|
roll: Expr
|
||||||
|
pitch: Expr
|
||||||
|
yaw: Expr
|
||||||
|
}): Error | { modifiedAst: Node<Program>; pathToNode: PathToNode } {
|
||||||
|
const noPercentSign = null
|
||||||
|
const call = createCallExpressionStdLibKw('rotate', noPercentSign, [
|
||||||
|
createLabeledArg('roll', roll),
|
||||||
|
createLabeledArg('pitch', pitch),
|
||||||
|
createLabeledArg('yaw', yaw),
|
||||||
|
])
|
||||||
|
|
||||||
|
const potentialPipe = getNodeFromPath<PipeExpression>(
|
||||||
|
modifiedAst,
|
||||||
|
pathToNode,
|
||||||
|
['PipeExpression']
|
||||||
|
)
|
||||||
|
if (!err(potentialPipe) && potentialPipe.node.type === 'PipeExpression') {
|
||||||
|
setTransformInPipe(potentialPipe.node, call)
|
||||||
|
} else {
|
||||||
|
const error = createPipeWithTransform(modifiedAst, pathToNode, call)
|
||||||
|
if (err(error)) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
modifiedAst,
|
||||||
|
pathToNode, // TODO: check if this should be updated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTransformInPipe(
|
||||||
|
expression: PipeExpression,
|
||||||
|
call: Node<CallExpressionKw>
|
||||||
|
) {
|
||||||
|
const existingIndex = expression.body.findIndex(
|
||||||
|
(v) =>
|
||||||
|
v.type === 'CallExpressionKw' &&
|
||||||
|
v.callee.type === 'Name' &&
|
||||||
|
v.callee.name.name === call.callee.name.name
|
||||||
|
)
|
||||||
|
if (existingIndex > -1) {
|
||||||
|
expression.body[existingIndex] = call
|
||||||
|
} else {
|
||||||
|
expression.body.push(call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPipeWithTransform(
|
||||||
|
modifiedAst: Node<Program>,
|
||||||
|
pathToNode: PathToNode,
|
||||||
|
call: Node<CallExpressionKw>
|
||||||
|
) {
|
||||||
|
const existingCall = getNodeFromPath<
|
||||||
|
VariableDeclarator | ExpressionStatement
|
||||||
|
>(modifiedAst, pathToNode, ['VariableDeclarator', 'ExpressionStatement'])
|
||||||
|
if (err(existingCall)) {
|
||||||
|
return new Error('Unsupported operation type.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingCall.node.type === 'ExpressionStatement') {
|
||||||
|
existingCall.node.expression = createPipeExpression([
|
||||||
|
existingCall.node.expression,
|
||||||
|
call,
|
||||||
|
])
|
||||||
|
} else if (existingCall.node.type === 'VariableDeclarator') {
|
||||||
|
existingCall.node.init = createPipeExpression([
|
||||||
|
existingCall.node.init,
|
||||||
|
call,
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
return new Error('Unsupported operation type.')
|
||||||
|
}
|
||||||
|
}
|
@ -51,6 +51,7 @@ import { Reason, err } from '@src/lib/trap'
|
|||||||
import { getAngle, isArray } from '@src/lib/utils'
|
import { getAngle, isArray } from '@src/lib/utils'
|
||||||
|
|
||||||
import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
|
import { ARG_INDEX_FIELD, LABELED_ARG_FIELD } from '@src/lang/queryAstConstants'
|
||||||
|
import type { KclCommandValue } from '@src/lib/commandTypes'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
|
* Retrieves a node from a given path within a Program node structure, optionally stopping at a specified node type.
|
||||||
@ -1052,3 +1053,9 @@ export function updatePathToNodesAfterEdit(
|
|||||||
newPath[1][0] = newIndex // Update the body index
|
newPath[1][0] = newIndex // Update the body index
|
||||||
return newPath
|
return newPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const valueOrVariable = (variable: KclCommandValue) => {
|
||||||
|
return 'variableName' in variable
|
||||||
|
? variable.variableIdentifierAst
|
||||||
|
: variable.valueAst
|
||||||
|
}
|
||||||
|
@ -22,7 +22,11 @@ import type {
|
|||||||
KclCommandValue,
|
KclCommandValue,
|
||||||
StateMachineCommandSetConfig,
|
StateMachineCommandSetConfig,
|
||||||
} from '@src/lib/commandTypes'
|
} from '@src/lib/commandTypes'
|
||||||
import { KCL_DEFAULT_DEGREE, KCL_DEFAULT_LENGTH } from '@src/lib/constants'
|
import {
|
||||||
|
KCL_DEFAULT_DEGREE,
|
||||||
|
KCL_DEFAULT_LENGTH,
|
||||||
|
KCL_DEFAULT_TRANSFORM,
|
||||||
|
} from '@src/lib/constants'
|
||||||
import type { components } from '@src/lib/machine-api'
|
import type { components } from '@src/lib/machine-api'
|
||||||
import type { Selections } from '@src/lib/selections'
|
import type { Selections } from '@src/lib/selections'
|
||||||
import { codeManager, kclManager } from '@src/lib/singletons'
|
import { codeManager, kclManager } from '@src/lib/singletons'
|
||||||
@ -163,6 +167,20 @@ export type ModelingCommandSchema = {
|
|||||||
nodeToEdit?: PathToNode
|
nodeToEdit?: PathToNode
|
||||||
color: string
|
color: string
|
||||||
}
|
}
|
||||||
|
Translate: {
|
||||||
|
nodeToEdit?: PathToNode
|
||||||
|
selection: Selections
|
||||||
|
x: KclCommandValue
|
||||||
|
y: KclCommandValue
|
||||||
|
z: KclCommandValue
|
||||||
|
}
|
||||||
|
Rotate: {
|
||||||
|
nodeToEdit?: PathToNode
|
||||||
|
selection: Selections
|
||||||
|
roll: KclCommandValue
|
||||||
|
pitch: KclCommandValue
|
||||||
|
yaw: KclCommandValue
|
||||||
|
}
|
||||||
'Boolean Subtract': {
|
'Boolean Subtract': {
|
||||||
target: Selections
|
target: Selections
|
||||||
tool: Selections
|
tool: Selections
|
||||||
@ -1024,6 +1042,88 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
|||||||
// Add more fields
|
// Add more fields
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Translate: {
|
||||||
|
description: 'Set translation on solid or sketch.',
|
||||||
|
icon: 'dimension', // TODO: likely not the best icon
|
||||||
|
needsReview: true,
|
||||||
|
hide: DEV || IS_NIGHTLY_OR_DEBUG ? undefined : 'both',
|
||||||
|
args: {
|
||||||
|
nodeToEdit: {
|
||||||
|
description:
|
||||||
|
'Path to the node in the AST to edit. Never shown to the user.',
|
||||||
|
skip: true,
|
||||||
|
inputType: 'text',
|
||||||
|
required: false,
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
// selectionMixed allows for feature tree selection of module imports
|
||||||
|
inputType: 'selectionMixed',
|
||||||
|
multiple: false,
|
||||||
|
required: true,
|
||||||
|
skip: true,
|
||||||
|
selectionTypes: ['path'],
|
||||||
|
selectionFilter: ['object'],
|
||||||
|
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
defaultValue: KCL_DEFAULT_TRANSFORM,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
defaultValue: KCL_DEFAULT_TRANSFORM,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
z: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
defaultValue: KCL_DEFAULT_TRANSFORM,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Rotate: {
|
||||||
|
description: 'Set rotation on solid or sketch.',
|
||||||
|
icon: 'angle', // TODO: likely not the best icon
|
||||||
|
needsReview: true,
|
||||||
|
hide: DEV || IS_NIGHTLY_OR_DEBUG ? undefined : 'both',
|
||||||
|
args: {
|
||||||
|
nodeToEdit: {
|
||||||
|
description:
|
||||||
|
'Path to the node in the AST to edit. Never shown to the user.',
|
||||||
|
skip: true,
|
||||||
|
inputType: 'text',
|
||||||
|
required: false,
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
selection: {
|
||||||
|
// selectionMixed allows for feature tree selection of module imports
|
||||||
|
inputType: 'selectionMixed',
|
||||||
|
multiple: false,
|
||||||
|
required: true,
|
||||||
|
skip: true,
|
||||||
|
selectionTypes: ['path'],
|
||||||
|
selectionFilter: ['object'],
|
||||||
|
hidden: (context) => Boolean(context.argumentsToSubmit.nodeToEdit),
|
||||||
|
},
|
||||||
|
roll: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
defaultValue: KCL_DEFAULT_TRANSFORM,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
pitch: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
defaultValue: KCL_DEFAULT_TRANSFORM,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
yaw: {
|
||||||
|
inputType: 'kcl',
|
||||||
|
defaultValue: KCL_DEFAULT_TRANSFORM,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
modelingMachineCommandConfig
|
modelingMachineCommandConfig
|
||||||
|
@ -55,6 +55,9 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
|
|||||||
/** The default KCL length expression */
|
/** The default KCL length expression */
|
||||||
export const KCL_DEFAULT_LENGTH = `5`
|
export const KCL_DEFAULT_LENGTH = `5`
|
||||||
|
|
||||||
|
/** The default KCL transform arg value that means no transform */
|
||||||
|
export const KCL_DEFAULT_TRANSFORM = `0`
|
||||||
|
|
||||||
/** The default KCL degree expression */
|
/** The default KCL degree expression */
|
||||||
export const KCL_DEFAULT_DEGREE = `360`
|
export const KCL_DEFAULT_DEGREE = `360`
|
||||||
|
|
||||||
|
@ -153,7 +153,6 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
|||||||
EXECUTION_TYPE_REAL,
|
EXECUTION_TYPE_REAL,
|
||||||
{ kclManager, editorManager, codeManager },
|
{ kclManager, editorManager, codeManager },
|
||||||
{
|
{
|
||||||
skipUpdateAst: true,
|
|
||||||
focusPath: [pathToImportNode, pathToInsertNode],
|
focusPath: [pathToImportNode, pathToInsertNode],
|
||||||
}
|
}
|
||||||
).catch(reportRejection)
|
).catch(reportRejection)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { executeAstMock } from '@src/lang/langHelpers'
|
import { executeAstMock } from '@src/lang/langHelpers'
|
||||||
import { parse, resultIsOk } from '@src/lang/wasm'
|
import { type CallExpressionKw, parse, resultIsOk } from '@src/lang/wasm'
|
||||||
import type { KclExpression } from '@src/lib/commandTypes'
|
import type { KclCommandValue, KclExpression } from '@src/lib/commandTypes'
|
||||||
import { rustContext } from '@src/lib/singletons'
|
import { rustContext } from '@src/lib/singletons'
|
||||||
import { err } from '@src/lib/trap'
|
import { err } from '@src/lib/trap'
|
||||||
|
|
||||||
@ -54,3 +54,23 @@ export async function stringToKclExpression(value: string) {
|
|||||||
valueText: value,
|
valueText: value,
|
||||||
} satisfies KclExpression
|
} satisfies KclExpression
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function retrieveArgFromPipedCallExpression(
|
||||||
|
callExpression: CallExpressionKw,
|
||||||
|
name: string
|
||||||
|
): Promise<KclCommandValue | undefined> {
|
||||||
|
const arg = callExpression.arguments.find(
|
||||||
|
(a) => a.label.type === 'Identifier' && a.label.name === name
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
arg?.type === 'LabeledArg' &&
|
||||||
|
(arg.arg.type === 'Name' || arg.arg.type === 'Literal')
|
||||||
|
) {
|
||||||
|
const value = arg.arg.type === 'Name' ? arg.arg.name.name : arg.arg.raw
|
||||||
|
const result = await stringToKclExpression(value)
|
||||||
|
if (!(err(result) || 'errors' in result)) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { OpKclValue, Operation } from '@rust/kcl-lib/bindings/Operation'
|
import type { OpKclValue, Operation } from '@rust/kcl-lib/bindings/Operation'
|
||||||
|
|
||||||
import type { CustomIconName } from '@src/components/CustomIcon'
|
import type { CustomIconName } from '@src/components/CustomIcon'
|
||||||
|
import { getNodeFromPath } from '@src/lang/queryAst'
|
||||||
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
||||||
import type { Artifact } from '@src/lang/std/artifactGraph'
|
import type { Artifact } from '@src/lang/std/artifactGraph'
|
||||||
import {
|
import {
|
||||||
@ -10,13 +11,16 @@ import {
|
|||||||
getSweepEdgeCodeRef,
|
getSweepEdgeCodeRef,
|
||||||
getWallCodeRef,
|
getWallCodeRef,
|
||||||
} from '@src/lang/std/artifactGraph'
|
} from '@src/lang/std/artifactGraph'
|
||||||
import { sourceRangeFromRust } from '@src/lang/wasm'
|
import { type PipeExpression, sourceRangeFromRust } from '@src/lang/wasm'
|
||||||
import type {
|
import type {
|
||||||
HelixModes,
|
HelixModes,
|
||||||
ModelingCommandSchema,
|
ModelingCommandSchema,
|
||||||
} from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
} from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
||||||
import type { KclExpression } from '@src/lib/commandTypes'
|
import type { KclExpression } from '@src/lib/commandTypes'
|
||||||
import { stringToKclExpression } from '@src/lib/kclHelpers'
|
import {
|
||||||
|
stringToKclExpression,
|
||||||
|
retrieveArgFromPipedCallExpression,
|
||||||
|
} from '@src/lib/kclHelpers'
|
||||||
import { isDefaultPlaneStr } from '@src/lib/planes'
|
import { isDefaultPlaneStr } from '@src/lib/planes'
|
||||||
import type { Selection, Selections } from '@src/lib/selections'
|
import type { Selection, Selections } from '@src/lib/selections'
|
||||||
import { codeManager, kclManager, rustContext } from '@src/lib/singletons'
|
import { codeManager, kclManager, rustContext } from '@src/lib/singletons'
|
||||||
@ -46,6 +50,7 @@ interface StdLibCallInfo {
|
|||||||
| PrepareToEditCallback
|
| PrepareToEditCallback
|
||||||
| PrepareToEditFailurePayload
|
| PrepareToEditFailurePayload
|
||||||
supportsAppearance?: boolean
|
supportsAppearance?: boolean
|
||||||
|
supportsTransform?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1008,6 +1013,7 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
|
|||||||
icon: 'extrude',
|
icon: 'extrude',
|
||||||
prepareToEdit: prepareToEditExtrude,
|
prepareToEdit: prepareToEditExtrude,
|
||||||
supportsAppearance: true,
|
supportsAppearance: true,
|
||||||
|
supportsTransform: true,
|
||||||
},
|
},
|
||||||
fillet: {
|
fillet: {
|
||||||
label: 'Fillet',
|
label: 'Fillet',
|
||||||
@ -1026,19 +1032,26 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
|
|||||||
hollow: {
|
hollow: {
|
||||||
label: 'Hollow',
|
label: 'Hollow',
|
||||||
icon: 'hollow',
|
icon: 'hollow',
|
||||||
|
supportsAppearance: true,
|
||||||
|
supportsTransform: true,
|
||||||
},
|
},
|
||||||
import: {
|
import: {
|
||||||
label: 'Import',
|
label: 'Import',
|
||||||
icon: 'import',
|
icon: 'import',
|
||||||
|
supportsAppearance: true,
|
||||||
|
supportsTransform: true,
|
||||||
},
|
},
|
||||||
intersect: {
|
intersect: {
|
||||||
label: 'Intersect',
|
label: 'Intersect',
|
||||||
icon: 'booleanIntersect',
|
icon: 'booleanIntersect',
|
||||||
|
supportsAppearance: true,
|
||||||
|
supportsTransform: true,
|
||||||
},
|
},
|
||||||
loft: {
|
loft: {
|
||||||
label: 'Loft',
|
label: 'Loft',
|
||||||
icon: 'loft',
|
icon: 'loft',
|
||||||
supportsAppearance: true,
|
supportsAppearance: true,
|
||||||
|
supportsTransform: true,
|
||||||
},
|
},
|
||||||
offsetPlane: {
|
offsetPlane: {
|
||||||
label: 'Offset Plane',
|
label: 'Offset Plane',
|
||||||
@ -1052,6 +1065,8 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
|
|||||||
patternCircular3d: {
|
patternCircular3d: {
|
||||||
label: 'Circular Pattern',
|
label: 'Circular Pattern',
|
||||||
icon: 'patternCircular3d',
|
icon: 'patternCircular3d',
|
||||||
|
supportsAppearance: true,
|
||||||
|
supportsTransform: true,
|
||||||
},
|
},
|
||||||
patternLinear2d: {
|
patternLinear2d: {
|
||||||
label: 'Linear Pattern',
|
label: 'Linear Pattern',
|
||||||
@ -1060,18 +1075,22 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
|
|||||||
patternLinear3d: {
|
patternLinear3d: {
|
||||||
label: 'Linear Pattern',
|
label: 'Linear Pattern',
|
||||||
icon: 'patternLinear3d',
|
icon: 'patternLinear3d',
|
||||||
|
supportsAppearance: true,
|
||||||
|
supportsTransform: true,
|
||||||
},
|
},
|
||||||
revolve: {
|
revolve: {
|
||||||
label: 'Revolve',
|
label: 'Revolve',
|
||||||
icon: 'revolve',
|
icon: 'revolve',
|
||||||
prepareToEdit: prepareToEditRevolve,
|
prepareToEdit: prepareToEditRevolve,
|
||||||
supportsAppearance: true,
|
supportsAppearance: true,
|
||||||
|
supportsTransform: true,
|
||||||
},
|
},
|
||||||
shell: {
|
shell: {
|
||||||
label: 'Shell',
|
label: 'Shell',
|
||||||
icon: 'shell',
|
icon: 'shell',
|
||||||
prepareToEdit: prepareToEditShell,
|
prepareToEdit: prepareToEditShell,
|
||||||
supportsAppearance: true,
|
supportsAppearance: true,
|
||||||
|
supportsTransform: true,
|
||||||
},
|
},
|
||||||
startSketchOn: {
|
startSketchOn: {
|
||||||
label: 'Sketch',
|
label: 'Sketch',
|
||||||
@ -1284,7 +1303,6 @@ export async function enterEditFlow({
|
|||||||
|
|
||||||
export async function enterAppearanceFlow({
|
export async function enterAppearanceFlow({
|
||||||
operation,
|
operation,
|
||||||
artifact,
|
|
||||||
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
|
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
|
||||||
if (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') {
|
if (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') {
|
||||||
return new Error(
|
return new Error(
|
||||||
@ -1300,7 +1318,6 @@ export async function enterAppearanceFlow({
|
|||||||
sourceRangeFromRust(operation.sourceRange)
|
sourceRangeFromRust(operation.sourceRange)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
console.log('argDefaultValues', argDefaultValues)
|
|
||||||
return {
|
return {
|
||||||
type: 'Find and select command',
|
type: 'Find and select command',
|
||||||
data: {
|
data: {
|
||||||
@ -1315,3 +1332,101 @@ export async function enterAppearanceFlow({
|
|||||||
'Appearance setting not yet supported for this operation. Please edit in the code editor.'
|
'Appearance setting not yet supported for this operation. Please edit in the code editor.'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function enterTranslateFlow({
|
||||||
|
operation,
|
||||||
|
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
|
||||||
|
const isModuleImport = operation.type === 'GroupBegin'
|
||||||
|
const isSupportedStdLibCall =
|
||||||
|
(operation.type === 'KclStdLibCall' || operation.type === 'StdLibCall') &&
|
||||||
|
stdLibMap[operation.name]?.supportsTransform
|
||||||
|
if (!isModuleImport && !isSupportedStdLibCall) {
|
||||||
|
return new Error(
|
||||||
|
'Unsupported operation type. Please edit in the code editor.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeToEdit = getNodePathFromSourceRange(
|
||||||
|
kclManager.ast,
|
||||||
|
sourceRangeFromRust(operation.sourceRange)
|
||||||
|
)
|
||||||
|
let x: KclExpression | undefined = undefined
|
||||||
|
let y: KclExpression | undefined = undefined
|
||||||
|
let z: KclExpression | undefined = undefined
|
||||||
|
const pipe = getNodeFromPath<PipeExpression>(
|
||||||
|
kclManager.ast,
|
||||||
|
nodeToEdit,
|
||||||
|
'PipeExpression'
|
||||||
|
)
|
||||||
|
if (!err(pipe) && pipe.node.body) {
|
||||||
|
const translate = pipe.node.body.find(
|
||||||
|
(n) => n.type === 'CallExpressionKw' && n.callee.name.name === 'translate'
|
||||||
|
)
|
||||||
|
if (translate?.type === 'CallExpressionKw') {
|
||||||
|
x = await retrieveArgFromPipedCallExpression(translate, 'x')
|
||||||
|
y = await retrieveArgFromPipedCallExpression(translate, 'y')
|
||||||
|
z = await retrieveArgFromPipedCallExpression(translate, 'z')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Won't be used since we provide nodeToEdit
|
||||||
|
const selection: Selections = { graphSelections: [], otherSelections: [] }
|
||||||
|
const argDefaultValues = { nodeToEdit, selection, x, y, z }
|
||||||
|
return {
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
name: 'Translate',
|
||||||
|
groupId: 'modeling',
|
||||||
|
argDefaultValues,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function enterRotateFlow({
|
||||||
|
operation,
|
||||||
|
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
|
||||||
|
const isModuleImport = operation.type === 'GroupBegin'
|
||||||
|
const isSupportedStdLibCall =
|
||||||
|
(operation.type === 'KclStdLibCall' || operation.type === 'StdLibCall') &&
|
||||||
|
stdLibMap[operation.name]?.supportsTransform
|
||||||
|
if (!isModuleImport && !isSupportedStdLibCall) {
|
||||||
|
return new Error(
|
||||||
|
'Unsupported operation type. Please edit in the code editor.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeToEdit = getNodePathFromSourceRange(
|
||||||
|
kclManager.ast,
|
||||||
|
sourceRangeFromRust(operation.sourceRange)
|
||||||
|
)
|
||||||
|
let roll: KclExpression | undefined = undefined
|
||||||
|
let pitch: KclExpression | undefined = undefined
|
||||||
|
let yaw: KclExpression | undefined = undefined
|
||||||
|
const pipe = getNodeFromPath<PipeExpression>(
|
||||||
|
kclManager.ast,
|
||||||
|
nodeToEdit,
|
||||||
|
'PipeExpression'
|
||||||
|
)
|
||||||
|
if (!err(pipe) && pipe.node.body) {
|
||||||
|
const rotate = pipe.node.body.find(
|
||||||
|
(n) => n.type === 'CallExpressionKw' && n.callee.name.name === 'rotate'
|
||||||
|
)
|
||||||
|
if (rotate?.type === 'CallExpressionKw') {
|
||||||
|
roll = await retrieveArgFromPipedCallExpression(rotate, 'roll')
|
||||||
|
pitch = await retrieveArgFromPipedCallExpression(rotate, 'pitch')
|
||||||
|
yaw = await retrieveArgFromPipedCallExpression(rotate, 'yaw')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Won't be used since we provide nodeToEdit
|
||||||
|
const selection: Selections = { graphSelections: [], otherSelections: [] }
|
||||||
|
const argDefaultValues = { nodeToEdit, selection, roll, pitch, yaw }
|
||||||
|
return {
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: {
|
||||||
|
name: 'Rotate',
|
||||||
|
groupId: 'modeling',
|
||||||
|
argDefaultValues,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -362,17 +362,33 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'transform',
|
id: 'translate',
|
||||||
icon: 'angle',
|
onClick: () =>
|
||||||
status: 'kcl-only',
|
commandBarActor.send({
|
||||||
title: 'Transform',
|
type: 'Find and select command',
|
||||||
description: 'Apply a translation and/or rotation to a module',
|
data: { name: 'Translate', groupId: 'modeling' },
|
||||||
onClick: () => undefined,
|
}),
|
||||||
|
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
|
||||||
|
title: 'Translate',
|
||||||
|
description: 'Apply a translation to a solid or sketch.',
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
label: 'API docs',
|
label: 'API docs',
|
||||||
url: 'https://zoo.dev/docs/kcl/translate',
|
url: 'https://zoo.dev/docs/kcl/translate',
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rotate',
|
||||||
|
onClick: () =>
|
||||||
|
commandBarActor.send({
|
||||||
|
type: 'Find and select command',
|
||||||
|
data: { name: 'Rotate', groupId: 'modeling' },
|
||||||
|
}),
|
||||||
|
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
|
||||||
|
title: 'Rotate',
|
||||||
|
description: 'Apply a rotation to a solid or sketch.',
|
||||||
|
links: [
|
||||||
{
|
{
|
||||||
label: 'API docs',
|
label: 'API docs',
|
||||||
url: 'https://zoo.dev/docs/kcl/rotate',
|
url: 'https://zoo.dev/docs/kcl/rotate',
|
||||||
|
@ -12,7 +12,12 @@ import type { Artifact } from '@src/lang/std/artifactGraph'
|
|||||||
import { getArtifactFromRange } from '@src/lang/std/artifactGraph'
|
import { getArtifactFromRange } from '@src/lang/std/artifactGraph'
|
||||||
import type { SourceRange } from '@src/lang/wasm'
|
import type { SourceRange } from '@src/lang/wasm'
|
||||||
import type { EnterEditFlowProps } from '@src/lib/operations'
|
import type { EnterEditFlowProps } from '@src/lib/operations'
|
||||||
import { enterAppearanceFlow, enterEditFlow } from '@src/lib/operations'
|
import {
|
||||||
|
enterAppearanceFlow,
|
||||||
|
enterEditFlow,
|
||||||
|
enterTranslateFlow,
|
||||||
|
enterRotateFlow,
|
||||||
|
} from '@src/lib/operations'
|
||||||
import { kclManager } from '@src/lib/singletons'
|
import { kclManager } from '@src/lib/singletons'
|
||||||
import { err } from '@src/lib/trap'
|
import { err } from '@src/lib/trap'
|
||||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||||
@ -38,6 +43,14 @@ type FeatureTreeEvent =
|
|||||||
type: 'enterAppearanceFlow'
|
type: 'enterAppearanceFlow'
|
||||||
data: { targetSourceRange: SourceRange; currentOperation: Operation }
|
data: { targetSourceRange: SourceRange; currentOperation: Operation }
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'enterTranslateFlow'
|
||||||
|
data: { targetSourceRange: SourceRange; currentOperation: Operation }
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'enterRotateFlow'
|
||||||
|
data: { targetSourceRange: SourceRange; currentOperation: Operation }
|
||||||
|
}
|
||||||
| { type: 'goToError' }
|
| { type: 'goToError' }
|
||||||
| { type: 'codePaneOpened' }
|
| { type: 'codePaneOpened' }
|
||||||
| { type: 'selected' }
|
| { type: 'selected' }
|
||||||
@ -108,6 +121,52 @@ export const featureTreeMachine = setup({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
prepareTranslateCommand: fromPromise(
|
||||||
|
({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: EnterEditFlowProps & {
|
||||||
|
commandBarSend: (typeof commandBarActor)['send']
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const { commandBarSend, ...editFlowProps } = input
|
||||||
|
enterTranslateFlow(editFlowProps)
|
||||||
|
.then((result) => {
|
||||||
|
if (err(result)) {
|
||||||
|
reject(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
input.commandBarSend(result)
|
||||||
|
resolve(result)
|
||||||
|
})
|
||||||
|
.catch(reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
),
|
||||||
|
prepareRotateCommand: fromPromise(
|
||||||
|
({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: EnterEditFlowProps & {
|
||||||
|
commandBarSend: (typeof commandBarActor)['send']
|
||||||
|
}
|
||||||
|
}) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const { commandBarSend, ...editFlowProps } = input
|
||||||
|
enterRotateFlow(editFlowProps)
|
||||||
|
.then((result) => {
|
||||||
|
if (err(result)) {
|
||||||
|
reject(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
input.commandBarSend(result)
|
||||||
|
resolve(result)
|
||||||
|
})
|
||||||
|
.catch(reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
),
|
||||||
sendDeleteCommand: fromPromise(
|
sendDeleteCommand: fromPromise(
|
||||||
({
|
({
|
||||||
input,
|
input,
|
||||||
@ -198,6 +257,16 @@ export const featureTreeMachine = setup({
|
|||||||
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
|
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
enterTranslateFlow: {
|
||||||
|
target: 'enteringTranslateFlow',
|
||||||
|
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
|
||||||
|
},
|
||||||
|
|
||||||
|
enterRotateFlow: {
|
||||||
|
target: 'enteringRotateFlow',
|
||||||
|
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
|
||||||
|
},
|
||||||
|
|
||||||
deleteOperation: {
|
deleteOperation: {
|
||||||
target: 'deletingOperation',
|
target: 'deletingOperation',
|
||||||
actions: ['saveTargetSourceRange'],
|
actions: ['saveTargetSourceRange'],
|
||||||
@ -363,6 +432,114 @@ export const featureTreeMachine = setup({
|
|||||||
exit: ['clearContext'],
|
exit: ['clearContext'],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
enteringTranslateFlow: {
|
||||||
|
states: {
|
||||||
|
selecting: {
|
||||||
|
on: {
|
||||||
|
selected: {
|
||||||
|
target: 'prepareTranslateCommand',
|
||||||
|
reenter: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
done: {
|
||||||
|
always: '#featureTree.idle',
|
||||||
|
},
|
||||||
|
|
||||||
|
prepareTranslateCommand: {
|
||||||
|
invoke: {
|
||||||
|
src: 'prepareTranslateCommand',
|
||||||
|
input: ({ context }) => {
|
||||||
|
const artifact = context.targetSourceRange
|
||||||
|
? (getArtifactFromRange(
|
||||||
|
context.targetSourceRange,
|
||||||
|
kclManager.artifactGraph
|
||||||
|
) ?? undefined)
|
||||||
|
: undefined
|
||||||
|
return {
|
||||||
|
// currentOperation is guaranteed to be defined here
|
||||||
|
operation: context.currentOperation!,
|
||||||
|
artifact,
|
||||||
|
commandBarSend: commandBarActor.send,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: 'done',
|
||||||
|
reenter: true,
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'done',
|
||||||
|
reenter: true,
|
||||||
|
actions: ({ event }) => {
|
||||||
|
if ('error' in event && err(event.error)) {
|
||||||
|
toast.error(event.error.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
initial: 'selecting',
|
||||||
|
entry: 'sendSelectionEvent',
|
||||||
|
exit: ['clearContext'],
|
||||||
|
},
|
||||||
|
|
||||||
|
enteringRotateFlow: {
|
||||||
|
states: {
|
||||||
|
selecting: {
|
||||||
|
on: {
|
||||||
|
selected: {
|
||||||
|
target: 'prepareRotateCommand',
|
||||||
|
reenter: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
done: {
|
||||||
|
always: '#featureTree.idle',
|
||||||
|
},
|
||||||
|
|
||||||
|
prepareRotateCommand: {
|
||||||
|
invoke: {
|
||||||
|
src: 'prepareRotateCommand',
|
||||||
|
input: ({ context }) => {
|
||||||
|
const artifact = context.targetSourceRange
|
||||||
|
? (getArtifactFromRange(
|
||||||
|
context.targetSourceRange,
|
||||||
|
kclManager.artifactGraph
|
||||||
|
) ?? undefined)
|
||||||
|
: undefined
|
||||||
|
return {
|
||||||
|
// currentOperation is guaranteed to be defined here
|
||||||
|
operation: context.currentOperation!,
|
||||||
|
artifact,
|
||||||
|
commandBarSend: commandBarActor.send,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
target: 'done',
|
||||||
|
reenter: true,
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: 'done',
|
||||||
|
reenter: true,
|
||||||
|
actions: ({ event }) => {
|
||||||
|
if ('error' in event && err(event.error)) {
|
||||||
|
toast.error(event.error.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
initial: 'selecting',
|
||||||
|
entry: 'sendSelectionEvent',
|
||||||
|
exit: ['clearContext'],
|
||||||
|
},
|
||||||
|
|
||||||
deletingOperation: {
|
deletingOperation: {
|
||||||
states: {
|
states: {
|
||||||
selecting: {
|
selecting: {
|
||||||
|
@ -52,6 +52,7 @@ import {
|
|||||||
deleteNodeInExtrudePipe,
|
deleteNodeInExtrudePipe,
|
||||||
extrudeSketch,
|
extrudeSketch,
|
||||||
insertNamedConstant,
|
insertNamedConstant,
|
||||||
|
insertVariableAndOffsetPathToNode,
|
||||||
loftSketches,
|
loftSketches,
|
||||||
} from '@src/lang/modifyAst'
|
} from '@src/lang/modifyAst'
|
||||||
import type {
|
import type {
|
||||||
@ -72,17 +73,21 @@ import {
|
|||||||
applyIntersectFromTargetOperatorSelections,
|
applyIntersectFromTargetOperatorSelections,
|
||||||
applySubtractFromTargetOperatorSelections,
|
applySubtractFromTargetOperatorSelections,
|
||||||
applyUnionFromTargetOperatorSelections,
|
applyUnionFromTargetOperatorSelections,
|
||||||
|
findAllChildrenAndOrderByPlaceInCode,
|
||||||
|
getLastVariable,
|
||||||
} from '@src/lang/modifyAst/boolean'
|
} from '@src/lang/modifyAst/boolean'
|
||||||
import {
|
import {
|
||||||
deleteSelectionPromise,
|
deleteSelectionPromise,
|
||||||
deletionErrorMessage,
|
deletionErrorMessage,
|
||||||
} from '@src/lang/modifyAst/deleteSelection'
|
} from '@src/lang/modifyAst/deleteSelection'
|
||||||
import { setAppearance } from '@src/lang/modifyAst/setAppearance'
|
import { setAppearance } from '@src/lang/modifyAst/setAppearance'
|
||||||
|
import { setTranslate, setRotate } from '@src/lang/modifyAst/setTransform'
|
||||||
import {
|
import {
|
||||||
getNodeFromPath,
|
getNodeFromPath,
|
||||||
isNodeSafeToReplacePath,
|
isNodeSafeToReplacePath,
|
||||||
stringifyPathToNode,
|
stringifyPathToNode,
|
||||||
updatePathToNodesAfterEdit,
|
updatePathToNodesAfterEdit,
|
||||||
|
valueOrVariable,
|
||||||
} from '@src/lang/queryAst'
|
} from '@src/lang/queryAst'
|
||||||
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
||||||
import {
|
import {
|
||||||
@ -373,6 +378,8 @@ export type ModelingMachineEvent =
|
|||||||
data: ModelingCommandSchema['Delete selection']
|
data: ModelingCommandSchema['Delete selection']
|
||||||
}
|
}
|
||||||
| { type: 'Appearance'; data: ModelingCommandSchema['Appearance'] }
|
| { type: 'Appearance'; data: ModelingCommandSchema['Appearance'] }
|
||||||
|
| { type: 'Translate'; data: ModelingCommandSchema['Translate'] }
|
||||||
|
| { type: 'Rotate'; data: ModelingCommandSchema['Rotate'] }
|
||||||
| {
|
| {
|
||||||
type:
|
type:
|
||||||
| 'Add circle origin'
|
| 'Add circle origin'
|
||||||
@ -2031,12 +2038,6 @@ export const modelingMachine = setup({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueOrVariable = (variable: KclCommandValue) => {
|
|
||||||
return 'variableName' in variable
|
|
||||||
? variable.variableIdentifierAst
|
|
||||||
: variable.valueAst
|
|
||||||
}
|
|
||||||
|
|
||||||
const { modifiedAst, pathToNode } = addHelix({
|
const { modifiedAst, pathToNode } = addHelix({
|
||||||
node: ast,
|
node: ast,
|
||||||
revolutions: valueOrVariable(revolutions),
|
revolutions: valueOrVariable(revolutions),
|
||||||
@ -2651,6 +2652,120 @@ export const modelingMachine = setup({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
translateAstMod: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: ModelingCommandSchema['Translate'] | undefined
|
||||||
|
}) => {
|
||||||
|
if (!input) return new Error('No input provided')
|
||||||
|
const ast = kclManager.ast
|
||||||
|
const modifiedAst = structuredClone(ast)
|
||||||
|
const { x, y, z, nodeToEdit, selection } = input
|
||||||
|
let pathToNode = nodeToEdit
|
||||||
|
if (!(pathToNode && typeof pathToNode[1][0] === 'number')) {
|
||||||
|
if (selection?.graphSelections[0].artifact) {
|
||||||
|
const children = findAllChildrenAndOrderByPlaceInCode(
|
||||||
|
selection?.graphSelections[0].artifact,
|
||||||
|
kclManager.artifactGraph
|
||||||
|
)
|
||||||
|
const variable = getLastVariable(children, modifiedAst)
|
||||||
|
if (!variable) {
|
||||||
|
return new Error("Couldn't find corresponding path to node")
|
||||||
|
}
|
||||||
|
pathToNode = variable.pathToNode
|
||||||
|
} else if (selection?.graphSelections[0].codeRef.pathToNode) {
|
||||||
|
pathToNode = selection?.graphSelections[0].codeRef.pathToNode
|
||||||
|
} else {
|
||||||
|
return new Error("Couldn't find corresponding path to node")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertVariableAndOffsetPathToNode(x, modifiedAst, pathToNode)
|
||||||
|
insertVariableAndOffsetPathToNode(y, modifiedAst, pathToNode)
|
||||||
|
insertVariableAndOffsetPathToNode(z, modifiedAst, pathToNode)
|
||||||
|
const result = setTranslate({
|
||||||
|
pathToNode,
|
||||||
|
modifiedAst,
|
||||||
|
x: valueOrVariable(x),
|
||||||
|
y: valueOrVariable(y),
|
||||||
|
z: valueOrVariable(z),
|
||||||
|
})
|
||||||
|
if (err(result)) {
|
||||||
|
return err(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateModelingState(
|
||||||
|
result.modifiedAst,
|
||||||
|
EXECUTION_TYPE_REAL,
|
||||||
|
{
|
||||||
|
kclManager,
|
||||||
|
editorManager,
|
||||||
|
codeManager,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
focusPath: [result.pathToNode],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
rotateAstMod: fromPromise(
|
||||||
|
async ({
|
||||||
|
input,
|
||||||
|
}: {
|
||||||
|
input: ModelingCommandSchema['Rotate'] | undefined
|
||||||
|
}) => {
|
||||||
|
if (!input) return new Error('No input provided')
|
||||||
|
const ast = kclManager.ast
|
||||||
|
const modifiedAst = structuredClone(ast)
|
||||||
|
const { roll, pitch, yaw, nodeToEdit, selection } = input
|
||||||
|
let pathToNode = nodeToEdit
|
||||||
|
if (!(pathToNode && typeof pathToNode[1][0] === 'number')) {
|
||||||
|
if (selection?.graphSelections[0].artifact) {
|
||||||
|
const children = findAllChildrenAndOrderByPlaceInCode(
|
||||||
|
selection?.graphSelections[0].artifact,
|
||||||
|
kclManager.artifactGraph
|
||||||
|
)
|
||||||
|
const variable = getLastVariable(children, modifiedAst)
|
||||||
|
if (!variable) {
|
||||||
|
return new Error("Couldn't find corresponding path to node")
|
||||||
|
}
|
||||||
|
pathToNode = variable.pathToNode
|
||||||
|
} else if (selection?.graphSelections[0].codeRef.pathToNode) {
|
||||||
|
pathToNode = selection?.graphSelections[0].codeRef.pathToNode
|
||||||
|
} else {
|
||||||
|
return new Error("Couldn't find corresponding path to node")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertVariableAndOffsetPathToNode(roll, modifiedAst, pathToNode)
|
||||||
|
insertVariableAndOffsetPathToNode(pitch, modifiedAst, pathToNode)
|
||||||
|
insertVariableAndOffsetPathToNode(yaw, modifiedAst, pathToNode)
|
||||||
|
const result = setRotate({
|
||||||
|
pathToNode,
|
||||||
|
modifiedAst,
|
||||||
|
roll: valueOrVariable(roll),
|
||||||
|
pitch: valueOrVariable(pitch),
|
||||||
|
yaw: valueOrVariable(yaw),
|
||||||
|
})
|
||||||
|
if (err(result)) {
|
||||||
|
return err(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateModelingState(
|
||||||
|
result.modifiedAst,
|
||||||
|
EXECUTION_TYPE_REAL,
|
||||||
|
{
|
||||||
|
kclManager,
|
||||||
|
editorManager,
|
||||||
|
codeManager,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
focusPath: [result.pathToNode],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
exportFromEngine: fromPromise(
|
exportFromEngine: fromPromise(
|
||||||
async ({}: { input?: ModelingCommandSchema['Export'] }) => {
|
async ({}: { input?: ModelingCommandSchema['Export'] }) => {
|
||||||
return undefined as Error | undefined
|
return undefined as Error | undefined
|
||||||
@ -2919,6 +3034,16 @@ export const modelingMachine = setup({
|
|||||||
reenter: true,
|
reenter: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Translate: {
|
||||||
|
target: 'Applying translate',
|
||||||
|
reenter: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
Rotate: {
|
||||||
|
target: 'Applying rotate',
|
||||||
|
reenter: true,
|
||||||
|
},
|
||||||
|
|
||||||
'Boolean Subtract': 'Boolean subtracting',
|
'Boolean Subtract': 'Boolean subtracting',
|
||||||
'Boolean Union': 'Boolean uniting',
|
'Boolean Union': 'Boolean uniting',
|
||||||
'Boolean Intersect': 'Boolean intersecting',
|
'Boolean Intersect': 'Boolean intersecting',
|
||||||
@ -4325,6 +4450,32 @@ export const modelingMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'Applying translate': {
|
||||||
|
invoke: {
|
||||||
|
src: 'translateAstMod',
|
||||||
|
id: 'translateAstMod',
|
||||||
|
input: ({ event }) => {
|
||||||
|
if (event.type !== 'Translate') return undefined
|
||||||
|
return event.data
|
||||||
|
},
|
||||||
|
onDone: ['idle'],
|
||||||
|
onError: ['idle'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'Applying rotate': {
|
||||||
|
invoke: {
|
||||||
|
src: 'rotateAstMod',
|
||||||
|
id: 'rotateAstMod',
|
||||||
|
input: ({ event }) => {
|
||||||
|
if (event.type !== 'Rotate') return undefined
|
||||||
|
return event.data
|
||||||
|
},
|
||||||
|
onDone: ['idle'],
|
||||||
|
onError: ['idle'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
Exporting: {
|
Exporting: {
|
||||||
invoke: {
|
invoke: {
|
||||||
src: 'exportFromEngine',
|
src: 'exportFromEngine',
|
||||||
|
Reference in New Issue
Block a user