diff --git a/e2e/playwright/fixtures/sceneFixture.ts b/e2e/playwright/fixtures/sceneFixture.ts index bfa55e834f..8772dffe96 100644 --- a/e2e/playwright/fixtures/sceneFixture.ts +++ b/e2e/playwright/fixtures/sceneFixture.ts @@ -13,6 +13,13 @@ type mouseParams = { pixelDiff: number } +type SceneSerialised = { + camera: { + position: [number, number, number] + target: [number, number, number] + } +} + export class SceneFixture { public page: Page @@ -22,6 +29,22 @@ export class SceneFixture { this.page = page this.reConstruct(page) } + private _serialiseScene = async (): Promise => { + const camera = await this.getCameraInfo() + + return { + camera, + } + } + + expectState = async (expected: SceneSerialised) => { + return expect + .poll(() => this._serialiseScene(), { + message: `Expected scene state to match`, + }) + .toEqual(expected) + } + reConstruct = (page: Page) => { this.page = page @@ -31,7 +54,7 @@ export class SceneFixture { makeMouseHelpers = ( x: number, y: number, - { steps }: { steps: number } = { steps: 5000 } + { steps }: { steps: number } = { steps: 20 } ) => [ (clickParams?: mouseParams) => { @@ -87,6 +110,36 @@ export class SceneFixture { ) await closeDebugPanel(this.page) } + /** Forces a refresh of the camera position and target displayed + * in the debug panel and then returns the values of the fields + */ + async getCameraInfo() { + await openAndClearDebugPanel(this.page) + await sendCustomCmd(this.page, { + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'default_camera_get_settings', + }, + }) + await this.waitForExecutionDone() + const position = await Promise.all([ + this.page.getByTestId('cam-x-position').inputValue().then(Number), + this.page.getByTestId('cam-y-position').inputValue().then(Number), + this.page.getByTestId('cam-z-position').inputValue().then(Number), + ]) + const target = await Promise.all([ + this.page.getByTestId('cam-x-target').inputValue().then(Number), + this.page.getByTestId('cam-y-target').inputValue().then(Number), + this.page.getByTestId('cam-z-target').inputValue().then(Number), + ]) + await closeDebugPanel(this.page) + return { + position, + target, + } + } + waitForExecutionDone = async () => { await expect(this.exeIndicator).toBeVisible() } @@ -114,4 +167,17 @@ export class SceneFixture { ) }) } + + get gizmo() { + return this.page.locator('[aria-label*=gizmo]') + } + + async clickGizmoMenuItem(name: string) { + await this.gizmo.click({ button: 'right' }) + const buttonToTest = this.page.getByRole('button', { + name: name, + }) + await expect(buttonToTest).toBeVisible() + await buttonToTest.click() + } } diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png index e3c6e45fde..e1a6009bce 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-1-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png index 8800042452..ec79f0465b 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/Client-side-scene-scale-should-match-engine-scale-Millimeter-scale-2-Google-Chrome-linux.png differ diff --git a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-win32.png b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-win32.png index d84a8b2729..e391ba2d03 100644 Binary files a/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-win32.png and b/e2e/playwright/snapshot-tests.spec.ts-snapshots/extrude-on-default-planes-should-be-stable-XY-1-Google-Chrome-win32.png differ diff --git a/e2e/playwright/testing-gizmo.spec.ts b/e2e/playwright/testing-gizmo.spec.ts index a058bc3ae3..bdd76fb15e 100644 --- a/e2e/playwright/testing-gizmo.spec.ts +++ b/e2e/playwright/testing-gizmo.spec.ts @@ -1,18 +1,18 @@ -import { test, expect } from '@playwright/test' - +import { _test, _expect } from './playwright-deprecated' +import { test } from './fixtures/fixtureSetup' import { getUtils, setup, tearDown } from './test-utils' import { uuidv4 } from 'lib/utils' import { TEST_CODE_GIZMO } from './storageStates' -test.beforeEach(async ({ context, page }, testInfo) => { +_test.beforeEach(async ({ context, page }, testInfo) => { await setup(context, page, testInfo) }) -test.afterEach(async ({ page }, testInfo) => { +_test.afterEach(async ({ page }, testInfo) => { await tearDown(page, testInfo) }) -test.describe('Testing Gizmo', () => { +_test.describe('Testing Gizmo', () => { const cases = [ { testDescription: 'top view', @@ -57,7 +57,7 @@ test.describe('Testing Gizmo', () => { expectedCameraTarget, testDescription, } of cases) { - test(`check ${testDescription}`, async ({ page, browserName }) => { + _test(`check ${testDescription}`, async ({ page, browserName }) => { const u = await getUtils(page) await page.addInitScript((TEST_CODE_GIZMO) => { localStorage.setItem('persistCode', TEST_CODE_GIZMO) @@ -117,30 +117,30 @@ test.describe('Testing Gizmo', () => { await Promise.all([ // position - expect(page.getByTestId('cam-x-position')).toHaveValue( + _expect(page.getByTestId('cam-x-position')).toHaveValue( expectedCameraPosition.x.toString() ), - expect(page.getByTestId('cam-y-position')).toHaveValue( + _expect(page.getByTestId('cam-y-position')).toHaveValue( expectedCameraPosition.y.toString() ), - expect(page.getByTestId('cam-z-position')).toHaveValue( + _expect(page.getByTestId('cam-z-position')).toHaveValue( expectedCameraPosition.z.toString() ), // target - expect(page.getByTestId('cam-x-target')).toHaveValue( + _expect(page.getByTestId('cam-x-target')).toHaveValue( expectedCameraTarget.x.toString() ), - expect(page.getByTestId('cam-y-target')).toHaveValue( + _expect(page.getByTestId('cam-y-target')).toHaveValue( expectedCameraTarget.y.toString() ), - expect(page.getByTestId('cam-z-target')).toHaveValue( + _expect(page.getByTestId('cam-z-target')).toHaveValue( expectedCameraTarget.z.toString() ), ]) }) } - test('Context menu and popover menu', async ({ page }) => { + _test('Context menu and popover menu', async ({ page }) => { const testCase = { testDescription: 'Right view', expectedCameraPosition: { x: 5660.02, y: -152, z: 26 }, @@ -196,7 +196,7 @@ test.describe('Testing Gizmo', () => { const buttonToTest = page.getByRole('button', { name: testCase.testDescription, }) - await expect(buttonToTest).toBeVisible() + await _expect(buttonToTest).toBeVisible() await buttonToTest.click() // Now assert we've moved to the correct view @@ -215,23 +215,23 @@ test.describe('Testing Gizmo', () => { await Promise.all([ // position - expect(page.getByTestId('cam-x-position')).toHaveValue( + _expect(page.getByTestId('cam-x-position')).toHaveValue( testCase.expectedCameraPosition.x.toString() ), - expect(page.getByTestId('cam-y-position')).toHaveValue( + _expect(page.getByTestId('cam-y-position')).toHaveValue( testCase.expectedCameraPosition.y.toString() ), - expect(page.getByTestId('cam-z-position')).toHaveValue( + _expect(page.getByTestId('cam-z-position')).toHaveValue( testCase.expectedCameraPosition.z.toString() ), // target - expect(page.getByTestId('cam-x-target')).toHaveValue( + _expect(page.getByTestId('cam-x-target')).toHaveValue( testCase.expectedCameraTarget.x.toString() ), - expect(page.getByTestId('cam-y-target')).toHaveValue( + _expect(page.getByTestId('cam-y-target')).toHaveValue( testCase.expectedCameraTarget.y.toString() ), - expect(page.getByTestId('cam-z-target')).toHaveValue( + _expect(page.getByTestId('cam-z-target')).toHaveValue( testCase.expectedCameraTarget.z.toString() ), ]) @@ -242,8 +242,60 @@ test.describe('Testing Gizmo', () => { const gizmoPopoverButton = page.getByRole('button', { name: 'view settings', }) - await expect(gizmoPopoverButton).toBeVisible() + await _expect(gizmoPopoverButton).toBeVisible() await gizmoPopoverButton.click() - await expect(buttonToTest).toBeVisible() + await _expect(buttonToTest).toBeVisible() + }) +}) + +test.describe(`Testing gizmo, fixture-based`, () => { + test('Center on selection from menu', async ({ + app, + cmdBar, + editor, + toolbar, + scene, + }) => { + test.skip( + process.platform === 'win32', + 'Fails on windows in CI, can not be replicated locally on windows.' + ) + + await test.step(`Setup`, async () => { + const file = await app.getInputFile('test-circle-extrude.kcl') + await app.initialise(file) + await scene.expectState({ + camera: { + position: [4982.21, -23865.37, 13810.64], + target: [4982.21, 0, 2737.1], + }, + }) + }) + const [clickCircle, moveToCircle] = scene.makeMouseHelpers(582, 217) + + await test.step(`Select an edge of this circle`, async () => { + const circleSnippet = + 'circle({ center: [318.33, 168.1], radius: 182.8 }, %)' + await moveToCircle() + await clickCircle() + await editor.expectState({ + activeLines: [circleSnippet.slice(-5)], + highlightedCode: circleSnippet, + diagnostics: [], + }) + }) + + await test.step(`Center on selection from menu`, async () => { + await scene.clickGizmoMenuItem('Center view on selection') + }) + + await test.step(`Verify the camera moved`, async () => { + await scene.expectState({ + camera: { + position: [0, -23865.37, 11073.54], + target: [0, 0, 0], + }, + }) + }) }) }) diff --git a/package.json b/package.json index f519c94243..7e763e0c79 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@headlessui/react": "^1.7.19", "@headlessui/tailwindcss": "^0.2.0", - "@kittycad/lib": "^2.0.1", + "@kittycad/lib": "2.0.7", "@lezer/highlight": "^1.2.1", "@lezer/lr": "^1.4.1", "@react-hook/resize-observer": "^2.0.1", diff --git a/src/clientSideScene/CameraControls.ts b/src/clientSideScene/CameraControls.ts index 13656712df..c7282003ce 100644 --- a/src/clientSideScene/CameraControls.ts +++ b/src/clientSideScene/CameraControls.ts @@ -893,6 +893,7 @@ export class CameraControls { type: 'zoom_to_fit', object_ids: [], // leave empty to zoom to all objects padding: 0.2, // padding around the objects + animated: false, // don't animate the zoom for now }, }) } diff --git a/src/components/Gizmo.tsx b/src/components/Gizmo.tsx index 9c345c806a..118f013515 100644 --- a/src/components/Gizmo.tsx +++ b/src/components/Gizmo.tsx @@ -28,6 +28,7 @@ import { import { Popover } from '@headlessui/react' import { CustomIcon } from './CustomIcon' import { reportRejection } from 'lib/trap' +import { useModelingContext } from 'hooks/useModelingContext' const CANVAS_SIZE = 80 const FRUSTUM_SIZE = 0.5 @@ -62,6 +63,7 @@ export default function Gizmo() { const raycasterIntersect = useRef | null>(null) const cameraPassiveUpdateTimer = useRef(0) const raycasterPassiveUpdateTimer = useRef(0) + const { send: modelingSend } = useModelingContext() const menuItems = useMemo( () => [ ...Object.entries(axisNamesSemantic).map(([axisName, axisSemantic]) => ( @@ -76,6 +78,7 @@ export default function Gizmo() { {axisSemantic} view )), + , { sceneInfra.camControls.resetCameraPosition().catch(reportRejection) @@ -83,6 +86,13 @@ export default function Gizmo() { > Reset view , + { + modelingSend({ type: 'Center camera on selection' }) + }} + > + Center view on selection + , , , ], diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index 9403ee3c47..0b0d73dbfc 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -83,6 +83,7 @@ import { } from 'lang/std/engineConnection' import { submitAndAwaitTextToKcl } from 'lib/textToCad' import { useFileContext } from 'hooks/useFileContext' +import { uuidv4 } from 'lib/utils' type MachineContext = { state: StateFrom @@ -243,6 +244,17 @@ export const ModelingMachineProvider = ({ return {} }, }), + 'Center camera on selection': () => { + engineCommandManager + .sendSceneCommand({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'default_camera_center_to_selection', + }, + }) + .catch(reportRejection) + }, 'Set sketchDetails': assign(({ context: { sketchDetails }, event }) => { if (event.type !== 'Delete segment') return {} if (!sketchDetails) return {} @@ -1037,6 +1049,11 @@ export const ModelingMachineProvider = ({ modelingSend({ type: 'Delete selection' }) }) + // Allow ctrl+alt+c to center to selection + useHotkeys(['mod + alt + c'], () => { + modelingSend({ type: 'Center camera on selection' }) + }) + useStateMachineCommands({ machineId: 'modeling', state: modelingState, diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index efd270db1b..d29d953bfc 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -282,6 +282,7 @@ export class KclManager { type: 'zoom_to_fit', object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects padding: 0.1, // padding around the objects + animated: false, // don't animate the zoom for now }, }) } diff --git a/src/lib/settings/initialKeybindings.ts b/src/lib/settings/initialKeybindings.ts index 6a7cfac41d..caead9e5e1 100644 --- a/src/lib/settings/initialKeybindings.ts +++ b/src/lib/settings/initialKeybindings.ts @@ -145,6 +145,13 @@ export const interactionMap: Record< description: 'Available while modeling with either a face selected or an empty selection, when not typing in the code editor.', }, + { + name: 'center-on-selection', + sequence: `${PRIMARY}+Alt+C`, + title: 'Center on selection', + description: + 'Centers the view on the selected geometry, or everything if nothing is selected.', + }, ], 'Code Editor': [ { diff --git a/src/lib/singletons.ts b/src/lib/singletons.ts index 0923501206..a4d1e8d1b4 100644 --- a/src/lib/singletons.ts +++ b/src/lib/singletons.ts @@ -49,6 +49,7 @@ if (typeof window !== 'undefined') { type: 'zoom_to_fit', object_ids: [], // leave empty to zoom to all objects padding: 0.2, // padding around the objects + animated: false, // don't animate the zoom for now }, }) } diff --git a/src/lib/textToCad.ts b/src/lib/textToCad.ts index 72db6724f8..fd6b9bcaea 100644 --- a/src/lib/textToCad.ts +++ b/src/lib/textToCad.ts @@ -249,7 +249,7 @@ export async function submitAndAwaitTextToKcl({ export async function sendTelemetry( id: string, - feedback: Models['AiFeedback_type'], + feedback: Models['MlFeedback_type'], token?: string ): Promise { const url = diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index f829e904db..e53700d5ae 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -252,6 +252,9 @@ export type ModelingMachineEvent = type: 'Set Segment Overlays' data: SegmentOverlayPayload } + | { + type: 'Center camera on selection' + } | { type: 'Delete segment' data: PathToNode @@ -938,6 +941,7 @@ export const modelingMachine = setup({ 'Set selection': () => {}, 'Set mouse state': () => {}, 'Set Segment Overlays': () => {}, + 'Center camera on selection': () => {}, 'Engine export': () => {}, 'Submit to Text-to-CAD API': () => {}, 'Set sketchDetails': () => {}, @@ -2105,6 +2109,10 @@ export const modelingMachine = setup({ reenter: false, actions: 'Set Segment Overlays', }, + 'Center camera on selection': { + reenter: false, + actions: 'Center camera on selection', + }, }, }) diff --git a/yarn.lock b/yarn.lock index 9ecb0fdf45..882680014c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2075,10 +2075,10 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@kittycad/lib@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-2.0.1.tgz#d3f1c80d9903452b0b9df378c72ed1e83b19a73d" - integrity sha512-VYunezWS+cNZbdKfVkB3zg2YbDCQEb/AjzER85+yyDAlTU5PL4paQDpNlEI6icSglDGRUIR4Er/bRFj68r3UQg== +"@kittycad/lib@2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@kittycad/lib/-/lib-2.0.7.tgz#63e9c81fc7705c9d0c5fab5939e5d839ec6f393b" + integrity sha512-P26rRZ0KF8C3zhEG2beLlkTJhTPtJF6Nn1wg7w1MxXNvK9RZF6P7DcXqdIh7nJGQt72+JrXoPmApB8Z/R1gQRg== dependencies: openapi-types "^12.0.0" ts-node "^10.9.1"