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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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(
|
||||
`Insert foreign parts into assembly as whole module import`,
|
||||
{ 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
|
||||
</button>
|
||||
)}
|
||||
|
||||
<span data-testid="cmd-bar-arg-name" className="sr-only">
|
||||
{arg.name}
|
||||
</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() {
|
||||
if (
|
||||
props.item.type === 'StdLibCall' ||
|
||||
@ -418,13 +450,6 @@ const OperationItem = (props: {
|
||||
...(props.item.type === 'StdLibCall' ||
|
||||
props.item.type === 'KclStdLibCall'
|
||||
? [
|
||||
<ContextMenuItem
|
||||
disabled={!stdLibMap[props.item.name]?.supportsAppearance}
|
||||
onClick={enterAppearanceFlow}
|
||||
data-testid="context-menu-set-appearance"
|
||||
>
|
||||
Set appearance
|
||||
</ContextMenuItem>,
|
||||
<ContextMenuItem
|
||||
disabled={!stdLibMap[props.item.name]?.prepareToEdit}
|
||||
onClick={enterEditFlow}
|
||||
@ -432,8 +457,39 @@ const OperationItem = (props: {
|
||||
>
|
||||
Edit
|
||||
</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
|
||||
onClick={deleteOperation}
|
||||
hotkey="Delete"
|
||||
@ -441,6 +497,8 @@ const OperationItem = (props: {
|
||||
>
|
||||
Delete
|
||||
</ContextMenuItem>,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[props.item, props.send]
|
||||
)
|
||||
|
@ -54,23 +54,18 @@ export async function updateModelingState(
|
||||
},
|
||||
options?: {
|
||||
focusPath?: Array<PathToNode>
|
||||
skipUpdateAst?: boolean
|
||||
}
|
||||
): Promise<void> {
|
||||
let updatedAst: {
|
||||
newAst: Node<Program>
|
||||
selections?: Selections
|
||||
} = { 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)
|
||||
updatedAst = await dependencies.kclManager.updateAst(
|
||||
ast,
|
||||
false, // Execution handled separately for error resilience
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
// Step 2: Update the code editor and save file
|
||||
await dependencies.codeManager.updateEditorWithAstAndWriteToFile(
|
||||
|
@ -81,7 +81,10 @@ import type {
|
||||
VariableMap,
|
||||
} 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 type { DefaultPlaneStr } from '@src/lib/planes'
|
||||
import type { Selection } from '@src/lib/selections'
|
||||
@ -1828,3 +1831,20 @@ export function createNodeFromExprSnippet(
|
||||
if (!node) return new Error('No node found')
|
||||
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 { 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.
|
||||
@ -1052,3 +1053,9 @@ export function updatePathToNodesAfterEdit(
|
||||
newPath[1][0] = newIndex // Update the body index
|
||||
return newPath
|
||||
}
|
||||
|
||||
export const valueOrVariable = (variable: KclCommandValue) => {
|
||||
return 'variableName' in variable
|
||||
? variable.variableIdentifierAst
|
||||
: variable.valueAst
|
||||
}
|
||||
|
@ -22,7 +22,11 @@ import type {
|
||||
KclCommandValue,
|
||||
StateMachineCommandSetConfig,
|
||||
} 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 { Selections } from '@src/lib/selections'
|
||||
import { codeManager, kclManager } from '@src/lib/singletons'
|
||||
@ -163,6 +167,20 @@ export type ModelingCommandSchema = {
|
||||
nodeToEdit?: PathToNode
|
||||
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': {
|
||||
target: Selections
|
||||
tool: Selections
|
||||
@ -1024,6 +1042,88 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
|
||||
// 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
|
||||
|
@ -55,6 +55,9 @@ export const KCL_DEFAULT_CONSTANT_PREFIXES = {
|
||||
/** The default KCL length expression */
|
||||
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 */
|
||||
export const KCL_DEFAULT_DEGREE = `360`
|
||||
|
||||
|
@ -153,7 +153,6 @@ export function kclCommands(commandProps: KclCommandConfig): Command[] {
|
||||
EXECUTION_TYPE_REAL,
|
||||
{ kclManager, editorManager, codeManager },
|
||||
{
|
||||
skipUpdateAst: true,
|
||||
focusPath: [pathToImportNode, pathToInsertNode],
|
||||
}
|
||||
).catch(reportRejection)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { executeAstMock } from '@src/lang/langHelpers'
|
||||
import { parse, resultIsOk } from '@src/lang/wasm'
|
||||
import type { KclExpression } from '@src/lib/commandTypes'
|
||||
import { type CallExpressionKw, parse, resultIsOk } from '@src/lang/wasm'
|
||||
import type { KclCommandValue, KclExpression } from '@src/lib/commandTypes'
|
||||
import { rustContext } from '@src/lib/singletons'
|
||||
import { err } from '@src/lib/trap'
|
||||
|
||||
@ -54,3 +54,23 @@ export async function stringToKclExpression(value: string) {
|
||||
valueText: value,
|
||||
} 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 { CustomIconName } from '@src/components/CustomIcon'
|
||||
import { getNodeFromPath } from '@src/lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
||||
import type { Artifact } from '@src/lang/std/artifactGraph'
|
||||
import {
|
||||
@ -10,13 +11,16 @@ import {
|
||||
getSweepEdgeCodeRef,
|
||||
getWallCodeRef,
|
||||
} from '@src/lang/std/artifactGraph'
|
||||
import { sourceRangeFromRust } from '@src/lang/wasm'
|
||||
import { type PipeExpression, sourceRangeFromRust } from '@src/lang/wasm'
|
||||
import type {
|
||||
HelixModes,
|
||||
ModelingCommandSchema,
|
||||
} from '@src/lib/commandBarConfigs/modelingCommandConfig'
|
||||
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 type { Selection, Selections } from '@src/lib/selections'
|
||||
import { codeManager, kclManager, rustContext } from '@src/lib/singletons'
|
||||
@ -46,6 +50,7 @@ interface StdLibCallInfo {
|
||||
| PrepareToEditCallback
|
||||
| PrepareToEditFailurePayload
|
||||
supportsAppearance?: boolean
|
||||
supportsTransform?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1008,6 +1013,7 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
|
||||
icon: 'extrude',
|
||||
prepareToEdit: prepareToEditExtrude,
|
||||
supportsAppearance: true,
|
||||
supportsTransform: true,
|
||||
},
|
||||
fillet: {
|
||||
label: 'Fillet',
|
||||
@ -1026,19 +1032,26 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
|
||||
hollow: {
|
||||
label: 'Hollow',
|
||||
icon: 'hollow',
|
||||
supportsAppearance: true,
|
||||
supportsTransform: true,
|
||||
},
|
||||
import: {
|
||||
label: 'Import',
|
||||
icon: 'import',
|
||||
supportsAppearance: true,
|
||||
supportsTransform: true,
|
||||
},
|
||||
intersect: {
|
||||
label: 'Intersect',
|
||||
icon: 'booleanIntersect',
|
||||
supportsAppearance: true,
|
||||
supportsTransform: true,
|
||||
},
|
||||
loft: {
|
||||
label: 'Loft',
|
||||
icon: 'loft',
|
||||
supportsAppearance: true,
|
||||
supportsTransform: true,
|
||||
},
|
||||
offsetPlane: {
|
||||
label: 'Offset Plane',
|
||||
@ -1052,6 +1065,8 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
|
||||
patternCircular3d: {
|
||||
label: 'Circular Pattern',
|
||||
icon: 'patternCircular3d',
|
||||
supportsAppearance: true,
|
||||
supportsTransform: true,
|
||||
},
|
||||
patternLinear2d: {
|
||||
label: 'Linear Pattern',
|
||||
@ -1060,18 +1075,22 @@ export const stdLibMap: Record<string, StdLibCallInfo> = {
|
||||
patternLinear3d: {
|
||||
label: 'Linear Pattern',
|
||||
icon: 'patternLinear3d',
|
||||
supportsAppearance: true,
|
||||
supportsTransform: true,
|
||||
},
|
||||
revolve: {
|
||||
label: 'Revolve',
|
||||
icon: 'revolve',
|
||||
prepareToEdit: prepareToEditRevolve,
|
||||
supportsAppearance: true,
|
||||
supportsTransform: true,
|
||||
},
|
||||
shell: {
|
||||
label: 'Shell',
|
||||
icon: 'shell',
|
||||
prepareToEdit: prepareToEditShell,
|
||||
supportsAppearance: true,
|
||||
supportsTransform: true,
|
||||
},
|
||||
startSketchOn: {
|
||||
label: 'Sketch',
|
||||
@ -1284,7 +1303,6 @@ export async function enterEditFlow({
|
||||
|
||||
export async function enterAppearanceFlow({
|
||||
operation,
|
||||
artifact,
|
||||
}: EnterEditFlowProps): Promise<Error | CommandBarMachineEvent> {
|
||||
if (operation.type !== 'StdLibCall' && operation.type !== 'KclStdLibCall') {
|
||||
return new Error(
|
||||
@ -1300,7 +1318,6 @@ export async function enterAppearanceFlow({
|
||||
sourceRangeFromRust(operation.sourceRange)
|
||||
),
|
||||
}
|
||||
console.log('argDefaultValues', argDefaultValues)
|
||||
return {
|
||||
type: 'Find and select command',
|
||||
data: {
|
||||
@ -1315,3 +1332,101 @@ export async function enterAppearanceFlow({
|
||||
'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',
|
||||
icon: 'angle',
|
||||
status: 'kcl-only',
|
||||
title: 'Transform',
|
||||
description: 'Apply a translation and/or rotation to a module',
|
||||
onClick: () => undefined,
|
||||
id: 'translate',
|
||||
onClick: () =>
|
||||
commandBarActor.send({
|
||||
type: 'Find and select command',
|
||||
data: { name: 'Translate', groupId: 'modeling' },
|
||||
}),
|
||||
status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
|
||||
title: 'Translate',
|
||||
description: 'Apply a translation to a solid or sketch.',
|
||||
links: [
|
||||
{
|
||||
label: 'API docs',
|
||||
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',
|
||||
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 type { SourceRange } from '@src/lang/wasm'
|
||||
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 { err } from '@src/lib/trap'
|
||||
import { commandBarActor } from '@src/machines/commandBarMachine'
|
||||
@ -38,6 +43,14 @@ type FeatureTreeEvent =
|
||||
type: 'enterAppearanceFlow'
|
||||
data: { targetSourceRange: SourceRange; currentOperation: Operation }
|
||||
}
|
||||
| {
|
||||
type: 'enterTranslateFlow'
|
||||
data: { targetSourceRange: SourceRange; currentOperation: Operation }
|
||||
}
|
||||
| {
|
||||
type: 'enterRotateFlow'
|
||||
data: { targetSourceRange: SourceRange; currentOperation: Operation }
|
||||
}
|
||||
| { type: 'goToError' }
|
||||
| { type: 'codePaneOpened' }
|
||||
| { 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(
|
||||
({
|
||||
input,
|
||||
@ -198,6 +257,16 @@ export const featureTreeMachine = setup({
|
||||
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
|
||||
},
|
||||
|
||||
enterTranslateFlow: {
|
||||
target: 'enteringTranslateFlow',
|
||||
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
|
||||
},
|
||||
|
||||
enterRotateFlow: {
|
||||
target: 'enteringRotateFlow',
|
||||
actions: ['saveTargetSourceRange', 'saveCurrentOperation'],
|
||||
},
|
||||
|
||||
deleteOperation: {
|
||||
target: 'deletingOperation',
|
||||
actions: ['saveTargetSourceRange'],
|
||||
@ -363,6 +432,114 @@ export const featureTreeMachine = setup({
|
||||
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: {
|
||||
states: {
|
||||
selecting: {
|
||||
|
@ -52,6 +52,7 @@ import {
|
||||
deleteNodeInExtrudePipe,
|
||||
extrudeSketch,
|
||||
insertNamedConstant,
|
||||
insertVariableAndOffsetPathToNode,
|
||||
loftSketches,
|
||||
} from '@src/lang/modifyAst'
|
||||
import type {
|
||||
@ -72,17 +73,21 @@ import {
|
||||
applyIntersectFromTargetOperatorSelections,
|
||||
applySubtractFromTargetOperatorSelections,
|
||||
applyUnionFromTargetOperatorSelections,
|
||||
findAllChildrenAndOrderByPlaceInCode,
|
||||
getLastVariable,
|
||||
} from '@src/lang/modifyAst/boolean'
|
||||
import {
|
||||
deleteSelectionPromise,
|
||||
deletionErrorMessage,
|
||||
} from '@src/lang/modifyAst/deleteSelection'
|
||||
import { setAppearance } from '@src/lang/modifyAst/setAppearance'
|
||||
import { setTranslate, setRotate } from '@src/lang/modifyAst/setTransform'
|
||||
import {
|
||||
getNodeFromPath,
|
||||
isNodeSafeToReplacePath,
|
||||
stringifyPathToNode,
|
||||
updatePathToNodesAfterEdit,
|
||||
valueOrVariable,
|
||||
} from '@src/lang/queryAst'
|
||||
import { getNodePathFromSourceRange } from '@src/lang/queryAstNodePathUtils'
|
||||
import {
|
||||
@ -373,6 +378,8 @@ export type ModelingMachineEvent =
|
||||
data: ModelingCommandSchema['Delete selection']
|
||||
}
|
||||
| { type: 'Appearance'; data: ModelingCommandSchema['Appearance'] }
|
||||
| { type: 'Translate'; data: ModelingCommandSchema['Translate'] }
|
||||
| { type: 'Rotate'; data: ModelingCommandSchema['Rotate'] }
|
||||
| {
|
||||
type:
|
||||
| '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({
|
||||
node: ast,
|
||||
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(
|
||||
async ({}: { input?: ModelingCommandSchema['Export'] }) => {
|
||||
return undefined as Error | undefined
|
||||
@ -2919,6 +3034,16 @@ export const modelingMachine = setup({
|
||||
reenter: true,
|
||||
},
|
||||
|
||||
Translate: {
|
||||
target: 'Applying translate',
|
||||
reenter: true,
|
||||
},
|
||||
|
||||
Rotate: {
|
||||
target: 'Applying rotate',
|
||||
reenter: true,
|
||||
},
|
||||
|
||||
'Boolean Subtract': 'Boolean subtracting',
|
||||
'Boolean Union': 'Boolean uniting',
|
||||
'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: {
|
||||
invoke: {
|
||||
src: 'exportFromEngine',
|
||||
|
Reference in New Issue
Block a user