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,15 +457,48 @@ 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"
 | 
			
		||||
              data-testid="context-menu-delete"
 | 
			
		||||
            >
 | 
			
		||||
              Delete
 | 
			
		||||
            </ContextMenuItem>,
 | 
			
		||||
          ]
 | 
			
		||||
        : []),
 | 
			
		||||
      <ContextMenuItem
 | 
			
		||||
        onClick={deleteOperation}
 | 
			
		||||
        hotkey="Delete"
 | 
			
		||||
        data-testid="context-menu-delete"
 | 
			
		||||
      >
 | 
			
		||||
        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 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