Merge branch 'main' into franknoirot/4088/create-file-url
This commit is contained in:
		
							
								
								
									
										2
									
								
								.github/ci-cd-scripts/playwright-electron.sh
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/ci-cd-scripts/playwright-electron.sh
									
									
									
									
										vendored
									
									
								
							@ -21,7 +21,7 @@ if [[ ! -f "test-results/.last-run.json" ]]; then
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
retry=1
 | 
			
		||||
max_retrys=4
 | 
			
		||||
max_retrys=5
 | 
			
		||||
 | 
			
		||||
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
 | 
			
		||||
while [[ $retry -le $max_retrys ]]; do
 | 
			
		||||
 | 
			
		||||
@ -491,721 +491,8 @@ sketch002 = startSketchOn(extrude001, seg03)
 | 
			
		||||
  |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
`,
 | 
			
		||||
      { shouldNormalise: true }
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test(`Verify axis, origin, and horizontal snapping`, async ({
 | 
			
		||||
  page,
 | 
			
		||||
  homePage,
 | 
			
		||||
  editor,
 | 
			
		||||
  toolbar,
 | 
			
		||||
  scene,
 | 
			
		||||
}) => {
 | 
			
		||||
  const viewPortSize = { width: 1200, height: 500 }
 | 
			
		||||
 | 
			
		||||
  await page.setBodyDimensions(viewPortSize)
 | 
			
		||||
 | 
			
		||||
  await homePage.goToModelingScene()
 | 
			
		||||
 | 
			
		||||
  // Constants and locators
 | 
			
		||||
  // These are mappings from screenspace to KCL coordinates,
 | 
			
		||||
  // until we merge in our coordinate system helpers
 | 
			
		||||
  const xzPlane = [
 | 
			
		||||
    viewPortSize.width * 0.65,
 | 
			
		||||
    viewPortSize.height * 0.3,
 | 
			
		||||
  ] as const
 | 
			
		||||
  const originSloppy = {
 | 
			
		||||
    screen: [
 | 
			
		||||
      viewPortSize.width / 2 + 3, // 3px off the center of the screen
 | 
			
		||||
      viewPortSize.height / 2,
 | 
			
		||||
    ],
 | 
			
		||||
    kcl: [0, 0],
 | 
			
		||||
  } as const
 | 
			
		||||
  const xAxisSloppy = {
 | 
			
		||||
    screen: [
 | 
			
		||||
      viewPortSize.width * 0.75,
 | 
			
		||||
      viewPortSize.height / 2 - 3, // 3px off the X-axis
 | 
			
		||||
    ],
 | 
			
		||||
    kcl: [20.34, 0],
 | 
			
		||||
  } as const
 | 
			
		||||
  const offYAxis = {
 | 
			
		||||
    screen: [
 | 
			
		||||
      viewPortSize.width * 0.6, // Well off the Y-axis, out of snapping range
 | 
			
		||||
      viewPortSize.height * 0.3,
 | 
			
		||||
    ],
 | 
			
		||||
    kcl: [8.14, 6.78],
 | 
			
		||||
  } as const
 | 
			
		||||
  const yAxisSloppy = {
 | 
			
		||||
    screen: [
 | 
			
		||||
      viewPortSize.width / 2 + 5, // 5px off the Y-axis
 | 
			
		||||
      viewPortSize.height * 0.3,
 | 
			
		||||
    ],
 | 
			
		||||
    kcl: [0, 6.78],
 | 
			
		||||
  } as const
 | 
			
		||||
  const [clickOnXzPlane, moveToXzPlane] = scene.makeMouseHelpers(...xzPlane)
 | 
			
		||||
  const [clickOriginSloppy] = scene.makeMouseHelpers(...originSloppy.screen)
 | 
			
		||||
  const [clickXAxisSloppy, moveXAxisSloppy] = scene.makeMouseHelpers(
 | 
			
		||||
    ...xAxisSloppy.screen
 | 
			
		||||
  )
 | 
			
		||||
  const [dragToOffYAxis, dragFromOffAxis] = scene.makeDragHelpers(
 | 
			
		||||
    ...offYAxis.screen
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const expectedCodeSnippets = {
 | 
			
		||||
    sketchOnXzPlane: `sketch001 = startSketchOn('XZ')`,
 | 
			
		||||
    pointAtOrigin: `startProfileAt([${originSloppy.kcl[0]}, ${originSloppy.kcl[1]}], %)`,
 | 
			
		||||
    segmentOnXAxis: `xLine(${xAxisSloppy.kcl[0]}, %)`,
 | 
			
		||||
    afterSegmentDraggedOffYAxis: `startProfileAt([${offYAxis.kcl[0]}, ${offYAxis.kcl[1]}], %)`,
 | 
			
		||||
    afterSegmentDraggedOnYAxis: `startProfileAt([${yAxisSloppy.kcl[0]}, ${yAxisSloppy.kcl[1]}], %)`,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  await test.step(`Start a sketch on the XZ plane`, async () => {
 | 
			
		||||
    await editor.closePane()
 | 
			
		||||
    await toolbar.startSketchPlaneSelection()
 | 
			
		||||
    await moveToXzPlane()
 | 
			
		||||
    await clickOnXzPlane()
 | 
			
		||||
    // timeout wait for engine animation is unavoidable
 | 
			
		||||
    await page.waitForTimeout(600)
 | 
			
		||||
    await editor.expectEditor.toContain(expectedCodeSnippets.sketchOnXzPlane)
 | 
			
		||||
  })
 | 
			
		||||
  await test.step(`Place a point a few pixels off the middle, verify it still snaps to 0,0`, async () => {
 | 
			
		||||
    await clickOriginSloppy()
 | 
			
		||||
    await editor.expectEditor.toContain(expectedCodeSnippets.pointAtOrigin)
 | 
			
		||||
  })
 | 
			
		||||
  await test.step(`Add a segment on x-axis after moving the mouse a bit, verify it snaps`, async () => {
 | 
			
		||||
    await moveXAxisSloppy()
 | 
			
		||||
    await clickXAxisSloppy()
 | 
			
		||||
    await editor.expectEditor.toContain(expectedCodeSnippets.segmentOnXAxis)
 | 
			
		||||
  })
 | 
			
		||||
  await test.step(`Unequip line tool`, async () => {
 | 
			
		||||
    await toolbar.lineBtn.click()
 | 
			
		||||
    await expect(toolbar.lineBtn).not.toHaveAttribute('aria-pressed', 'true')
 | 
			
		||||
  })
 | 
			
		||||
  await test.step(`Drag the origin point up and to the right, verify it's past snapping`, async () => {
 | 
			
		||||
    await dragToOffYAxis({
 | 
			
		||||
      fromPoint: { x: originSloppy.screen[0], y: originSloppy.screen[1] },
 | 
			
		||||
    })
 | 
			
		||||
    await editor.expectEditor.toContain(
 | 
			
		||||
      expectedCodeSnippets.afterSegmentDraggedOffYAxis
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
  await test.step(`Drag the origin point left to the y-axis, verify it snaps back`, async () => {
 | 
			
		||||
    await dragFromOffAxis({
 | 
			
		||||
      toPoint: { x: yAxisSloppy.screen[0], y: yAxisSloppy.screen[1] },
 | 
			
		||||
    })
 | 
			
		||||
    await editor.expectEditor.toContain(
 | 
			
		||||
      expectedCodeSnippets.afterSegmentDraggedOnYAxis
 | 
			
		||||
    )
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test(`Verify user can double-click to edit a sketch`, async ({
 | 
			
		||||
  context,
 | 
			
		||||
  page,
 | 
			
		||||
  homePage,
 | 
			
		||||
  editor,
 | 
			
		||||
  toolbar,
 | 
			
		||||
  scene,
 | 
			
		||||
}) => {
 | 
			
		||||
  const u = await getUtils(page)
 | 
			
		||||
 | 
			
		||||
  const initialCode = `closedSketch = startSketchOn('XZ')
 | 
			
		||||
  |> circle({ center = [8, 5], radius = 2 }, %)
 | 
			
		||||
openSketch = startSketchOn('XY')
 | 
			
		||||
  |> startProfileAt([-5, 0], %)
 | 
			
		||||
  |> lineTo([0, 5], %)
 | 
			
		||||
  |> xLine(5, %)
 | 
			
		||||
  |> tangentialArcTo([10, 0], %)
 | 
			
		||||
`
 | 
			
		||||
  const viewPortSize = { width: 1000, height: 500 }
 | 
			
		||||
  await page.setBodyDimensions(viewPortSize)
 | 
			
		||||
 | 
			
		||||
  await context.addInitScript((code) => {
 | 
			
		||||
    localStorage.setItem('persistCode', code)
 | 
			
		||||
  }, initialCode)
 | 
			
		||||
 | 
			
		||||
  await homePage.goToModelingScene()
 | 
			
		||||
  await u.waitForPageLoad()
 | 
			
		||||
  await page.waitForTimeout(1000)
 | 
			
		||||
 | 
			
		||||
  const pointInsideCircle = {
 | 
			
		||||
    x: viewPortSize.width * 0.63,
 | 
			
		||||
    y: viewPortSize.height * 0.5,
 | 
			
		||||
  }
 | 
			
		||||
  const pointOnPathAfterSketching = {
 | 
			
		||||
    x: viewPortSize.width * 0.65,
 | 
			
		||||
    y: viewPortSize.height * 0.5,
 | 
			
		||||
  }
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  const [_clickOpenPath, moveToOpenPath, dblClickOpenPath] =
 | 
			
		||||
    scene.makeMouseHelpers(
 | 
			
		||||
      pointOnPathAfterSketching.x,
 | 
			
		||||
      pointOnPathAfterSketching.y
 | 
			
		||||
    )
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  const [_clickCircle, moveToCircle, dblClickCircle] = scene.makeMouseHelpers(
 | 
			
		||||
    pointInsideCircle.x,
 | 
			
		||||
    pointInsideCircle.y
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const exitSketch = async () => {
 | 
			
		||||
    await test.step(`Exit sketch mode`, async () => {
 | 
			
		||||
      await toolbar.exitSketchBtn.click()
 | 
			
		||||
      await expect(toolbar.exitSketchBtn).not.toBeVisible()
 | 
			
		||||
      await expect(toolbar.startSketchBtn).toBeEnabled()
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  await test.step(`Double-click on the closed sketch`, async () => {
 | 
			
		||||
    await moveToCircle()
 | 
			
		||||
    await dblClickCircle()
 | 
			
		||||
    await expect(toolbar.startSketchBtn).not.toBeVisible()
 | 
			
		||||
    await expect(toolbar.exitSketchBtn).toBeVisible()
 | 
			
		||||
    await editor.expectState({
 | 
			
		||||
      activeLines: [`|>circle({center=[8,5],radius=2},%)`],
 | 
			
		||||
      highlightedCode: 'circle({center=[8,5],radius=2},%)',
 | 
			
		||||
      diagnostics: [],
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
  await page.waitForTimeout(1000)
 | 
			
		||||
 | 
			
		||||
  await exitSketch()
 | 
			
		||||
  await page.waitForTimeout(1000)
 | 
			
		||||
 | 
			
		||||
  // Drag the sketch line out of the axis view which blocks the click
 | 
			
		||||
  await page.dragAndDrop('#stream', '#stream', {
 | 
			
		||||
    sourcePosition: {
 | 
			
		||||
      x: viewPortSize.width * 0.7,
 | 
			
		||||
      y: viewPortSize.height * 0.5,
 | 
			
		||||
    },
 | 
			
		||||
    targetPosition: {
 | 
			
		||||
      x: viewPortSize.width * 0.7,
 | 
			
		||||
      y: viewPortSize.height * 0.4,
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await page.waitForTimeout(500)
 | 
			
		||||
 | 
			
		||||
  await test.step(`Double-click on the open sketch`, async () => {
 | 
			
		||||
    await moveToOpenPath()
 | 
			
		||||
    await scene.expectPixelColor([250, 250, 250], pointOnPathAfterSketching, 15)
 | 
			
		||||
    // There is a full execution after exiting sketch that clears the scene.
 | 
			
		||||
    await page.waitForTimeout(500)
 | 
			
		||||
    await dblClickOpenPath()
 | 
			
		||||
    await expect(toolbar.startSketchBtn).not.toBeVisible()
 | 
			
		||||
    await expect(toolbar.exitSketchBtn).toBeVisible()
 | 
			
		||||
    // Wait for enter sketch mode to complete
 | 
			
		||||
    await page.waitForTimeout(500)
 | 
			
		||||
    await editor.expectState({
 | 
			
		||||
      activeLines: [`|>tangentialArcTo([10,0],%)`],
 | 
			
		||||
      highlightedCode: 'tangentialArcTo([10,0],%)',
 | 
			
		||||
      diagnostics: [],
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test(`Offset plane point-and-click`, async ({
 | 
			
		||||
  context,
 | 
			
		||||
  page,
 | 
			
		||||
  homePage,
 | 
			
		||||
  scene,
 | 
			
		||||
  editor,
 | 
			
		||||
  toolbar,
 | 
			
		||||
  cmdBar,
 | 
			
		||||
}) => {
 | 
			
		||||
  // One dumb hardcoded screen pixel value
 | 
			
		||||
  const testPoint = { x: 700, y: 150 }
 | 
			
		||||
  const [clickOnXzPlane] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
 | 
			
		||||
  const expectedOutput = `plane001 = offsetPlane('XZ', 5)`
 | 
			
		||||
 | 
			
		||||
  await homePage.goToModelingScene()
 | 
			
		||||
 | 
			
		||||
  await test.step(`Look for the blue of the XZ plane`, async () => {
 | 
			
		||||
    await scene.expectPixelColor([50, 51, 96], testPoint, 15)
 | 
			
		||||
  })
 | 
			
		||||
  await test.step(`Go through the command bar flow`, async () => {
 | 
			
		||||
    await toolbar.offsetPlaneButton.click()
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      stage: 'arguments',
 | 
			
		||||
      currentArgKey: 'plane',
 | 
			
		||||
      currentArgValue: '',
 | 
			
		||||
      headerArguments: { Plane: '', Distance: '' },
 | 
			
		||||
      highlightedHeaderArg: 'plane',
 | 
			
		||||
      commandName: 'Offset plane',
 | 
			
		||||
    })
 | 
			
		||||
    await clickOnXzPlane()
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      stage: 'arguments',
 | 
			
		||||
      currentArgKey: 'distance',
 | 
			
		||||
      currentArgValue: '5',
 | 
			
		||||
      headerArguments: { Plane: '1 plane', Distance: '' },
 | 
			
		||||
      highlightedHeaderArg: 'distance',
 | 
			
		||||
      commandName: 'Offset plane',
 | 
			
		||||
    })
 | 
			
		||||
    await cmdBar.progressCmdBar()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
 | 
			
		||||
    await editor.expectEditor.toContain(expectedOutput)
 | 
			
		||||
    await editor.expectState({
 | 
			
		||||
      diagnostics: [],
 | 
			
		||||
      activeLines: [expectedOutput],
 | 
			
		||||
      highlightedCode: '',
 | 
			
		||||
    })
 | 
			
		||||
    await scene.expectPixelColor([74, 74, 74], testPoint, 15)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step('Delete offset plane via feature tree selection', async () => {
 | 
			
		||||
    await editor.closePane()
 | 
			
		||||
    const operationButton = await toolbar.getFeatureTreeOperation(
 | 
			
		||||
      'Offset Plane',
 | 
			
		||||
      0
 | 
			
		||||
    )
 | 
			
		||||
    await operationButton.click({ button: 'left' })
 | 
			
		||||
    await page.keyboard.press('Backspace')
 | 
			
		||||
    await scene.expectPixelColor([50, 51, 96], testPoint, 15)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const loftPointAndClickCases = [
 | 
			
		||||
  { shouldPreselect: true },
 | 
			
		||||
  { shouldPreselect: false },
 | 
			
		||||
]
 | 
			
		||||
loftPointAndClickCases.forEach(({ shouldPreselect }) => {
 | 
			
		||||
  test(`Loft point-and-click (preselected sketches: ${shouldPreselect})`, async ({
 | 
			
		||||
    context,
 | 
			
		||||
    page,
 | 
			
		||||
    homePage,
 | 
			
		||||
    scene,
 | 
			
		||||
    editor,
 | 
			
		||||
    toolbar,
 | 
			
		||||
    cmdBar,
 | 
			
		||||
  }) => {
 | 
			
		||||
    const initialCode = `sketch001 = startSketchOn('XZ')
 | 
			
		||||
    |> circle({ center = [0, 0], radius = 30 }, %)
 | 
			
		||||
    plane001 = offsetPlane('XZ', 50)
 | 
			
		||||
    sketch002 = startSketchOn(plane001)
 | 
			
		||||
    |> circle({ center = [0, 0], radius = 20 }, %)
 | 
			
		||||
`
 | 
			
		||||
    await context.addInitScript((initialCode) => {
 | 
			
		||||
      localStorage.setItem('persistCode', initialCode)
 | 
			
		||||
    }, initialCode)
 | 
			
		||||
    await page.setBodyDimensions({ width: 1000, height: 500 })
 | 
			
		||||
    await homePage.goToModelingScene()
 | 
			
		||||
 | 
			
		||||
    // One dumb hardcoded screen pixel value
 | 
			
		||||
    const testPoint = { x: 575, y: 200 }
 | 
			
		||||
    const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
 | 
			
		||||
    const [clickOnSketch2] = scene.makeMouseHelpers(
 | 
			
		||||
      testPoint.x,
 | 
			
		||||
      testPoint.y + 80
 | 
			
		||||
    )
 | 
			
		||||
    const loftDeclaration = 'loft001 = loft([sketch001, sketch002])'
 | 
			
		||||
 | 
			
		||||
    await test.step(`Look for the white of the sketch001 shape`, async () => {
 | 
			
		||||
      await scene.expectPixelColor([254, 254, 254], testPoint, 15)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    async function selectSketches() {
 | 
			
		||||
      await clickOnSketch1()
 | 
			
		||||
      await page.keyboard.down('Shift')
 | 
			
		||||
      await clickOnSketch2()
 | 
			
		||||
      await page.waitForTimeout(500)
 | 
			
		||||
      await page.keyboard.up('Shift')
 | 
			
		||||
        { shouldNormalise: true }
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!shouldPreselect) {
 | 
			
		||||
      await test.step(`Go through the command bar flow without preselected sketches`, async () => {
 | 
			
		||||
        await toolbar.loftButton.click()
 | 
			
		||||
        await cmdBar.expectState({
 | 
			
		||||
          stage: 'arguments',
 | 
			
		||||
          currentArgKey: 'selection',
 | 
			
		||||
          currentArgValue: '',
 | 
			
		||||
          headerArguments: { Selection: '' },
 | 
			
		||||
          highlightedHeaderArg: 'selection',
 | 
			
		||||
          commandName: 'Loft',
 | 
			
		||||
        })
 | 
			
		||||
        await selectSketches()
 | 
			
		||||
        await cmdBar.progressCmdBar()
 | 
			
		||||
        await cmdBar.expectState({
 | 
			
		||||
          stage: 'review',
 | 
			
		||||
          headerArguments: { Selection: '2 faces' },
 | 
			
		||||
          commandName: 'Loft',
 | 
			
		||||
        })
 | 
			
		||||
        await cmdBar.progressCmdBar()
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      await test.step(`Preselect the two sketches`, async () => {
 | 
			
		||||
        await selectSketches()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step(`Go through the command bar flow with preselected sketches`, async () => {
 | 
			
		||||
        await toolbar.loftButton.click()
 | 
			
		||||
        await cmdBar.progressCmdBar()
 | 
			
		||||
        await cmdBar.expectState({
 | 
			
		||||
          stage: 'review',
 | 
			
		||||
          headerArguments: { Selection: '2 faces' },
 | 
			
		||||
          commandName: 'Loft',
 | 
			
		||||
        })
 | 
			
		||||
        await cmdBar.progressCmdBar()
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
 | 
			
		||||
      await editor.expectEditor.toContain(loftDeclaration)
 | 
			
		||||
      await editor.expectState({
 | 
			
		||||
        diagnostics: [],
 | 
			
		||||
        activeLines: [loftDeclaration],
 | 
			
		||||
        highlightedCode: '',
 | 
			
		||||
      })
 | 
			
		||||
      await scene.expectPixelColor([89, 89, 89], testPoint, 15)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    await test.step('Delete loft via feature tree selection', async () => {
 | 
			
		||||
      await editor.closePane()
 | 
			
		||||
      const operationButton = await toolbar.getFeatureTreeOperation('Loft', 0)
 | 
			
		||||
      await operationButton.click({ button: 'left' })
 | 
			
		||||
      await page.keyboard.press('Backspace')
 | 
			
		||||
      await scene.expectPixelColor([254, 254, 254], testPoint, 15)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// TODO: merge with above test. Right now we're not able to delete a loft
 | 
			
		||||
// right after creation via selection for some reason, so we go with a new instance
 | 
			
		||||
test('Loft and offset plane deletion via selection', async ({
 | 
			
		||||
  context,
 | 
			
		||||
  page,
 | 
			
		||||
  homePage,
 | 
			
		||||
  scene,
 | 
			
		||||
}) => {
 | 
			
		||||
  const initialCode = `sketch001 = startSketchOn('XZ')
 | 
			
		||||
  |> circle({ center = [0, 0], radius = 30 }, %)
 | 
			
		||||
  plane001 = offsetPlane('XZ', 50)
 | 
			
		||||
  sketch002 = startSketchOn(plane001)
 | 
			
		||||
  |> circle({ center = [0, 0], radius = 20 }, %)
 | 
			
		||||
loft001 = loft([sketch001, sketch002])
 | 
			
		||||
`
 | 
			
		||||
  await context.addInitScript((initialCode) => {
 | 
			
		||||
    localStorage.setItem('persistCode', initialCode)
 | 
			
		||||
  }, initialCode)
 | 
			
		||||
  await page.setBodyDimensions({ width: 1000, height: 500 })
 | 
			
		||||
  await homePage.goToModelingScene()
 | 
			
		||||
 | 
			
		||||
  // One dumb hardcoded screen pixel value
 | 
			
		||||
  const testPoint = { x: 575, y: 200 }
 | 
			
		||||
  const [clickOnSketch1] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
 | 
			
		||||
  const [clickOnSketch2] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 80)
 | 
			
		||||
 | 
			
		||||
  await test.step(`Delete loft`, async () => {
 | 
			
		||||
    // Check for loft
 | 
			
		||||
    await scene.expectPixelColor([89, 89, 89], testPoint, 15)
 | 
			
		||||
    await clickOnSketch1()
 | 
			
		||||
    await expect(page.locator('.cm-activeLine')).toHaveText(`
 | 
			
		||||
      |> circle({ center = [0, 0], radius = 30 }, %)
 | 
			
		||||
    `)
 | 
			
		||||
    await page.keyboard.press('Backspace')
 | 
			
		||||
    // Check for sketch 1
 | 
			
		||||
    await scene.expectPixelColor([254, 254, 254], testPoint, 15)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step('Delete sketch002', async () => {
 | 
			
		||||
    await page.waitForTimeout(1000)
 | 
			
		||||
    await clickOnSketch2()
 | 
			
		||||
    await expect(page.locator('.cm-activeLine')).toHaveText(`
 | 
			
		||||
      |> circle({ center = [0, 0], radius = 20 }, %)
 | 
			
		||||
    `)
 | 
			
		||||
    await page.keyboard.press('Backspace')
 | 
			
		||||
    // Check for plane001
 | 
			
		||||
    await scene.expectPixelColor([228, 228, 228], testPoint, 15)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step('Delete plane001', async () => {
 | 
			
		||||
    await page.waitForTimeout(1000)
 | 
			
		||||
    await clickOnSketch2()
 | 
			
		||||
    await expect(page.locator('.cm-activeLine')).toHaveText(`
 | 
			
		||||
      plane001 = offsetPlane('XZ', 50)
 | 
			
		||||
    `)
 | 
			
		||||
    await page.keyboard.press('Backspace')
 | 
			
		||||
    // Check for sketch 1
 | 
			
		||||
    await scene.expectPixelColor([254, 254, 254], testPoint, 15)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const shellPointAndClickCapCases = [
 | 
			
		||||
  { shouldPreselect: true },
 | 
			
		||||
  { shouldPreselect: false },
 | 
			
		||||
]
 | 
			
		||||
shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
 | 
			
		||||
  test(`Shell point-and-click cap (preselected sketches: ${shouldPreselect})`, async ({
 | 
			
		||||
    context,
 | 
			
		||||
    page,
 | 
			
		||||
    homePage,
 | 
			
		||||
    scene,
 | 
			
		||||
    editor,
 | 
			
		||||
    toolbar,
 | 
			
		||||
    cmdBar,
 | 
			
		||||
  }) => {
 | 
			
		||||
    // TODO: fix this test on windows after the electron migration
 | 
			
		||||
    test.skip(process.platform === 'win32', 'Skip on windows')
 | 
			
		||||
    const initialCode = `sketch001 = startSketchOn('XZ')
 | 
			
		||||
    |> circle({ center = [0, 0], radius = 30 }, %)
 | 
			
		||||
    extrude001 = extrude(30, sketch001)
 | 
			
		||||
    `
 | 
			
		||||
    await context.addInitScript((initialCode) => {
 | 
			
		||||
      localStorage.setItem('persistCode', initialCode)
 | 
			
		||||
    }, initialCode)
 | 
			
		||||
    await page.setBodyDimensions({ width: 1000, height: 500 })
 | 
			
		||||
    await homePage.goToModelingScene()
 | 
			
		||||
 | 
			
		||||
    // One dumb hardcoded screen pixel value
 | 
			
		||||
    const testPoint = { x: 575, y: 200 }
 | 
			
		||||
    const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
 | 
			
		||||
    const shellDeclaration =
 | 
			
		||||
      "shell001 = shell({ faces = ['end'], thickness = 5 }, extrude001)"
 | 
			
		||||
 | 
			
		||||
    await test.step(`Look for the grey of the shape`, async () => {
 | 
			
		||||
      await scene.expectPixelColor([127, 127, 127], testPoint, 15)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (!shouldPreselect) {
 | 
			
		||||
      await test.step(`Go through the command bar flow without preselected faces`, async () => {
 | 
			
		||||
        await toolbar.shellButton.click()
 | 
			
		||||
        await cmdBar.expectState({
 | 
			
		||||
          stage: 'arguments',
 | 
			
		||||
          currentArgKey: 'selection',
 | 
			
		||||
          currentArgValue: '',
 | 
			
		||||
          headerArguments: {
 | 
			
		||||
            Selection: '',
 | 
			
		||||
            Thickness: '',
 | 
			
		||||
          },
 | 
			
		||||
          highlightedHeaderArg: 'selection',
 | 
			
		||||
          commandName: 'Shell',
 | 
			
		||||
        })
 | 
			
		||||
        await clickOnCap()
 | 
			
		||||
        await page.waitForTimeout(500)
 | 
			
		||||
        await cmdBar.progressCmdBar()
 | 
			
		||||
        await cmdBar.progressCmdBar()
 | 
			
		||||
        await cmdBar.expectState({
 | 
			
		||||
          stage: 'review',
 | 
			
		||||
          headerArguments: {
 | 
			
		||||
            Selection: '1 cap',
 | 
			
		||||
            Thickness: '5',
 | 
			
		||||
          },
 | 
			
		||||
          commandName: 'Shell',
 | 
			
		||||
        })
 | 
			
		||||
        await cmdBar.progressCmdBar()
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      await test.step(`Preselect the cap`, async () => {
 | 
			
		||||
        await clickOnCap()
 | 
			
		||||
        await page.waitForTimeout(500)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {
 | 
			
		||||
        await toolbar.shellButton.click()
 | 
			
		||||
        await cmdBar.progressCmdBar()
 | 
			
		||||
        await cmdBar.progressCmdBar()
 | 
			
		||||
        await cmdBar.expectState({
 | 
			
		||||
          stage: 'review',
 | 
			
		||||
          headerArguments: {
 | 
			
		||||
            Selection: '1 cap',
 | 
			
		||||
            Thickness: '5',
 | 
			
		||||
          },
 | 
			
		||||
          commandName: 'Shell',
 | 
			
		||||
        })
 | 
			
		||||
        await cmdBar.progressCmdBar()
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
 | 
			
		||||
      await editor.expectEditor.toContain(shellDeclaration)
 | 
			
		||||
      await editor.expectState({
 | 
			
		||||
        diagnostics: [],
 | 
			
		||||
        activeLines: [shellDeclaration],
 | 
			
		||||
        highlightedCode: '',
 | 
			
		||||
      })
 | 
			
		||||
      await scene.expectPixelColor([146, 146, 146], testPoint, 15)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test('Shell point-and-click wall', async ({
 | 
			
		||||
  context,
 | 
			
		||||
  page,
 | 
			
		||||
  homePage,
 | 
			
		||||
  scene,
 | 
			
		||||
  editor,
 | 
			
		||||
  toolbar,
 | 
			
		||||
  cmdBar,
 | 
			
		||||
}) => {
 | 
			
		||||
  const initialCode = `sketch001 = startSketchOn('XY')
 | 
			
		||||
  |> startProfileAt([-20, 20], %)
 | 
			
		||||
  |> xLine(40, %)
 | 
			
		||||
  |> yLine(-60, %)
 | 
			
		||||
  |> xLine(-40, %)
 | 
			
		||||
  |> lineTo([profileStartX(%), profileStartY(%)], %)
 | 
			
		||||
  |> close(%)
 | 
			
		||||
extrude001 = extrude(40, sketch001)
 | 
			
		||||
  `
 | 
			
		||||
  await context.addInitScript((initialCode) => {
 | 
			
		||||
    localStorage.setItem('persistCode', initialCode)
 | 
			
		||||
  }, initialCode)
 | 
			
		||||
  await page.setBodyDimensions({ width: 1000, height: 500 })
 | 
			
		||||
  await homePage.goToModelingScene()
 | 
			
		||||
 | 
			
		||||
  // One dumb hardcoded screen pixel value
 | 
			
		||||
  const testPoint = { x: 580, y: 180 }
 | 
			
		||||
  const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
 | 
			
		||||
  const [clickOnWall] = scene.makeMouseHelpers(testPoint.x, testPoint.y + 70)
 | 
			
		||||
  const mutatedCode = 'xLine(-40, %, $seg01)'
 | 
			
		||||
  const shellDeclaration =
 | 
			
		||||
    "shell001 = shell({  faces = ['end', seg01],  thickness = 5}, extrude001)"
 | 
			
		||||
  const formattedOutLastLine = '}, extrude001)'
 | 
			
		||||
 | 
			
		||||
  await test.step(`Look for the grey of the shape`, async () => {
 | 
			
		||||
    await scene.expectPixelColor([99, 99, 99], testPoint, 15)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step(`Go through the command bar flow, selecting a wall and keeping default thickness`, async () => {
 | 
			
		||||
    await toolbar.shellButton.click()
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      stage: 'arguments',
 | 
			
		||||
      currentArgKey: 'selection',
 | 
			
		||||
      currentArgValue: '',
 | 
			
		||||
      headerArguments: {
 | 
			
		||||
        Selection: '',
 | 
			
		||||
        Thickness: '',
 | 
			
		||||
      },
 | 
			
		||||
      highlightedHeaderArg: 'selection',
 | 
			
		||||
      commandName: 'Shell',
 | 
			
		||||
    })
 | 
			
		||||
    await clickOnCap()
 | 
			
		||||
    await page.keyboard.down('Shift')
 | 
			
		||||
    await clickOnWall()
 | 
			
		||||
    await page.waitForTimeout(500)
 | 
			
		||||
    await page.keyboard.up('Shift')
 | 
			
		||||
    await cmdBar.progressCmdBar()
 | 
			
		||||
    await cmdBar.progressCmdBar()
 | 
			
		||||
    await cmdBar.expectState({
 | 
			
		||||
      stage: 'review',
 | 
			
		||||
      headerArguments: {
 | 
			
		||||
        Selection: '1 cap, 1 face',
 | 
			
		||||
        Thickness: '5',
 | 
			
		||||
      },
 | 
			
		||||
      commandName: 'Shell',
 | 
			
		||||
    })
 | 
			
		||||
    await cmdBar.progressCmdBar()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
 | 
			
		||||
    await editor.expectEditor.toContain(mutatedCode)
 | 
			
		||||
    await editor.expectEditor.toContain(shellDeclaration)
 | 
			
		||||
    await editor.expectState({
 | 
			
		||||
      diagnostics: [],
 | 
			
		||||
      activeLines: [formattedOutLastLine],
 | 
			
		||||
      highlightedCode: '',
 | 
			
		||||
    })
 | 
			
		||||
    await scene.expectPixelColor([49, 49, 49], testPoint, 15)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  await test.step('Delete shell via feature tree selection', async () => {
 | 
			
		||||
    await editor.closePane()
 | 
			
		||||
    const operationButton = await toolbar.getFeatureTreeOperation('Shell', 0)
 | 
			
		||||
    await operationButton.click({ button: 'left' })
 | 
			
		||||
    await page.keyboard.press('Backspace')
 | 
			
		||||
    await scene.expectPixelColor([99, 99, 99], testPoint, 15)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const shellSketchOnFacesCases = [
 | 
			
		||||
  `sketch001 = startSketchOn('XZ')
 | 
			
		||||
  |> circle({ center = [0, 0], radius = 100 }, %)
 | 
			
		||||
  |> extrude(100, %)
 | 
			
		||||
 | 
			
		||||
sketch002 = startSketchOn(sketch001, 'END')
 | 
			
		||||
  |> circle({ center = [0, 0], radius = 50 }, %)
 | 
			
		||||
  |> extrude(50, %)
 | 
			
		||||
  `,
 | 
			
		||||
  `sketch001 = startSketchOn('XZ')
 | 
			
		||||
  |> circle({ center = [0, 0], radius = 100 }, %)
 | 
			
		||||
extrude001 = extrude(100, sketch001)
 | 
			
		||||
 | 
			
		||||
sketch002 = startSketchOn(extrude001, 'END')
 | 
			
		||||
  |> circle({ center = [0, 0], radius = 50 }, %)
 | 
			
		||||
extrude002 = extrude(50, sketch002)
 | 
			
		||||
  `,
 | 
			
		||||
]
 | 
			
		||||
shellSketchOnFacesCases.forEach((initialCode, index) => {
 | 
			
		||||
  const hasExtrudesInPipe = index === 0
 | 
			
		||||
  test(`Shell point-and-click sketch on face (extrudes in pipes: ${hasExtrudesInPipe})`, async ({
 | 
			
		||||
    context,
 | 
			
		||||
    page,
 | 
			
		||||
    homePage,
 | 
			
		||||
    scene,
 | 
			
		||||
    editor,
 | 
			
		||||
    toolbar,
 | 
			
		||||
    cmdBar,
 | 
			
		||||
  }) => {
 | 
			
		||||
    await context.addInitScript((initialCode) => {
 | 
			
		||||
      localStorage.setItem('persistCode', initialCode)
 | 
			
		||||
    }, initialCode)
 | 
			
		||||
    await page.setBodyDimensions({ width: 1000, height: 500 })
 | 
			
		||||
    await homePage.goToModelingScene()
 | 
			
		||||
    await scene.waitForExecutionDone()
 | 
			
		||||
 | 
			
		||||
    // One dumb hardcoded screen pixel value
 | 
			
		||||
    const testPoint = { x: 550, y: 295 }
 | 
			
		||||
    const [clickOnCap] = scene.makeMouseHelpers(testPoint.x, testPoint.y)
 | 
			
		||||
    const shellDeclaration = `shell001 = shell({ faces = ['end'], thickness = 5 }, ${
 | 
			
		||||
      hasExtrudesInPipe ? 'sketch002' : 'extrude002'
 | 
			
		||||
    })`
 | 
			
		||||
 | 
			
		||||
    await test.step(`Look for the grey of the shape`, async () => {
 | 
			
		||||
      await toolbar.closePane('code')
 | 
			
		||||
      await scene.expectPixelColor([128, 128, 128], testPoint, 15)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    await test.step(`Go through the command bar flow, selecting a cap and keeping default thickness`, async () => {
 | 
			
		||||
      await toolbar.shellButton.click()
 | 
			
		||||
      await cmdBar.expectState({
 | 
			
		||||
        stage: 'arguments',
 | 
			
		||||
        currentArgKey: 'selection',
 | 
			
		||||
        currentArgValue: '',
 | 
			
		||||
        headerArguments: {
 | 
			
		||||
          Selection: '',
 | 
			
		||||
          Thickness: '',
 | 
			
		||||
        },
 | 
			
		||||
        highlightedHeaderArg: 'selection',
 | 
			
		||||
        commandName: 'Shell',
 | 
			
		||||
      })
 | 
			
		||||
      await clickOnCap()
 | 
			
		||||
      await cmdBar.progressCmdBar()
 | 
			
		||||
      await page.waitForTimeout(500)
 | 
			
		||||
      await cmdBar.progressCmdBar()
 | 
			
		||||
      await cmdBar.expectState({
 | 
			
		||||
        stage: 'review',
 | 
			
		||||
        headerArguments: {
 | 
			
		||||
          Selection: '1 cap',
 | 
			
		||||
          Thickness: '5',
 | 
			
		||||
        },
 | 
			
		||||
        commandName: 'Shell',
 | 
			
		||||
      })
 | 
			
		||||
      await cmdBar.progressCmdBar()
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    await test.step(`Confirm code is added to the editor, scene has changed`, async () => {
 | 
			
		||||
      await toolbar.openPane('code')
 | 
			
		||||
      await editor.expectEditor.toContain(shellDeclaration)
 | 
			
		||||
      await editor.expectState({
 | 
			
		||||
        diagnostics: [],
 | 
			
		||||
        activeLines: [shellDeclaration],
 | 
			
		||||
        highlightedCode: '',
 | 
			
		||||
      })
 | 
			
		||||
      await toolbar.closePane('code')
 | 
			
		||||
      await scene.expectPixelColor([73, 73, 73], testPoint, 15)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -32,10 +32,9 @@ export default defineConfig({
 | 
			
		||||
  },
 | 
			
		||||
  projects: [
 | 
			
		||||
    {
 | 
			
		||||
      name: 'Google Chrome',
 | 
			
		||||
      name: 'chromium',
 | 
			
		||||
      use: {
 | 
			
		||||
        ...devices['Desktop Chrome'],
 | 
			
		||||
        channel: 'chrome',
 | 
			
		||||
        contextOptions: {
 | 
			
		||||
          /* Chromium is the only one with these permission types */
 | 
			
		||||
          permissions: ['clipboard-write', 'clipboard-read'],
 | 
			
		||||
 | 
			
		||||
@ -47,6 +47,9 @@ export type ModelingCommandSchema = {
 | 
			
		||||
  Revolve: {
 | 
			
		||||
    selection: Selections
 | 
			
		||||
    angle: KclCommandValue
 | 
			
		||||
    axisOrEdge: string
 | 
			
		||||
    axis: string
 | 
			
		||||
    edge: Selections
 | 
			
		||||
  }
 | 
			
		||||
  Fillet: {
 | 
			
		||||
    // todo
 | 
			
		||||
@ -289,10 +292,44 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  // TODO: Update this configuration, copied from extrude for MVP of revolve, specifically the args.selection
 | 
			
		||||
  Loft: {
 | 
			
		||||
    description: 'Create a 3D body by blending between two or more sketches',
 | 
			
		||||
    icon: 'loft',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    args: {
 | 
			
		||||
      selection: {
 | 
			
		||||
        inputType: 'selection',
 | 
			
		||||
        selectionTypes: ['solid2D'],
 | 
			
		||||
        multiple: true,
 | 
			
		||||
        required: true,
 | 
			
		||||
        skip: false,
 | 
			
		||||
        validation: loftValidator,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  Shell: {
 | 
			
		||||
    description: 'Hollow out a 3D solid.',
 | 
			
		||||
    icon: 'shell',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    args: {
 | 
			
		||||
      selection: {
 | 
			
		||||
        inputType: 'selection',
 | 
			
		||||
        selectionTypes: ['cap', 'wall'],
 | 
			
		||||
        multiple: true,
 | 
			
		||||
        required: true,
 | 
			
		||||
        skip: false,
 | 
			
		||||
      },
 | 
			
		||||
      thickness: {
 | 
			
		||||
        inputType: 'kcl',
 | 
			
		||||
        defaultValue: KCL_DEFAULT_LENGTH,
 | 
			
		||||
        required: true,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  Revolve: {
 | 
			
		||||
    description: 'Create a 3D body by rotating a sketch region about an axis.',
 | 
			
		||||
    icon: 'revolve',
 | 
			
		||||
    status: 'development',
 | 
			
		||||
    needsReview: true,
 | 
			
		||||
    args: {
 | 
			
		||||
      selection: {
 | 
			
		||||
@ -301,6 +338,38 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
 | 
			
		||||
        multiple: false, // TODO: multiple selection
 | 
			
		||||
        required: true,
 | 
			
		||||
        skip: true,
 | 
			
		||||
        warningMessage:
 | 
			
		||||
          'The revolve workflow is new and under tested. Please break it and report issues.',
 | 
			
		||||
      },
 | 
			
		||||
      axisOrEdge: {
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        required: true,
 | 
			
		||||
        defaultValue: 'Axis',
 | 
			
		||||
        options: [
 | 
			
		||||
          { name: 'Axis', isCurrent: true, value: 'Axis' },
 | 
			
		||||
          { name: 'Edge', isCurrent: false, value: 'Edge' },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      axis: {
 | 
			
		||||
        required: (commandContext) =>
 | 
			
		||||
          ['Axis'].includes(
 | 
			
		||||
            commandContext.argumentsToSubmit.axisOrEdge as string
 | 
			
		||||
          ),
 | 
			
		||||
        inputType: 'options',
 | 
			
		||||
        options: [
 | 
			
		||||
          { name: 'X Axis', isCurrent: true, value: 'X' },
 | 
			
		||||
          { name: 'Y Axis', isCurrent: false, value: 'Y' },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      edge: {
 | 
			
		||||
        required: (commandContext) =>
 | 
			
		||||
          ['Edge'].includes(
 | 
			
		||||
            commandContext.argumentsToSubmit.axisOrEdge as string
 | 
			
		||||
          ),
 | 
			
		||||
        inputType: 'selection',
 | 
			
		||||
        selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
 | 
			
		||||
        multiple: false,
 | 
			
		||||
        validation: revolveAxisValidator,
 | 
			
		||||
      },
 | 
			
		||||
      angle: {
 | 
			
		||||
        inputType: 'kcl',
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,155 @@
 | 
			
		||||
import { Models } from '@kittycad/lib'
 | 
			
		||||
import { engineCommandManager } from 'lib/singletons'
 | 
			
		||||
import { uuidv4 } from 'lib/utils'
 | 
			
		||||
import { CommandBarContext } from 'machines/commandBarMachine'
 | 
			
		||||
import { Selections } from 'lib/selections'
 | 
			
		||||
 | 
			
		||||
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
 | 
			
		||||
  for (let tries = 0; tries < numberOfRetries; tries++) {
 | 
			
		||||
    try {
 | 
			
		||||
      await engineCommandManager.sendSceneCommand({
 | 
			
		||||
        type: 'modeling_cmd_req',
 | 
			
		||||
        cmd_id: uuidv4(),
 | 
			
		||||
        cmd: { type: 'disable_dry_run' },
 | 
			
		||||
      })
 | 
			
		||||
      // Exit out since the command was successful
 | 
			
		||||
      return
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error(e)
 | 
			
		||||
      console.error('disable_dry_run failed. This is bad!')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Takes a callback function and wraps it around enable_dry_run and disable_dry_run
 | 
			
		||||
export const dryRunWrapper = async (callback: () => Promise<any>) => {
 | 
			
		||||
  // Gotcha: What about race conditions?
 | 
			
		||||
  try {
 | 
			
		||||
    await engineCommandManager.sendSceneCommand({
 | 
			
		||||
      type: 'modeling_cmd_req',
 | 
			
		||||
      cmd_id: uuidv4(),
 | 
			
		||||
      cmd: { type: 'enable_dry_run' },
 | 
			
		||||
    })
 | 
			
		||||
    const result = await callback()
 | 
			
		||||
    return result
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    console.error(e)
 | 
			
		||||
  } finally {
 | 
			
		||||
    await disableDryRunWithRetry(5)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isSelections(selections: unknown): selections is Selections {
 | 
			
		||||
  return (
 | 
			
		||||
    (selections as Selections).graphSelections !== undefined &&
 | 
			
		||||
    (selections as Selections).otherSelections !== undefined
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const revolveAxisValidator = async ({
 | 
			
		||||
  data,
 | 
			
		||||
  context,
 | 
			
		||||
}: {
 | 
			
		||||
  data: { [key: string]: Selections }
 | 
			
		||||
  context: CommandBarContext
 | 
			
		||||
}): Promise<boolean | string> => {
 | 
			
		||||
  if (!isSelections(context.argumentsToSubmit.selection)) {
 | 
			
		||||
    return 'Unable to revolve, selections are missing'
 | 
			
		||||
  }
 | 
			
		||||
  const artifact =
 | 
			
		||||
    context.argumentsToSubmit.selection.graphSelections[0].artifact
 | 
			
		||||
 | 
			
		||||
  if (!artifact) {
 | 
			
		||||
    return 'Unable to revolve, sketch not found'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!('pathId' in artifact)) {
 | 
			
		||||
    return 'Unable to revolve, sketch has no path'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const sketchSelection = artifact.pathId
 | 
			
		||||
  let edgeSelection = data.edge.graphSelections[0].artifact?.id
 | 
			
		||||
 | 
			
		||||
  if (!sketchSelection) {
 | 
			
		||||
    return 'Unable to revolve, sketch is missing'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!edgeSelection) {
 | 
			
		||||
    return 'Unable to revolve, edge is missing'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const angleInDegrees: Models['Angle_type'] = {
 | 
			
		||||
    unit: 'degrees',
 | 
			
		||||
    value: 360,
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const revolveAboutEdgeCommand = async () => {
 | 
			
		||||
    return await engineCommandManager.sendSceneCommand({
 | 
			
		||||
      type: 'modeling_cmd_req',
 | 
			
		||||
      cmd_id: uuidv4(),
 | 
			
		||||
      cmd: {
 | 
			
		||||
        type: 'revolve_about_edge',
 | 
			
		||||
        angle: angleInDegrees,
 | 
			
		||||
        edge_id: edgeSelection,
 | 
			
		||||
        target: sketchSelection,
 | 
			
		||||
        tolerance: 0.0001,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
 | 
			
		||||
  if (attemptRevolve?.success) {
 | 
			
		||||
    return true
 | 
			
		||||
  } else {
 | 
			
		||||
    // return error message for the toast
 | 
			
		||||
    return 'Unable to revolve with selected edge'
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const loftValidator = async ({
 | 
			
		||||
  data,
 | 
			
		||||
}: {
 | 
			
		||||
  data: { [key: string]: Selections }
 | 
			
		||||
  context: CommandBarContext
 | 
			
		||||
}): Promise<boolean | string> => {
 | 
			
		||||
  if (!isSelections(data.selection)) {
 | 
			
		||||
    return 'Unable to loft, selections are missing'
 | 
			
		||||
  }
 | 
			
		||||
  const { selection } = data
 | 
			
		||||
 | 
			
		||||
  if (selection.graphSelections.some((s) => s.artifact?.type !== 'solid2D')) {
 | 
			
		||||
    return 'Unable to loft, some selection are not solid2Ds'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const sectionIds = data.selection.graphSelections.flatMap((s) =>
 | 
			
		||||
    s.artifact?.type === 'solid2D' ? s.artifact.pathId : []
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  if (sectionIds.length < 2) {
 | 
			
		||||
    return 'Unable to loft, selection contains less than two solid2Ds'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const loftCommand = async () => {
 | 
			
		||||
    // TODO: check what to do with these
 | 
			
		||||
    const DEFAULT_V_DEGREE = 2
 | 
			
		||||
    const DEFAULT_TOLERANCE = 2
 | 
			
		||||
    const DEFAULT_BEZ_APPROXIMATE_RATIONAL = false
 | 
			
		||||
    return await engineCommandManager.sendSceneCommand({
 | 
			
		||||
      type: 'modeling_cmd_req',
 | 
			
		||||
      cmd_id: uuidv4(),
 | 
			
		||||
      cmd: {
 | 
			
		||||
        section_ids: sectionIds,
 | 
			
		||||
        type: 'loft',
 | 
			
		||||
        bez_approximate_rational: DEFAULT_BEZ_APPROXIMATE_RATIONAL,
 | 
			
		||||
        tolerance: DEFAULT_TOLERANCE,
 | 
			
		||||
        v_degree: DEFAULT_V_DEGREE,
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  const attempt = await dryRunWrapper(loftCommand)
 | 
			
		||||
  if (attempt?.success) {
 | 
			
		||||
    return true
 | 
			
		||||
  } else {
 | 
			
		||||
    // return error message for the toast
 | 
			
		||||
    return 'Unable to loft with selected sketches'
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ import {
 | 
			
		||||
  StateMachineCommandSetSchema,
 | 
			
		||||
} from './commandTypes'
 | 
			
		||||
import { DEV } from 'env'
 | 
			
		||||
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
 | 
			
		||||
 | 
			
		||||
interface CreateMachineCommandProps<
 | 
			
		||||
  T extends AnyStateMachine,
 | 
			
		||||
@ -84,7 +85,7 @@ export function createMachineCommand<
 | 
			
		||||
  } else if ('status' in commandConfig) {
 | 
			
		||||
    const { status } = commandConfig
 | 
			
		||||
    if (status === 'inactive') return null
 | 
			
		||||
    if (status === 'development' && !DEV) return null
 | 
			
		||||
    if (status === 'development' && !(DEV || IS_NIGHTLY_OR_DEBUG)) return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const icon = ('icon' in commandConfig && commandConfig.icon) || undefined
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import {
 | 
			
		||||
  modelingMachine,
 | 
			
		||||
  pipeHasCircle,
 | 
			
		||||
} from 'machines/modelingMachine'
 | 
			
		||||
import { IS_NIGHTLY_OR_DEBUG } from 'routes/Settings'
 | 
			
		||||
import { EventFrom, StateFrom } from 'xstate'
 | 
			
		||||
 | 
			
		||||
export type ToolbarModeName = 'modeling' | 'sketching'
 | 
			
		||||
@ -103,7 +104,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
            data: { name: 'Revolve', groupId: 'modeling' },
 | 
			
		||||
          }),
 | 
			
		||||
        icon: 'revolve',
 | 
			
		||||
        status: DEV ? 'available' : 'kcl-only',
 | 
			
		||||
        status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
 | 
			
		||||
        title: 'Revolve',
 | 
			
		||||
        hotkey: 'R',
 | 
			
		||||
        description:
 | 
			
		||||
@ -161,7 +162,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
 | 
			
		||||
            data: { name: 'Fillet', groupId: 'modeling' },
 | 
			
		||||
          }),
 | 
			
		||||
        icon: 'fillet3d',
 | 
			
		||||
        status: DEV ? 'available' : 'kcl-only',
 | 
			
		||||
        status: DEV || IS_NIGHTLY_OR_DEBUG ? 'available' : 'kcl-only',
 | 
			
		||||
        title: 'Fillet',
 | 
			
		||||
        hotkey: 'F',
 | 
			
		||||
        description: 'Round the edges of a 3D solid.',
 | 
			
		||||
 | 
			
		||||
@ -32,6 +32,8 @@ export const PACKAGE_NAME = isDesktop()
 | 
			
		||||
 | 
			
		||||
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
 | 
			
		||||
 | 
			
		||||
export const IS_NIGHTLY_OR_DEBUG = IS_NIGHTLY || APP_VERSION === '0.0.0'
 | 
			
		||||
 | 
			
		||||
export function getReleaseUrl(version: string = APP_VERSION) {
 | 
			
		||||
  return `https://github.com/KittyCAD/modeling-app/releases/tag/${
 | 
			
		||||
    IS_NIGHTLY ? 'nightly-' : ''
 | 
			
		||||
 | 
			
		||||
@ -1087,6 +1087,17 @@ async fn inner_start_sketch_on(
 | 
			
		||||
 | 
			
		||||
            Ok(SketchSurface::Plane(plane))
 | 
			
		||||
        }
 | 
			
		||||
        SketchData::Plane(plane) => {
 | 
			
		||||
            // Create artifact used only by the UI, not the engine.
 | 
			
		||||
            let id = exec_state.next_uuid();
 | 
			
		||||
            exec_state.add_artifact(Artifact {
 | 
			
		||||
                id: ArtifactId::from(id),
 | 
			
		||||
                inner: ArtifactInner::StartSketchOnPlane { plane_id: plane.id },
 | 
			
		||||
                source_range: args.source_range,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            Ok(SketchSurface::Plane(plane))
 | 
			
		||||
        }
 | 
			
		||||
        SketchData::Plane(plane) => Ok(SketchSurface::Plane(plane)),
 | 
			
		||||
        SketchData::Solid(solid) => {
 | 
			
		||||
            let Some(tag) = tag else {
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user