Add keyboard shortcuts for sketch and modeling tools (#2419)

* Add keyboard shortcuts for sketch and modeling tools

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* Add a playwright test

* skip linux

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fmt fml

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* Give more generous test timeout for worst case engine runs

* Fix up test mouse clicks after zoom bug fixes

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
Co-authored-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Frank Noirot
2024-05-22 11:07:02 -04:00
committed by GitHub
parent 48ef0885b7
commit 0384e5e6c6
6 changed files with 205 additions and 3 deletions

View File

@ -2213,3 +2213,105 @@ test('Extrude from command bar selects extrude line after', async ({
` |> extrude(${KCL_DEFAULT_LENGTH}, %)`
)
})
test('Basic default modeling and sketch hotkeys work', async ({ page }) => {
// This test can run long if it takes a little too long to load
// the engine.
test.setTimeout(90000)
// This test has a weird bug on ubuntu
test.skip(
process.platform === 'linux',
'weird playwright bug on ubuntu https://github.com/KittyCAD/modeling-app/issues/2444'
)
// Load the app with the code pane open
await page.addInitScript(async () => {
localStorage.setItem(
'store',
JSON.stringify({
state: {
openPanes: ['code'],
},
version: 0,
})
)
})
// Wait for the app to be ready for use
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/')
await u.waitForAuthSkipAppStart()
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.closeDebugPanel()
const codePane = page.getByRole('textbox').locator('div')
const codePaneButton = page.getByRole('tab', { name: 'KCL Code' })
const lineButton = page.getByRole('button', { name: 'Line' })
const arcButton = page.getByRole('button', { name: 'Tangential Arc' })
const extrudeButton = page.getByRole('button', { name: 'Extrude' })
// Test that the hotkeys do nothing when
// focus is on the code pane
await codePane.click()
await page.keyboard.press('s')
await page.keyboard.press('l')
await page.keyboard.press('a')
await page.keyboard.press('e')
await expect(page.locator('.cm-content')).toHaveText('slae')
await page.keyboard.press('Meta+/')
// Test these hotkeys perform actions when
// focus is on the canvas
await page.mouse.move(600, 250)
await page.mouse.click(600, 250)
// Start a sketch
await page.keyboard.press('s')
await page.mouse.move(800, 300)
await page.mouse.click(800, 300)
await page.waitForTimeout(1000)
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
/**
* TODO: There is a bug somewhere that causes this test to fail
* if you toggle the codePane closed before your trigger the
* start of the sketch.
*/
await codePaneButton.click()
// Draw a line
await page.mouse.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200)
await page.mouse.move(800, 250, { steps: 5 })
await page.mouse.click(800, 250)
// Unequip line tool
await page.keyboard.press('l')
await expect(lineButton).not.toHaveAttribute('aria-pressed', 'true')
// Equip arc tool
await page.keyboard.press('a')
await expect(arcButton).toHaveAttribute('aria-pressed', 'true')
await page.mouse.move(1000, 100, { steps: 5 })
await page.mouse.click(1000, 100)
await page.keyboard.press('Escape')
await page.keyboard.press('l')
await expect(lineButton).toHaveAttribute('aria-pressed', 'true')
// Close profile
await page.mouse.move(700, 200, { steps: 5 })
await page.mouse.click(700, 200)
// Unequip line tool
await page.keyboard.press('Escape')
// Exit sketch
await page.keyboard.press('Escape')
// Extrude
await page.mouse.click(750, 150)
await expect(extrudeButton).not.toBeDisabled()
await page.keyboard.press('e')
await page.mouse.move(850, 180, { steps: 5 })
await page.mouse.click(850, 180)
await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Continue' }).click()
await page.getByRole('button', { name: 'Submit command' }).click()
await codePaneButton.click()
await expect(page.locator('.cm-content')).toContainText('extrude(')
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -58,9 +58,8 @@ export function App() {
const {
app: { onboardingStatus },
} = settings.context
const { state, send } = useModelingContext()
const { state } = useModelingContext()
useHotkeys('esc', () => send('Cancel'))
useHotkeys('backspace', (e) => {
e.preventDefault()
})

View File

@ -12,6 +12,8 @@ import {
} from 'components/NetworkHealthIndicator'
import { useStore } from 'useStore'
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from 'components/Tooltip'
export const Toolbar = () => {
const { commandBarSend } = useCommandsContext()
@ -40,6 +42,56 @@ export const Toolbar = () => {
const disableAllButtons =
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
useHotkeys(
'l',
() =>
state.matches('Sketch.Line tool')
? send('CancelSketch')
: send('Equip Line tool'),
{ enabled: !disableAllButtons, scopes: ['sketch'] }
)
useHotkeys(
'a',
() =>
state.matches('Sketch.Tangential arc to')
? send('CancelSketch')
: send('Equip tangential arc to'),
{ enabled: !disableAllButtons, scopes: ['sketch'] }
)
useHotkeys(
'r',
() =>
state.matches('Sketch.Rectangle tool')
? send('CancelSketch')
: send('Equip rectangle tool'),
{ enabled: !disableAllButtons, scopes: ['sketch'] }
)
useHotkeys(
's',
() =>
state.nextEvents.includes('Enter sketch') && pathId
? send({ type: 'Enter sketch' })
: send({ type: 'Enter sketch', data: { forceNewSketch: true } }),
{ enabled: !disableAllButtons, scopes: ['modeling'] }
)
useHotkeys(
'esc',
() =>
state.matches('Sketch.SketchIdle')
? send('Cancel')
: send('CancelSketch'),
{ enabled: !disableAllButtons, scopes: ['sketch'] }
)
useHotkeys(
'e',
() =>
commandBarSend({
type: 'Find and select command',
data: { name: 'Extrude', ownerMachine: 'modeling' },
}),
{ enabled: !disableAllButtons, scopes: ['modeling'] }
)
function handleToolbarButtonsWheelEvent(ev: WheelEvent<HTMLSpanElement>) {
const span = toolbarButtonsRef.current
if (!span) {
@ -77,6 +129,13 @@ export const Toolbar = () => {
disabled={disableAllButtons}
>
<span data-testid="start-sketch">Start Sketch</span>
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: S
</Tooltip>
</ActionButton>
</li>
)}
@ -94,6 +153,13 @@ export const Toolbar = () => {
disabled={disableAllButtons}
>
Edit Sketch
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: S
</Tooltip>
</ActionButton>
</li>
)}
@ -111,6 +177,13 @@ export const Toolbar = () => {
disabled={disableAllButtons}
>
Exit Sketch
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: Esc
</Tooltip>
</ActionButton>
</li>
)}
@ -134,6 +207,13 @@ export const Toolbar = () => {
disabled={disableAllButtons}
>
Line
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: L
</Tooltip>
</ActionButton>
</li>
<li className="contents" key="tangential-arc-button">
@ -158,6 +238,13 @@ export const Toolbar = () => {
}
>
Tangential Arc
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: A
</Tooltip>
</ActionButton>
</li>
<li className="contents" key="rectangle-button">
@ -187,6 +274,13 @@ export const Toolbar = () => {
}
>
Rectangle
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: R
</Tooltip>
</ActionButton>
</li>
</>
@ -264,6 +358,13 @@ export const Toolbar = () => {
}}
>
Extrude
<Tooltip
delay={1250}
position="bottom"
className="!px-2 !text-xs"
>
Shortcut: E
</Tooltip>
</ActionButton>
</li>
)}

View File

@ -290,7 +290,7 @@ export const ModelingMachineProvider = ({
kclManager.ast,
sketchDetails?.sketchPathToNode || [],
'VariableDeclaration'
)?.node?.declarations[0]?.init.type !== 'PipeExpression',
)?.node?.declarations?.[0]?.init.type !== 'PipeExpression',
'Selection is on face': ({ selectionRanges }, { data }) => {
if (data?.forceNewSketch) return false
if (!isSingleCursorInPipe(selectionRanges, kclManager.ast))