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
|
||||
|
||||
@ -493,719 +493,6 @@ sketch002 = startSketchOn(extrude001, seg03)
|
||||
`,
|
||||
{ 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')
|
||||
}
|
||||
|
||||
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