Add rectangle tool to sketch mode (#2005)
* Initial draft rectangle appear on screen * rectangle tool extra * Fix draft lines in all quadrants * Wait for first click to set up draft rectangle * Working rectangle commit * Update toolbar icon and disabling logic * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * fmt * Working tool, one remaining bug around naively updating sketch nodes * Break out rectangle AST utilities * Remove unused imports * Disable Rectangle tool if sketch is not empty * Use existing tools for generating tag names * Add snapshot test for tool * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Rerun CI * Add comments, remove unrelated changes * fix rectangle bug from bad ast * Make rectangle tool equippable when the line tool is equipped * Change snapshot test to check the draft rectangle instead of commited one * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Rerun CI * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) * Rerun CI --------- Co-authored-by: Kurt Hutten Irev-Dev <k.hutten@protonmail.ch> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@ -480,6 +480,52 @@ test('Draft segments should look right', async ({ page, context }) => {
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('Draft rectangles should look right', async ({ page, context }) => {
 | 
			
		||||
  const u = getUtils(page)
 | 
			
		||||
  await page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
  const PUR = 400 / 37.5 //pixeltoUnitRatio
 | 
			
		||||
  await page.goto('/')
 | 
			
		||||
  await u.waitForAuthSkipAppStart()
 | 
			
		||||
  await u.openDebugPanel()
 | 
			
		||||
 | 
			
		||||
  await expect(
 | 
			
		||||
    page.getByRole('button', { name: 'Start Sketch' })
 | 
			
		||||
  ).not.toBeDisabled()
 | 
			
		||||
  await expect(page.getByRole('button', { name: 'Start Sketch' })).toBeVisible()
 | 
			
		||||
 | 
			
		||||
  // click on "Start Sketch" button
 | 
			
		||||
  await u.clearCommandLogs()
 | 
			
		||||
  await u.doAndWaitForImageDiff(
 | 
			
		||||
    () => page.getByRole('button', { name: 'Start Sketch' }).click(),
 | 
			
		||||
    200
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  // select a plane
 | 
			
		||||
  await page.mouse.click(700, 200)
 | 
			
		||||
 | 
			
		||||
  await expect(page.locator('.cm-content')).toHaveText(
 | 
			
		||||
    `const part001 = startSketchOn('-XZ')`
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  await page.waitForTimeout(300) // TODO detect animation ending, or disable animation
 | 
			
		||||
  await u.closeDebugPanel()
 | 
			
		||||
 | 
			
		||||
  const startXPx = 600
 | 
			
		||||
 | 
			
		||||
  // Equip the rectangle tool
 | 
			
		||||
  await page.getByRole('button', { name: 'Line' }).click()
 | 
			
		||||
  await page.getByRole('button', { name: 'Rectangle' }).click()
 | 
			
		||||
 | 
			
		||||
  // Draw the rectangle
 | 
			
		||||
  await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 30)
 | 
			
		||||
  await page.mouse.move(startXPx + PUR * 10, 500 - PUR * 10, { steps: 5 })
 | 
			
		||||
 | 
			
		||||
  // Ensure the draft rectangle looks the same as it usually does
 | 
			
		||||
  await expect(page).toHaveScreenshot({
 | 
			
		||||
    maxDiffPixels: 100,
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test.describe('Client side scene scale should match engine scale', () => {
 | 
			
		||||
  test('Inch scale', async ({ page }) => {
 | 
			
		||||
    const u = getUtils(page)
 | 
			
		||||
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB  | 
| 
		 Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB  | 
| 
		 After Width: | Height: | Size: 27 KiB  | 
| 
		 Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB  | 
| 
		 Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB  | 
| 
		 After Width: | Height: | Size: 66 KiB  | 
| 
		 Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 94 KiB  | 
| 
		 Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB  | 
@ -164,6 +164,35 @@ export const Toolbar = () => {
 | 
			
		||||
                Tangential Arc
 | 
			
		||||
              </ActionButton>
 | 
			
		||||
            </li>
 | 
			
		||||
            <li className="contents" key="rectangle-button">
 | 
			
		||||
              <ActionButton
 | 
			
		||||
                className={buttonClassName}
 | 
			
		||||
                Element="button"
 | 
			
		||||
                onClick={() =>
 | 
			
		||||
                  state.matches('Sketch.Rectangle tool')
 | 
			
		||||
                    ? send('CancelSketch')
 | 
			
		||||
                    : send('Equip rectangle tool')
 | 
			
		||||
                }
 | 
			
		||||
                aria-pressed={state.matches('Sketch.Rectangle tool')}
 | 
			
		||||
                icon={{
 | 
			
		||||
                  icon: 'rectangle',
 | 
			
		||||
                  iconClassName,
 | 
			
		||||
                  bgClassName,
 | 
			
		||||
                }}
 | 
			
		||||
                disabled={
 | 
			
		||||
                  (!state.can('Equip rectangle tool') &&
 | 
			
		||||
                    !state.matches('Sketch.Rectangle tool')) ||
 | 
			
		||||
                  disableAllButtons
 | 
			
		||||
                }
 | 
			
		||||
                title={
 | 
			
		||||
                  state.can('Equip rectangle tool')
 | 
			
		||||
                    ? 'Rectangle'
 | 
			
		||||
                    : 'Can only be used when a sketch is empty currently'
 | 
			
		||||
                }
 | 
			
		||||
              >
 | 
			
		||||
                Rectangle
 | 
			
		||||
              </ActionButton>
 | 
			
		||||
            </li>
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
        {state.matches('Sketch.SketchIdle') &&
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,7 @@ import {
 | 
			
		||||
} from './sceneInfra'
 | 
			
		||||
import { isQuaternionVertical, quaternionFromUpNForward } from './helpers'
 | 
			
		||||
import {
 | 
			
		||||
  ArrayExpression,
 | 
			
		||||
  CallExpression,
 | 
			
		||||
  getTangentialArcToInfo,
 | 
			
		||||
  parse,
 | 
			
		||||
@ -73,12 +74,14 @@ import {
 | 
			
		||||
  changeSketchArguments,
 | 
			
		||||
  updateStartProfileAtArgs,
 | 
			
		||||
} from 'lang/std/sketch'
 | 
			
		||||
import { throttle } from 'lib/utils'
 | 
			
		||||
import { roundOff, throttle } from 'lib/utils'
 | 
			
		||||
import {
 | 
			
		||||
  createArrayExpression,
 | 
			
		||||
  createCallExpressionStdLib,
 | 
			
		||||
  createLiteral,
 | 
			
		||||
  createPipeExpression,
 | 
			
		||||
  createPipeSubstitution,
 | 
			
		||||
  findUniqueName,
 | 
			
		||||
} from 'lang/modifyAst'
 | 
			
		||||
import {
 | 
			
		||||
  getEventForSegmentSelection,
 | 
			
		||||
@ -90,6 +93,10 @@ import { Models } from '@kittycad/lib'
 | 
			
		||||
import { uuidv4 } from 'lib/utils'
 | 
			
		||||
import { SketchDetails } from 'machines/modelingMachine'
 | 
			
		||||
import { EngineCommandManager } from 'lang/std/engineConnection'
 | 
			
		||||
import {
 | 
			
		||||
  getRectangleCallExpressions,
 | 
			
		||||
  updateRectangleSketch,
 | 
			
		||||
} from 'lib/rectangleTool'
 | 
			
		||||
 | 
			
		||||
type DraftSegment = 'line' | 'tangentialArcTo'
 | 
			
		||||
 | 
			
		||||
@ -340,7 +347,7 @@ export class SceneEntities {
 | 
			
		||||
      sceneInfra._baseUnitMultiplier
 | 
			
		||||
 | 
			
		||||
    const segPathToNode = getNodePathFromSourceRange(
 | 
			
		||||
      kclManager.ast,
 | 
			
		||||
      maybeModdedAst,
 | 
			
		||||
      sketchGroup.start.__geoMeta.sourceRange
 | 
			
		||||
    )
 | 
			
		||||
    const _profileStart = profileStart({
 | 
			
		||||
@ -358,7 +365,7 @@ export class SceneEntities {
 | 
			
		||||
 | 
			
		||||
    sketchGroup.value.forEach((segment, index) => {
 | 
			
		||||
      let segPathToNode = getNodePathFromSourceRange(
 | 
			
		||||
        kclManager.ast,
 | 
			
		||||
        maybeModdedAst,
 | 
			
		||||
        segment.__geoMeta.sourceRange
 | 
			
		||||
      )
 | 
			
		||||
      if (
 | 
			
		||||
@ -368,7 +375,7 @@ export class SceneEntities {
 | 
			
		||||
        const previousSegment =
 | 
			
		||||
          sketchGroup.value[index - 1] || sketchGroup.start
 | 
			
		||||
        const previousSegmentPathToNode = getNodePathFromSourceRange(
 | 
			
		||||
          kclManager.ast,
 | 
			
		||||
          maybeModdedAst,
 | 
			
		||||
          previousSegment.__geoMeta.sourceRange
 | 
			
		||||
        )
 | 
			
		||||
        const bodyIndex = previousSegmentPathToNode[1][0]
 | 
			
		||||
@ -384,7 +391,7 @@ export class SceneEntities {
 | 
			
		||||
        index >= draftExpressionsIndices.start
 | 
			
		||||
      let seg
 | 
			
		||||
      const callExpName = getNodeFromPath<CallExpression>(
 | 
			
		||||
        kclManager.ast,
 | 
			
		||||
        maybeModdedAst,
 | 
			
		||||
        segPathToNode,
 | 
			
		||||
        'CallExpression'
 | 
			
		||||
      )?.node?.callee?.name
 | 
			
		||||
@ -572,6 +579,173 @@ export class SceneEntities {
 | 
			
		||||
      ...this.mouseEnterLeaveCallbacks(),
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  setupRectangleOriginListener = () => {
 | 
			
		||||
    sceneInfra.setCallbacks({
 | 
			
		||||
      onClick: (args) => {
 | 
			
		||||
        const twoD = args.intersectionPoint?.twoD
 | 
			
		||||
        if (!twoD) {
 | 
			
		||||
          console.warn(`This click didn't have a 2D intersection`, args)
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        sceneInfra.modelingSend({
 | 
			
		||||
          type: 'Add rectangle origin',
 | 
			
		||||
          data: [twoD.x, twoD.y],
 | 
			
		||||
        })
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  setupDraftRectangle = async (
 | 
			
		||||
    sketchPathToNode: PathToNode,
 | 
			
		||||
    forward: [number, number, number],
 | 
			
		||||
    up: [number, number, number],
 | 
			
		||||
    sketchOrigin: [number, number, number],
 | 
			
		||||
    rectangleOrigin: [x: number, y: number]
 | 
			
		||||
  ) => {
 | 
			
		||||
    let _ast = JSON.parse(JSON.stringify(kclManager.ast))
 | 
			
		||||
 | 
			
		||||
    const variableDeclarationName =
 | 
			
		||||
      getNodeFromPath<VariableDeclaration>(
 | 
			
		||||
        _ast,
 | 
			
		||||
        sketchPathToNode || [],
 | 
			
		||||
        'VariableDeclaration'
 | 
			
		||||
      )?.node?.declarations?.[0]?.id?.name || ''
 | 
			
		||||
 | 
			
		||||
    const tags: [string, string, string] = [
 | 
			
		||||
      findUniqueName(_ast, 'rectangleSegmentA'),
 | 
			
		||||
      findUniqueName(_ast, 'rectangleSegmentB'),
 | 
			
		||||
      findUniqueName(_ast, 'rectangleSegmentC'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    const startSketchOn = getNodeFromPath<VariableDeclaration>(
 | 
			
		||||
      _ast,
 | 
			
		||||
      sketchPathToNode || [],
 | 
			
		||||
      'VariableDeclaration'
 | 
			
		||||
    )?.node?.declarations
 | 
			
		||||
 | 
			
		||||
    const startSketchOnInit = startSketchOn?.[0]?.init
 | 
			
		||||
    startSketchOn[0].init = createPipeExpression([
 | 
			
		||||
      startSketchOnInit,
 | 
			
		||||
      ...getRectangleCallExpressions(rectangleOrigin, tags),
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    _ast = parse(recast(_ast))
 | 
			
		||||
 | 
			
		||||
    const { programMemoryOverride, truncatedAst } = await this.setupSketch({
 | 
			
		||||
      sketchPathToNode,
 | 
			
		||||
      forward,
 | 
			
		||||
      up,
 | 
			
		||||
      position: sketchOrigin,
 | 
			
		||||
      maybeModdedAst: _ast,
 | 
			
		||||
      draftExpressionsIndices: { start: 0, end: 3 },
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    sceneInfra.setCallbacks({
 | 
			
		||||
      onMove: async (args) => {
 | 
			
		||||
        // Update the width and height of the draft rectangle
 | 
			
		||||
        const pathToNodeTwo = JSON.parse(JSON.stringify(sketchPathToNode))
 | 
			
		||||
        pathToNodeTwo[1][0] = 0
 | 
			
		||||
 | 
			
		||||
        const sketchInit = getNodeFromPath<VariableDeclaration>(
 | 
			
		||||
          truncatedAst,
 | 
			
		||||
          pathToNodeTwo || [],
 | 
			
		||||
          'VariableDeclaration'
 | 
			
		||||
        )?.node?.declarations?.[0]?.init
 | 
			
		||||
 | 
			
		||||
        const x = (args.intersectionPoint.twoD.x || 0) - rectangleOrigin[0]
 | 
			
		||||
        const y = (args.intersectionPoint.twoD.y || 0) - rectangleOrigin[1]
 | 
			
		||||
 | 
			
		||||
        if (sketchInit.type === 'PipeExpression') {
 | 
			
		||||
          updateRectangleSketch(sketchInit, x, y, tags[0])
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const { programMemory } = await executeAst({
 | 
			
		||||
          ast: truncatedAst,
 | 
			
		||||
          useFakeExecutor: true,
 | 
			
		||||
          engineCommandManager: this.engineCommandManager,
 | 
			
		||||
          programMemoryOverride,
 | 
			
		||||
        })
 | 
			
		||||
        this.sceneProgramMemory = programMemory
 | 
			
		||||
        const sketchGroup = programMemory.root[
 | 
			
		||||
          variableDeclarationName
 | 
			
		||||
        ] as SketchGroup
 | 
			
		||||
        const sgPaths = sketchGroup.value
 | 
			
		||||
        const orthoFactor = orthoScale(sceneInfra.camControls.camera)
 | 
			
		||||
 | 
			
		||||
        this.updateSegment(
 | 
			
		||||
          sketchGroup.start,
 | 
			
		||||
          0,
 | 
			
		||||
          0,
 | 
			
		||||
          _ast,
 | 
			
		||||
          orthoFactor,
 | 
			
		||||
          sketchGroup
 | 
			
		||||
        )
 | 
			
		||||
        sgPaths.forEach((seg, index) =>
 | 
			
		||||
          this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      onClick: async (args) => {
 | 
			
		||||
        // Commit the rectangle to the full AST/code and return to sketch.idle
 | 
			
		||||
        const cornerPoint = args.intersectionPoint?.twoD
 | 
			
		||||
        if (!cornerPoint || args.mouseEvent.button !== 0) return
 | 
			
		||||
 | 
			
		||||
        const x = roundOff((cornerPoint.x || 0) - rectangleOrigin[0])
 | 
			
		||||
        const y = roundOff((cornerPoint.y || 0) - rectangleOrigin[1])
 | 
			
		||||
 | 
			
		||||
        const sketchInit = getNodeFromPath<VariableDeclaration>(
 | 
			
		||||
          _ast,
 | 
			
		||||
          sketchPathToNode || [],
 | 
			
		||||
          'VariableDeclaration'
 | 
			
		||||
        )?.node?.declarations?.[0]?.init
 | 
			
		||||
 | 
			
		||||
        if (sketchInit.type === 'PipeExpression') {
 | 
			
		||||
          updateRectangleSketch(sketchInit, x, y, tags[0])
 | 
			
		||||
 | 
			
		||||
          _ast = parse(recast(_ast))
 | 
			
		||||
 | 
			
		||||
          console.log('onClick', {
 | 
			
		||||
            sketchInit: sketchInit,
 | 
			
		||||
            _ast,
 | 
			
		||||
            x,
 | 
			
		||||
            y,
 | 
			
		||||
            truncatedAst,
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          // Update the primary AST and unequip the rectangle tool
 | 
			
		||||
          await kclManager.executeAstMock(_ast)
 | 
			
		||||
          sceneInfra.modelingSend({ type: 'CancelSketch' })
 | 
			
		||||
 | 
			
		||||
          const { programMemory } = await executeAst({
 | 
			
		||||
            ast: _ast,
 | 
			
		||||
            useFakeExecutor: true,
 | 
			
		||||
            engineCommandManager: this.engineCommandManager,
 | 
			
		||||
            programMemoryOverride,
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          // Prepare to update the THREEjs scene
 | 
			
		||||
          this.sceneProgramMemory = programMemory
 | 
			
		||||
          const sketchGroup = programMemory.root[
 | 
			
		||||
            variableDeclarationName
 | 
			
		||||
          ] as SketchGroup
 | 
			
		||||
          const sgPaths = sketchGroup.value
 | 
			
		||||
          const orthoFactor = orthoScale(sceneInfra.camControls.camera)
 | 
			
		||||
 | 
			
		||||
          // Update the starting segment of the THREEjs scene
 | 
			
		||||
          this.updateSegment(
 | 
			
		||||
            sketchGroup.start,
 | 
			
		||||
            0,
 | 
			
		||||
            0,
 | 
			
		||||
            _ast,
 | 
			
		||||
            orthoFactor,
 | 
			
		||||
            sketchGroup
 | 
			
		||||
          )
 | 
			
		||||
          // Update the rest of the segments of the THREEjs scene
 | 
			
		||||
          sgPaths.forEach((seg, index) =>
 | 
			
		||||
            this.updateSegment(seg, index, 0, _ast, orthoFactor, sketchGroup)
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  setupSketchIdleCallbacks = ({
 | 
			
		||||
    pathToNode,
 | 
			
		||||
    up,
 | 
			
		||||
@ -803,53 +977,85 @@ export class SceneEntities {
 | 
			
		||||
      const sgPaths = sketchGroup.value
 | 
			
		||||
      const orthoFactor = orthoScale(sceneInfra.camControls.camera)
 | 
			
		||||
 | 
			
		||||
      const updateSegment = (
 | 
			
		||||
        segment: Path | SketchGroup['start'],
 | 
			
		||||
        index: number
 | 
			
		||||
      ) => {
 | 
			
		||||
        const segPathToNode = getNodePathFromSourceRange(
 | 
			
		||||
      this.updateSegment(
 | 
			
		||||
        sketchGroup.start,
 | 
			
		||||
        0,
 | 
			
		||||
        varDecIndex,
 | 
			
		||||
        modifiedAst,
 | 
			
		||||
        orthoFactor,
 | 
			
		||||
        sketchGroup
 | 
			
		||||
      )
 | 
			
		||||
      sgPaths.forEach((group, index) =>
 | 
			
		||||
        this.updateSegment(
 | 
			
		||||
          group,
 | 
			
		||||
          index,
 | 
			
		||||
          varDecIndex,
 | 
			
		||||
          modifiedAst,
 | 
			
		||||
          segment.__geoMeta.sourceRange
 | 
			
		||||
          orthoFactor,
 | 
			
		||||
          sketchGroup
 | 
			
		||||
        )
 | 
			
		||||
        const originalPathToNodeStr = JSON.stringify(segPathToNode)
 | 
			
		||||
        segPathToNode[1][0] = varDecIndex
 | 
			
		||||
        const pathToNodeStr = JSON.stringify(segPathToNode)
 | 
			
		||||
        // more hacks to hopefully be solved by proper pathToNode info in memory/sketchGroup segments
 | 
			
		||||
        const group =
 | 
			
		||||
          this.activeSegments[pathToNodeStr] ||
 | 
			
		||||
          this.activeSegments[originalPathToNodeStr]
 | 
			
		||||
        // const prevSegment = sketchGroup.slice(index - 1)[0]
 | 
			
		||||
        const type = group?.userData?.type
 | 
			
		||||
        const factor =
 | 
			
		||||
          (sceneInfra.camControls.camera instanceof OrthographicCamera
 | 
			
		||||
            ? orthoFactor
 | 
			
		||||
            : perspScale(sceneInfra.camControls.camera, group)) /
 | 
			
		||||
          sceneInfra._baseUnitMultiplier
 | 
			
		||||
        if (type === TANGENTIAL_ARC_TO_SEGMENT) {
 | 
			
		||||
          this.updateTangentialArcToSegment({
 | 
			
		||||
            prevSegment: sgPaths[index - 1],
 | 
			
		||||
            from: segment.from,
 | 
			
		||||
            to: segment.to,
 | 
			
		||||
            group: group,
 | 
			
		||||
            scale: factor,
 | 
			
		||||
          })
 | 
			
		||||
        } else if (type === STRAIGHT_SEGMENT) {
 | 
			
		||||
          this.updateStraightSegment({
 | 
			
		||||
            from: segment.from,
 | 
			
		||||
            to: segment.to,
 | 
			
		||||
            group: group,
 | 
			
		||||
            scale: factor,
 | 
			
		||||
          })
 | 
			
		||||
        } else if (type === PROFILE_START) {
 | 
			
		||||
          group.position.set(segment.from[0], segment.from[1], 0)
 | 
			
		||||
          group.scale.set(factor, factor, factor)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      updateSegment(sketchGroup.start, 0)
 | 
			
		||||
      sgPaths.forEach(updateSegment)
 | 
			
		||||
      )
 | 
			
		||||
    })()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Update the THREEjs sketch entities with new segment data
 | 
			
		||||
   * mapping them back to the AST
 | 
			
		||||
   * @param segment
 | 
			
		||||
   * @param index
 | 
			
		||||
   * @param varDecIndex
 | 
			
		||||
   * @param modifiedAst
 | 
			
		||||
   * @param orthoFactor
 | 
			
		||||
   * @param sketchGroup
 | 
			
		||||
   */
 | 
			
		||||
  updateSegment = (
 | 
			
		||||
    segment: Path | SketchGroup['start'],
 | 
			
		||||
    index: number,
 | 
			
		||||
    varDecIndex: number,
 | 
			
		||||
    modifiedAst: Program,
 | 
			
		||||
    orthoFactor: number,
 | 
			
		||||
    sketchGroup: SketchGroup
 | 
			
		||||
  ) => {
 | 
			
		||||
    const segPathToNode = getNodePathFromSourceRange(
 | 
			
		||||
      modifiedAst,
 | 
			
		||||
      segment.__geoMeta.sourceRange
 | 
			
		||||
    )
 | 
			
		||||
    const sgPaths = sketchGroup.value
 | 
			
		||||
    const originalPathToNodeStr = JSON.stringify(segPathToNode)
 | 
			
		||||
    segPathToNode[1][0] = varDecIndex
 | 
			
		||||
    const pathToNodeStr = JSON.stringify(segPathToNode)
 | 
			
		||||
    // more hacks to hopefully be solved by proper pathToNode info in memory/sketchGroup segments
 | 
			
		||||
    const group =
 | 
			
		||||
      this.activeSegments[pathToNodeStr] ||
 | 
			
		||||
      this.activeSegments[originalPathToNodeStr]
 | 
			
		||||
    // const prevSegment = sketchGroup.slice(index - 1)[0]
 | 
			
		||||
    const type = group?.userData?.type
 | 
			
		||||
    const factor =
 | 
			
		||||
      (sceneInfra.camControls.camera instanceof OrthographicCamera
 | 
			
		||||
        ? orthoFactor
 | 
			
		||||
        : perspScale(sceneInfra.camControls.camera, group)) /
 | 
			
		||||
      sceneInfra._baseUnitMultiplier
 | 
			
		||||
    if (type === TANGENTIAL_ARC_TO_SEGMENT) {
 | 
			
		||||
      this.updateTangentialArcToSegment({
 | 
			
		||||
        prevSegment: sgPaths[index - 1],
 | 
			
		||||
        from: segment.from,
 | 
			
		||||
        to: segment.to,
 | 
			
		||||
        group: group,
 | 
			
		||||
        scale: factor,
 | 
			
		||||
      })
 | 
			
		||||
    } else if (type === STRAIGHT_SEGMENT) {
 | 
			
		||||
      this.updateStraightSegment({
 | 
			
		||||
        from: segment.from,
 | 
			
		||||
        to: segment.to,
 | 
			
		||||
        group,
 | 
			
		||||
        scale: factor,
 | 
			
		||||
      })
 | 
			
		||||
    } else if (type === PROFILE_START) {
 | 
			
		||||
      group.position.set(segment.from[0], segment.from[1], 0)
 | 
			
		||||
      group.scale.set(factor, factor, factor)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateTangentialArcToSegment({
 | 
			
		||||
    prevSegment,
 | 
			
		||||
    from,
 | 
			
		||||
 | 
			
		||||
@ -305,6 +305,16 @@ const CustomIconMap = {
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  ),
 | 
			
		||||
  rectangle: (
 | 
			
		||||
    <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
      <path
 | 
			
		||||
        fillRule="evenodd"
 | 
			
		||||
        clipRule="evenodd"
 | 
			
		||||
        d="M16 5H4V15H16V5ZM4 4H3V5V15V16H4H16H17V15V5V4H16H4Z"
 | 
			
		||||
        fill="currentColor"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  ),
 | 
			
		||||
  refresh: (
 | 
			
		||||
    <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
      <path
 | 
			
		||||
 | 
			
		||||
@ -42,8 +42,12 @@ import {
 | 
			
		||||
  getSketchQuaternion,
 | 
			
		||||
} from 'clientSideScene/sceneEntities'
 | 
			
		||||
import { sketchOnExtrudedFace, startSketchOnDefault } from 'lang/modifyAst'
 | 
			
		||||
import { Program, coreDump } from 'lang/wasm'
 | 
			
		||||
import { getNodePathFromSourceRange, isSingleCursorInPipe } from 'lang/queryAst'
 | 
			
		||||
import { Program, VariableDeclaration, coreDump } from 'lang/wasm'
 | 
			
		||||
import {
 | 
			
		||||
  getNodeFromPath,
 | 
			
		||||
  getNodePathFromSourceRange,
 | 
			
		||||
  isSingleCursorInPipe,
 | 
			
		||||
} from 'lang/queryAst'
 | 
			
		||||
import { TEST } from 'env'
 | 
			
		||||
import { exportFromEngine } from 'lib/exportFromEngine'
 | 
			
		||||
import { Models } from '@kittycad/lib/dist/types/src'
 | 
			
		||||
@ -278,6 +282,12 @@ export const ModelingMachineProvider = ({
 | 
			
		||||
 | 
			
		||||
          return canExtrudeSelection(selectionRanges)
 | 
			
		||||
        },
 | 
			
		||||
        'Sketch is empty': ({ sketchDetails }) =>
 | 
			
		||||
          getNodeFromPath<VariableDeclaration>(
 | 
			
		||||
            kclManager.ast,
 | 
			
		||||
            sketchDetails?.sketchPathToNode || [],
 | 
			
		||||
            'VariableDeclaration'
 | 
			
		||||
          )?.node?.declarations[0]?.init.type !== 'PipeExpression',
 | 
			
		||||
        'Selection is on face': ({ selectionRanges }, { data }) => {
 | 
			
		||||
          if (data?.forceNewSketch) return false
 | 
			
		||||
          if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										105
									
								
								src/lib/rectangleTool.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,105 @@
 | 
			
		||||
import {
 | 
			
		||||
  createArrayExpression,
 | 
			
		||||
  createBinaryExpression,
 | 
			
		||||
  createCallExpressionStdLib,
 | 
			
		||||
  createLiteral,
 | 
			
		||||
  createPipeSubstitution,
 | 
			
		||||
  createUnaryExpression,
 | 
			
		||||
} from 'lang/modifyAst'
 | 
			
		||||
import { roundOff } from './utils'
 | 
			
		||||
import { ArrayExpression, CallExpression, PipeExpression } from 'lang/wasm'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns AST expressions for this KCL code:
 | 
			
		||||
 * const yo = startSketchOn('XY')
 | 
			
		||||
 *  |> startProfileAt([0, 0], %)
 | 
			
		||||
 *  |> angledLine([0, 0], %, 'a')
 | 
			
		||||
 *  |> angledLine([segAng('a', %) - 90, 0], %, 'b')
 | 
			
		||||
 *  |> angledLine([segAng('a', %), -segLen('a', %)], %, 'c')
 | 
			
		||||
 *  |> close(%)
 | 
			
		||||
 */
 | 
			
		||||
export const getRectangleCallExpressions = (
 | 
			
		||||
  rectangleOrigin: [number, number],
 | 
			
		||||
  tags: [string, string, string]
 | 
			
		||||
) => [
 | 
			
		||||
  createCallExpressionStdLib('startProfileAt', [
 | 
			
		||||
    createArrayExpression([
 | 
			
		||||
      createLiteral(roundOff(rectangleOrigin[0])),
 | 
			
		||||
      createLiteral(roundOff(rectangleOrigin[1])),
 | 
			
		||||
    ]),
 | 
			
		||||
    createPipeSubstitution(),
 | 
			
		||||
  ]),
 | 
			
		||||
  createCallExpressionStdLib('angledLine', [
 | 
			
		||||
    createArrayExpression([
 | 
			
		||||
      createLiteral(0), // 0 deg
 | 
			
		||||
      createLiteral(0), // This will be the width of the rectangle
 | 
			
		||||
    ]),
 | 
			
		||||
    createPipeSubstitution(),
 | 
			
		||||
    createLiteral(tags[0]),
 | 
			
		||||
  ]),
 | 
			
		||||
  createCallExpressionStdLib('angledLine', [
 | 
			
		||||
    createArrayExpression([
 | 
			
		||||
      createBinaryExpression([
 | 
			
		||||
        createCallExpressionStdLib('segAng', [
 | 
			
		||||
          createLiteral(tags[0]),
 | 
			
		||||
          createPipeSubstitution(),
 | 
			
		||||
        ]),
 | 
			
		||||
        '+',
 | 
			
		||||
        createLiteral(90),
 | 
			
		||||
      ]), // 90 offset from the previous line
 | 
			
		||||
      createLiteral(0), // This will be the height of the rectangle
 | 
			
		||||
    ]),
 | 
			
		||||
    createPipeSubstitution(),
 | 
			
		||||
    createLiteral(tags[1]),
 | 
			
		||||
  ]),
 | 
			
		||||
  createCallExpressionStdLib('angledLine', [
 | 
			
		||||
    createArrayExpression([
 | 
			
		||||
      createCallExpressionStdLib('segAng', [
 | 
			
		||||
        createLiteral(tags[0]),
 | 
			
		||||
        createPipeSubstitution(),
 | 
			
		||||
      ]), // same angle as the first line
 | 
			
		||||
      createUnaryExpression(
 | 
			
		||||
        createCallExpressionStdLib('segLen', [
 | 
			
		||||
          createLiteral(tags[0]),
 | 
			
		||||
          createPipeSubstitution(),
 | 
			
		||||
        ]),
 | 
			
		||||
        '-'
 | 
			
		||||
      ), // negative height
 | 
			
		||||
    ]),
 | 
			
		||||
    createPipeSubstitution(),
 | 
			
		||||
    createLiteral(tags[2]),
 | 
			
		||||
  ]),
 | 
			
		||||
  createCallExpressionStdLib('close', [createPipeSubstitution()]),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Mutates the pipeExpression to update the rectangle sketch
 | 
			
		||||
 * @param pipeExpression
 | 
			
		||||
 * @param x
 | 
			
		||||
 * @param y
 | 
			
		||||
 * @param tag
 | 
			
		||||
 */
 | 
			
		||||
export function updateRectangleSketch(
 | 
			
		||||
  pipeExpression: PipeExpression,
 | 
			
		||||
  x: number,
 | 
			
		||||
  y: number,
 | 
			
		||||
  tag: string
 | 
			
		||||
) {
 | 
			
		||||
  ;((pipeExpression.body[2] as CallExpression)
 | 
			
		||||
    .arguments[0] as ArrayExpression) = createArrayExpression([
 | 
			
		||||
    createLiteral(x >= 0 ? 0 : 180),
 | 
			
		||||
    createLiteral(Math.abs(x)),
 | 
			
		||||
  ])
 | 
			
		||||
  ;((pipeExpression.body[3] as CallExpression)
 | 
			
		||||
    .arguments[0] as ArrayExpression) = createArrayExpression([
 | 
			
		||||
    createBinaryExpression([
 | 
			
		||||
      createCallExpressionStdLib('segAng', [
 | 
			
		||||
        createLiteral(tag),
 | 
			
		||||
        createPipeSubstitution(),
 | 
			
		||||
      ]),
 | 
			
		||||
      Math.sign(y) === Math.sign(x) ? '+' : '-',
 | 
			
		||||
      createLiteral(90),
 | 
			
		||||
    ]), // 90 offset from the previous line
 | 
			
		||||
    createLiteral(Math.abs(y)), // This will be the height of the rectangle
 | 
			
		||||
  ])
 | 
			
		||||
}
 | 
			
		||||
@ -137,6 +137,11 @@ export type ModelingMachineEvent =
 | 
			
		||||
  | { type: 'Extrude'; data?: ModelingCommandSchema['Extrude'] }
 | 
			
		||||
  | { type: 'Equip Line tool' }
 | 
			
		||||
  | { type: 'Equip tangential arc to' }
 | 
			
		||||
  | { type: 'Equip rectangle tool' }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'Add rectangle origin'
 | 
			
		||||
      data: [x: number, y: number]
 | 
			
		||||
    }
 | 
			
		||||
  | {
 | 
			
		||||
      type: 'done.invoke.animate-to-face' | 'done.invoke.animate-to-sketch'
 | 
			
		||||
      data: SketchDetails
 | 
			
		||||
@ -147,7 +152,7 @@ export type MoveDesc = { line: number; snippet: string }
 | 
			
		||||
 | 
			
		||||
export const modelingMachine = createMachine(
 | 
			
		||||
  {
 | 
			
		||||
    /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0ANgCcARgB0NcQHYAHKIBMw8QGY1ogKxKANCACeiWUvmT5SraNE15a4QBZxW2QF9X+tBhz4CAZTB2AAJYLDByTm5aBiQQFjZInliBBEFtWUlxGlEnWWVZNQc1fSMEDSl1NXlVewcirWE1d090LDxCAODYdgAnMBIAWyDyAFce2FQe6N54ji4k0BSaEsQaZpAvNvxJbAhMMAIAUVx2MB6QgGtA8gALadjZxN4UwRUM8RVRR3kP8XFhWQOFYIJQ5SRFIpKJxqbLVUTrTY+KA7PYHY6nc6wK7sW5UcQxZisObcZ5CKE0cH-GRaWww+Q0WTAhw0JSSfLCLQfYQ-Kr-BGtJEo-ZHPi9EYYe6EhLzUmpLQuSSgmgNbQOLQ-YF-GmK6qKbQ0Bw5Jz87ztIVovgsHrsSVxIlPZJk+RmUzkpRKcSGtXyYGyWRSAFaQoc+lKQFaE1bZF+bG3YhkSiYGPXO70Gb2mWOhB-DTg5TCDm-bnqTXiGySTkOeQKDSKaoOSOC5M4m6SZu3ACSqIIyBIVxCYCgAzAJyCADczpxyCRMLbHpnFogPlUK9yaI4TEHnJqHO7JGJhNlLGIJOJ5I2ze3W1eu8Le-3QkOR8EbpNsAAvbjsGdzjMkrMfB6bKiFU7rOMy8pAoYS5qEoFI5HI3IFj8djCBe2xXm2sY3LeBxENw3Q9CQeBBK+PQfl+M5BBA2DdAmYC-tK-6LtmcFSLusg6NysjCHBjLQSCEHgpyDSAl8ULqOh0bYVhKa4cQBG9MRuDjpO2DTpg1G0d+FAMWmDx-gs-BLh6wiSLW666koagmMUAnuhYbJwZYZ6epoDYeBsAqXjJN7dvhuCEcpQQAIIAEJ+EEAAajHEkZKTLqyFhhmoZabvSUGlKCjjmTo8gOLI66pTI56eYiPkprJLbyQFQUkeFkUAJqxQ6LGJfuKV5PKYh1HZpQwqIZhFmejjqrZUlVbck04f5ilESRZBQPsLULsZrHljxNhqqoOQAsCLlmRyDiOAVIHKKVLSmhhvnYTVc3Bfs+DsKmBJ2kx8UmflkicbYKhyPYNCwftvFDUd+WDWWHITZhfnCrVSkkUwZxI7gNGjJgJDnDRdG6StzFrX8nHmPlOhGqlVb8VlFgOJkzhfAVqguGhZXeddlWw3h90kWRFEnDOmAGEEM7YFAuB4x9rFZIqRSFdZ1Q9ZlS46Fo+4SBIDPHRIF1eVd0ns7ds2BQjKkTta6n84Lwui+LsqAWZKHZByTN2HoAn-IUbLyOqHImDIzhuCzuvTdNd1G-NKmwLgJBMEE7CoNFNsASoFKODYydljo-r7cqAYegUtQwjI0M3XJht1RHUcx3HQTNfpUpxbbyfgozhR+nYyiu1lzhmUaeSgloar5MX+ul3DXMqWAACOIxUY9UDPYnbXkuY1jWACx0D2emoFKI5gqPYhUgWIJjDy2Idl8bQRMJj-PUHXb0N0ngLggy+XVK3ypaJqu3gqC7ppzWbkp8pocwUmHYKfQBioAnMMce7BYCLwJhnYCPF86WDqI4TU+UKROFBNWAqLJPQeUulGYOoCABKYBBBgD4OEEYpxEEJULhWD0-8rCFSKKIYEogibWCqCyL2NBAbVmAdeA2wpDjT2wDHAAMngMAsdUCoFnPfec+MEr5TUGyM88trAFR4sCfKNMbAaCPBIP0ORRHnwkVIqui1nzYCopjcgijGGIA9M4b6hU34nkSqWP4mRHAAldMqQEAcSFNhkiFAA7sRF8b5Px800tjHSlAgh4AAGaoAIBAbgYAdi4DHKgK4kgYDsEEDzRJ35MCCEyagNxIIXLmFSrxAEPwVBf3sruXeUJnRpyKl7KxMS4mkQSZRZJ2l6LpNwFkggZweiTEkEwDG7Ask9AGKUwIFSxlJNqTM+pqjDKyndDoPM4k7DVBcAY+ymh2L2ALAUOsihiE61IZhYZHBVJmw0lpHGaS6k5LyQUopJSymCFNlOGceyskNJObvHQMIwzMjfuufaoItFhnUGWOQgMhHMwiRVM+HzggQvNhMv5CiAXzMWcskgqzJgbLBaSjS0KDmvTURLd0h5Mg2B4bxOQHwfT2SaQ0f4PV0H-G1uVNmRLYmfIatFaZszcm4HyXgEF+SwUkAAEawEEHwVlsKVA009CYMsoIqwAkpu4nhUggzfGDDw+U4TXmRMqsS0KEVFVUp6AsnoSyVlrMZVsnVeqDV1KNTlH4Eg8WpTsC4faygVacQKmxJw1ZARDLlcEBVjUlXZJVWqwpxTNUht1YIAwhrDnvWOfvb6zIeGgkKgWKEwIYQ8RXjSRCbFqZZpGbm-NczfU0sDQyzZ5TQ0Vqreyo5WYuU01wV7IoDJeLaDbf9ZuLg-7OhpAaPtnzFr7EHYW4FJbx2CEPVQiN1bH4sXnfud+CKZDWS4fZBoZlYIcmLMoNe+7giXsHdS-1tL6XrPPZe6d6Ya1zpUGZWwIE1T-Dzl8faXEKyOxsqYHi8o-1BDns9Y9QL1VnrBfhm4kGDLQbvSoJKiE-gegHjYYQwMwSDyqJw50EFcNkcA8O4Do6wOkZHPPcj16Z1UbWlyrRVZnU6GyNcrKHJWTKi9g0TkSoXnSr1rKkZSMegozRiMDGWNJm6UI6q09oKtl6YM+pIzmNBApPohR+urVJMegpIUNQm8CyaA4ftOwNNUFBnsPcoMUrWbaamh6mzI5DPGd+akyl+yh1+oDXSoN57Yuozs8ZxzpnKAuYfm5lIYFPP1CcL5nhBp9oKDMsFjQjQCzhasXI1VijlFHFsbHexJxHGaWca4m9JX3FmorLcuQIE-h2GtQgQa9s8ED0BMEiwrX5EdcwJIDsuAOAEFhWeGm7obLefUJBWbYgU7ebxQyQ0cEIyBzeTJNrCi47KK2zt9ge38RQdve5ssxN6QaDVBoBNAkFBmC9l8dcFqBEuq08HZ7G3JAADl44AAVUB4HgQQEKEAIAhG-NaK+mOTiwtUKyJU1YgkgR0MCNpzTvZODpi2tb7XXubdR0EDHWPYDxl0io8Tv2Ep5DMrkKwEh7Cmt9FLRQXLAaGmsJ6Kx23dsNK+Fo6sR51zHUNICTUUIkpNZAvJzQKhlcfa+4Lkbc3rCKh4kWAopgciahhBUH40J3a8SUFYgAKr1zgTieguLjl1kY0igiI-Z7CqwUgfiHhZIrmwQrSjaDMpKxFBQ-RiSsSMVG8c6LWivAAeVwICizxGSkhT8D7wQufcmCAL+wYvYthurSYSuTOE3eo0mT4gdU8Edq2AKlUDQcPIvTSCLgeOGSSCUH8GECI1EwAz6M8EWlqqGmvACWeHINlATVDEp00orTzDLhltYHihoJpkGwAMOl7RFFXwxqqsvRaNWSBv3f04gg46CBn5QBpeUN3V+A0IRd0BkOnUwcyZcRFRQffCLIOT-e-fAR-GhbSB-LEFMV-SzfJJA7-X-TAlsBpXiFWFkXiPIH4AeTDddMwB5KoL4PpAqUqTyKfDAeAWILTH7a3QQHeaQOQWXGoTQWnN2TkcwfvHFKEKwGQTTcfXYfYLgtvIQb4VcPIR5b0AqQxH4cbWwZwVKayGQeEB7N1FsBQ9RRACQFWTXZQbXVOPXG5XebzekSbdUduTiKxDmUwiWU7FOCwA8WCb2XvViQGcbEwPlbxONXDSpcZRLKZOpTw22SgveHaFwAeFTPqdxZkKQQEdQK1IqX9IwwlaLbNL5SFclJLfNeIgCCweFNUfQq5FwV9LKSwXeXcFkGTQGAESxAomVIo-tL1KKCoyjIXJcbBZuQLRwQGegxNZkMQ0wIMZUONAEXDAdOIoY63M8TQbRWoTjAeZ0NtG7DqJDY6bzdIb3boqLVsD1ADVY1zRQ7MTRPgriA0TQH4OwrKTkF0MsDuE6TiM4glHoy44onjG44rO4jYjIcHBQUwQ8I6WbY1NPZ0ZUAqf4dUAqXDbLeLTGGIszEEjlBIg0TIV49BY6FyRWEEPICoOmP6H9A3VnF7JRTASou9ZUHBb2FkVI7uYEGPcyRwxoGQGyOkpHFXdgJkyTWEYSaodkzIhoX0GQVcCSf4e3TicQQU9nSQKfdZGcUU0rOoGmaodBcnKkHhaXCkWXZOQoVeJXc4hHdbNUznbnE4dg24swwSIRfcbkewSsWo46X0f0fcP0M8FkKEP0Roc3DgbUpcIRDIXFRwLIEXPIMkv4csSsasOweg+sX3f3frIWIPRRCMkEIMVkf2L2DQMMWwPaN2dcAMDcbzQ8S-FU60zCOvfPQnJvbCEvfMz3JyBkMggMviLk+UMQvICws6dQBAx7FMSfafWfMAfM14GYl2QMMJIfI-RANUGmGkKQmQBkLqKEa-HbL-B-audfWctYsExoNkTaA0dID4VFASE-YRKnXYukMfRAg85AqAVAvgdAlAwg24Tsnhb6d9FwQhIoc1ALFOQ8RQNo8s0wdwdwIAA */
 | 
			
		||||
    /** @xstate-layout N4IgpgJg5mDOIC5QFkD2EwBsCWA7KAxAMICGuAxlgNoAMAuoqAA6qzYAu2qujIAHogC0AdgCsAZgB04gEyjhADnEA2GgoUAWJQBoQAT0QBGGuICckmoZkbTM42YWKAvk91oMOfAQDKYdgAJYLDByTm5aBiQQFjYwniiBBFtpGhlxDRphGg1ZURlhXQMEcRLDSVF5UwV84XEK8Rc3dCw8Ql8AgFtUAFcgwPYSdjAI3hiOLnjQIsFRQzLVDUNlRbSJWeVdRJLkjIUJdPzlVWVGkHcW-ElsCEwwAgBRXCGAJ0CAaz9yAAsRqLG43iJQQyZTCSRWZSmZYKKxzUEaQqIGSmDSSHI5ayGcQ0KqQ07nTxQK43O6PF7vT4-QyRZiscbcQFCaw0NFLSyiNTYhSZREIDIySS2YTKWYgmHiGEnVxnZqE4m3B58djPboYX602ITRkIGbySTImiiSGiDSiGG8uYc-UKZQKUymDk2DSLfGy1ry0l8FjPdjq6J0gEJJnqa3MmR2J1m3nCYTzMTpEXc-Km10ed3eD7sb7EMiUTAZyl+-5aoMIOY28GZZ3c7lmWQWwww8GNmFYyx7BSpi5EgtZr6SXvfACSJIIyBIH0CYCgHTAT38ADcwD7sOQSJgiwGS6BElZTGVGzJNMLNKkCvojIspNiaKl6vklBou3LB-3XyOFePJ0EZ3OAl9UGebAAC9uAGDd6FGLcGVLKxhFRVQbUNOZDmEGQLXEOZylNLIlkww08mfdNM2+AcSK+D87iIbhYGVEg8H8ACgNAp5138CBsFo3Nhkgv5oMmfgjDkVFsjSHFH33dReTSKELAqDlslEfcsSIy5XzIylKOIGi6IYpcVzXTB2M4gYKB4ml-U1GCdyE21BTyPYlkWNQNHQi8EHDWpykxY8YxBE1VJ7ciNL7LTqNwWjnno3B-AAQQAIW8fwAA1NysgTdyPUQ0XEMRDBsTDkTSaSHVRB1QW5O9TFqBppQJYjKRC4dR3CyLorixL-AATTS+kMqEvZ9VcpY5EMI0jTcoouQUSQ0PktlHGdQKmrfciwp0qKGLIKBbl6wMbLLdJryUao9kUQxhFMaSOTBOQzpjI1Luq5b1PfFqNva258HYH5eI1PrtSxXLpBFVR91kTDRGk5RG1myFFku6sVGEF7grehVWt0mKmGXHHcA48hukwEhXg4rizL27dBLLcMZpsUxCoyRtjmkmFUVjKpqrQ50NDEVHGvRqiPoYpiQLA9dMD0fx12wKBcEp6zqYhAUOy0CRTQyZRxAtdZ9QdXz8iNEp+b7Fb1oirHF2XThDMl6WcDlhX+ppy6cqPS71jMWYLWUORyjtS6bWqKpahN0jBe0i3Npi2BcBIJh-HYVAUqdwHMKkEEJTUarqjMaTRP1OQjiOQ1HHDMPVs096o-a2P48T5Oer+yyAdg9OLGFTCY1y4wEXciMwRNMulIujRllqpo0zUtG1urtqGLAABHbo2K+qAftT2DkSkWZecurI8gkc8igu7EcpMW9DVB0wK7NufLaYEmJeoZvi0V3dZDBVJM7GhmEb7k+DMv6lBhvaTIahRC3wjpjaO-hnhgC6Eufw5BhZPFgJvA6VgtCSBhtYCamgxo6Hco2TCFgli2GyCUNQvsoGzwVAAJTAIIMAfAQjdCGBgpWdQpBYlUFYeSYgNjuShMocoXM6gwmRL3WhVcFT3GXtgBOAAZPAYAG6oAghZN+ztGyyX2Mse06spLuSUNlG61hwwmhFHMGRoVRzyO6IoxO20-zYDYiTcgDdOGJDsH7DIphDQOhKGPbWxDs76nqMcXEhoJ4yinkFAWdDSQKITvA0I21bjqM0VBdK2pPLZTsC5ZEzoOShJPjISwaJkTqG-kA2J9Vp6NVigAd3ov+QCYtWJGTJqZSg-g8AADNUAEAgNwMAVxcALlQB8SQMB2CCFFixcCghBmoG8UibEqJnTLBxLUe03dpKLHMHsI8aRyFFxRnVN0jTTYtLaYxDpSy2I9O4v03AQyCDLmeIBSQTBibsCGc8Dosy-ALMeeLTAKz3lrNfvxPJ2JspzGdHMdIDlqrSQlAKbINBVAmDyMKQwt87kcCtgZZ5JlXmrJGWMiZUyZlzMEPpG264oVDPWR5Qq4JcowhMKCQwQppJHikI4Q+dhShoSJa0klTLVzkvJn0qlXyfl-MGIC4FDKZWGVZTCrRcLSxnJVoaMQ+4xCaEFa5cEoNUgSlOkoSV9yEpJWSm8j5ozcDjLwHS8ZDKSAACNYCCD4Nq9lZzRHCgqd-NYCgSq+3BBkRY48QT5HqdchJtypUBEdSlF1wylXPF+f8tVIL5l+oDUG1ZIau45RtXUAJQ8SpLGkGaEU+FeY2HtSSrNXUc3UvdbS6Z3rQWlsEHoYNsLcn6swqIuo8gKi2CAaaXk2JQRok5hyM0CKnxXPiStSQxLM2dW7Yq543z80qoBYBdVQ7-UjrHbqidB0zlf0OLlEEWsE1LqchYSERxLFYgldu7su793S3wJkqlbqPWTIHcWwQGSmEVvHa3R9VbsQosPoYww10jSCgRZYMazaYwdoCPBnteaC2qsvbB+Dd6cnIepmcmaWQViSOzhda6F1mx1CyHIQ0IJLmTyA+pEDa8fo9sg-2+loLRNfFo3xB9DHIZuzMP-Cp+RoaDRxeKlQF0rDEf8DJsjJ7lWFqowymTcn-r7UU424weLJTHkmkiZYApf1aBRGIB6UpBMvmCiBnGzw8YEyJiTYy8q1EQZpZ6mDDKAtBdXCF54ggXlmUsy3azPik32SUNYX2Yo5Csz9j3MeIpDFyBTTu4TGb-BxbnMF4mpMKVmSM6eijF6gWwdq-jBLDXktNcoGl7ReSsv61kGPcMtoCv9xjGCYrRwlIOnK7fFR7qskPBSc4-Arj3HPE8UnENl1RFVHysxusl0ymIBzmiEw+UbTyBBI2ZbqismSCHLgDgBAQ1pFESdWY3IURyCuu5W0ZQ0h2jMCYGMtgKtCeCittRScNGvfe+wT71I6MZaRPkAUHIjxOSsDioHRQodoi1hIWwRoAmGie6txHmBJAADlk4AAVUB4HYLAAgsUIAQH6CTAILB2chqOGVK+SlXLcmWLyI45gcjVWqpkdQEoacI9QEjpn-hWfs856QMy2T5P0d3JocwJgwcVJRB2aN7lcKrtAaUI8Wtb5vY++y4wja1ZZBUHxnEAChIlFJ+G44qRliEsA75xqzvUdUHRwbzHZZkKSDtDibIWsI35QtFI2aewEwQgKt5uJsPGoABUXFPDcUZDxDd1uOOUc9unwvnTn0UEpDkvteQojDWhVYWQcVjwEwX8Pptuj42TlxH0r4ADyuBe1Qa9Xu7wRfBDD9GYIMf7BJ-yyQ3HoGM18VmCRTkDkVuihmhZFCflLlahKFyrfRh6SwOq6R-u1o-gOlQDwFznncCQimR2mot-eAnCJ8iw2U6gjYVgSgdm6KDGjgoiEgikkI1UuiFc-guAycAyJAlAPgwQoQ7EYAGBRMAuxM7q7KwIWEuiOQMYWg8IDo0uIIieVgR0T0wo7aYe7oZA2AHQgwL+ScNWxBdwEm0WMyHBXBQwggScggGBlA7KFQB43Iig2K38PIwOR40gjB2IR41UWgnYbBlwIh3B+ADc-gLCJkL+sA5EM+km4y+hYhEh5hhYW+VMiQvs2Uwe+Qx2Jor6n6M0RwV+UI6gHmOhPmDUps8Oa2DiTiaSv+mS9ejh78WOYg+oxgf8sY1q9YxCY0nehSiujkcgLg0oaBGA8AUQDSUAGOThQg3IZQuQigXu6gWgF2ZYkIZCt4mgvG5Orky01wtw5R8ROo0I5QoI1UEopox4vIYB5QXIY0hUaGN8uhaa3wvRzs-haIZ0vK3uKI1006VCmQeEewlgtizUPRseFRZYGQGcWhl0R4OEVQDYKIcajBWmuUJgMOg+pEIGiyEKYWvSEW0KSxgMQcaI-iigsYNQqQGKyQqKN4Mu+QoewRNy7x1WmqcqPxOa-xsE6gAobIlg3Cvs+4gqVoqQiwbM82sw+mWazqqy6JmC4OFgkB28vsUBJUpCpoFUfCqgZoMg5Jh6aJJxfRuiOO1xmE3IrkS6OKBScgFQxqmJcx8JCx-YIGpGVJfJOido2UWst4OQDgeQDM10agieYOFQqg4adQ+mhmypVmpxuiLI5x40MIDMxc0MKgs0+4IcQxrJrxIRiJ9yXW9WoWKWCqfxKpAJOGh2R+8aUIJQ0k50s0-Kew+4UpUIKuWS1JDG9gHcJQewNg6gtovInJzYtokido2hXJ8xu6YRdOyOHAqZPiY0ZQmQmZJoVQQc0uyQSw76ISASY8spA+Xp-YFZau9OaBQK64NZSINgYIe8sgAStY4Y0YlSNgduhUNosgyZlZGuWuaCY5HkpoGckBWg9g32jR4a1ocwWIjYLBMITuKO25xgSkOCGszIEo++vuZY4BzYYBbYjYZot8JeW2ZeO2e2qA25d0wC1Qrk42MMY8Fo4pD51USkjgmQ9opZcpu6y+o+Aw4+5EU+t5to5gawasZOOWROiAVipO-KeCj0ew+epRu6d+0Rj+mAIFEgoiyw8gOEEgjYOKGeTYaEDoumqKOIt+P+pGlZz+hhQQKC+MyCgE7qzwzFmgD57Fs6wp3FxCSkNpKgvMsw55SgPZtF6k9Folg5e6GaL+ABAkQ2+qMMoiV4IIKISBdoFoIBieMIRZ+EuyKBaB-gUhYA25wI-uiYamrkLm4J7kvMGcE0UINQWsDMy0NhPBycKq7quFUg1UkI2QDooJalRQvsoiJg6gwoQ8XI-etFCVhhvBJhtEZh5Et51Us0RoIoPG+UmEtgrMLIJcSezINoR4+RTgQAA */
 | 
			
		||||
    id: 'Modeling',
 | 
			
		||||
 | 
			
		||||
    tsTypes: {} as import('./modelingMachine.typegen').Typegen0,
 | 
			
		||||
@ -321,6 +326,11 @@ export const modelingMachine = createMachine(
 | 
			
		||||
                target: 'Tangential arc to',
 | 
			
		||||
                cond: 'is editing existing sketch',
 | 
			
		||||
              },
 | 
			
		||||
 | 
			
		||||
              'Equip rectangle tool': {
 | 
			
		||||
                target: 'Rectangle tool',
 | 
			
		||||
                cond: 'Sketch is empty',
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            entry: 'setup client side sketch segments',
 | 
			
		||||
@ -418,6 +428,11 @@ export const modelingMachine = createMachine(
 | 
			
		||||
                target: 'Tangential arc to',
 | 
			
		||||
                cond: 'is editing existing sketch',
 | 
			
		||||
              },
 | 
			
		||||
 | 
			
		||||
              'Equip rectangle tool': {
 | 
			
		||||
                target: 'Rectangle tool',
 | 
			
		||||
                cond: 'Sketch is empty',
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            states: {
 | 
			
		||||
@ -476,6 +491,24 @@ export const modelingMachine = createMachine(
 | 
			
		||||
              onDone: '#Modeling.idle',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          'Rectangle tool': {
 | 
			
		||||
            entry: ['listen for rectangle origin'],
 | 
			
		||||
            states: {
 | 
			
		||||
              'Awaiting second corner': {},
 | 
			
		||||
 | 
			
		||||
              'Awaiting origin': {
 | 
			
		||||
                on: {
 | 
			
		||||
                  'Add rectangle origin': {
 | 
			
		||||
                    target: 'Awaiting second corner',
 | 
			
		||||
                    actions: 'set up draft rectangle',
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            initial: 'Awaiting origin',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        initial: 'Init',
 | 
			
		||||
@ -858,6 +891,20 @@ export const modelingMachine = createMachine(
 | 
			
		||||
          'tangentialArcTo'
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'listen for rectangle origin': ({ sketchDetails }) => {
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.setupRectangleOriginListener()
 | 
			
		||||
      },
 | 
			
		||||
      'set up draft rectangle': ({ sketchDetails }, { data }) => {
 | 
			
		||||
        if (!sketchDetails || !data) return
 | 
			
		||||
        sceneEntitiesManager.setupDraftRectangle(
 | 
			
		||||
          sketchDetails.sketchPathToNode,
 | 
			
		||||
          sketchDetails.zAxis,
 | 
			
		||||
          sketchDetails.yAxis,
 | 
			
		||||
          sketchDetails.origin,
 | 
			
		||||
          data
 | 
			
		||||
        )
 | 
			
		||||
      },
 | 
			
		||||
      'set up draft line without teardown': ({ sketchDetails }) => {
 | 
			
		||||
        if (!sketchDetails) return
 | 
			
		||||
        sceneEntitiesManager.setUpDraftSegment(
 | 
			
		||||
 | 
			
		||||