diff --git a/e2e/playwright/sketch-tests.spec.ts b/e2e/playwright/sketch-tests.spec.ts index 3d66a92c9..ed9d4dd75 100644 --- a/e2e/playwright/sketch-tests.spec.ts +++ b/e2e/playwright/sketch-tests.spec.ts @@ -202,35 +202,19 @@ test.describe('Sketch tests', () => { }) const u = await getUtils(page) - await page.setViewportSize({ width: 1200, height: 500 }) + const viewport = { width: 1200, height: 500 } + await page.setViewportSize(viewport) await u.waitForAuthSkipAppStart() await expect( page.getByRole('button', { name: 'Start Sketch' }) ).not.toBeDisabled() - await page.waitForTimeout(100) - await u.openAndClearDebugPanel() - await u.sendCustomCmd({ - type: 'modeling_cmd_req', - cmd_id: uuidv4(), - cmd: { - type: 'default_camera_look_at', - vantage: { x: 0, y: -1250, z: 580 }, - center: { x: 0, y: 0, z: 0 }, - up: { x: 0, y: 0, z: 1 }, - }, - }) - await page.waitForTimeout(100) - await u.sendCustomCmd({ - type: 'modeling_cmd_req', - cmd_id: uuidv4(), - cmd: { - type: 'default_camera_get_settings', - }, - }) - await page.waitForTimeout(100) - await u.closeDebugPanel() + const center = { + x: viewport.width / 2, + y: viewport.height / 2, + } + const modelAreaSize = await u.getModelViewAreaSize() // If we have the code pane open, we should see the code. if (openPanes.includes('code')) { @@ -244,7 +228,7 @@ test.describe('Sketch tests', () => { await expect(u.codeLocator).not.toBeVisible() } - const startPX = [665, 458] + const startPX = [center.x + 65, 458] const dragPX = 30 let prevContent = '' @@ -255,7 +239,7 @@ test.describe('Sketch tests', () => { // Wait for the render. await page.waitForTimeout(1000) // Select the sketch - await page.mouse.click(700, 370) + await page.mouse.click(center.x + 100, 370) } await expect( page.getByRole('button', { name: 'Edit Sketch' }) @@ -266,45 +250,72 @@ test.describe('Sketch tests', () => { prevContent = await page.locator('.cm-content').innerText() } + await page.waitForTimeout(1000) + await u.openAndClearDebugPanel() + await u.sendCustomCmd({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'default_camera_look_at', + vantage: { x: 0, y: -1250, z: 580 - modelAreaSize.w }, + center: { x: 0, y: 0, z: 0 }, + up: { x: 0, y: 0, z: 1 }, + }, + }) + await page.waitForTimeout(100) + await u.sendCustomCmd({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'default_camera_get_settings', + }, + }) + await page.waitForTimeout(1000) + await u.closeDebugPanel() + + const step5 = { steps: 5 } await expect(page.getByTestId('segment-overlay')).toHaveCount(2) - // drag startProfieAt handle - await page.mouse.move(startPX[0], startPX[1]) - await page.mouse.down() - await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5) - await page.mouse.up() + test.step('drag startProfileAt handle', async () => { + await page.mouse.move(startPX[0], startPX[1]) + await page.mouse.down() + await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5) + await page.mouse.up() + if (openPanes.includes('code')) { + await expect(page.locator('.cm-content')).not.toHaveText(prevContent) + prevContent = await page.locator('.cm-content').innerText() + } + }) - if (openPanes.includes('code')) { - await expect(page.locator('.cm-content')).not.toHaveText(prevContent) - prevContent = await page.locator('.cm-content').innerText() - } - // drag line handle await page.waitForTimeout(100) - const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]') - await page.mouse.move(lineEnd.x - 5, lineEnd.y) - await page.mouse.down() - await page.mouse.move(lineEnd.x + dragPX, lineEnd.y - dragPX, step5) - await page.mouse.up() - await page.waitForTimeout(100) - if (openPanes.includes('code')) { - await expect(page.locator('.cm-content')).not.toHaveText(prevContent) - prevContent = await page.locator('.cm-content').innerText() - } + test.step('drag line handle', async () => { + const lineEnd = await u.getBoundingBox('[data-overlay-index="0"]') + await page.mouse.move(lineEnd.x - 5, lineEnd.y) + await page.mouse.down() + await page.mouse.move(lineEnd.x + dragPX, lineEnd.y - dragPX, step5) + await page.mouse.up() + await page.waitForTimeout(100) + if (openPanes.includes('code')) { + await expect(page.locator('.cm-content')).not.toHaveText(prevContent) + prevContent = await page.locator('.cm-content').innerText() + } + }) - // drag tangentialArcTo handle - const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]') - await page.mouse.move(tangentEnd.x, tangentEnd.y - 5) - await page.mouse.down() - await page.mouse.move(tangentEnd.x + dragPX, tangentEnd.y - dragPX, step5) - await page.mouse.up() - await page.waitForTimeout(100) - if (openPanes.includes('code')) { - await expect(page.locator('.cm-content')).not.toHaveText(prevContent) - } + test.step('drag tangentialArcTo handle', async () => { + const tangentEnd = await u.getBoundingBox('[data-overlay-index="1"]') + await page.mouse.move(tangentEnd.x, tangentEnd.y - 5) + await page.mouse.down() + await page.mouse.move(tangentEnd.x + dragPX, tangentEnd.y - dragPX, step5) + await page.mouse.up() + await page.waitForTimeout(100) + if (openPanes.includes('code')) { + await expect(page.locator('.cm-content')).not.toHaveText(prevContent) + } + }) // Open the code pane await u.openKclCodePanel() @@ -580,7 +591,7 @@ test.describe('Sketch tests', () => { }) await page.waitForTimeout(100) - const startPX = [665, 458] + const center = await u.getCenterOfModelViewArea() const dragPX = 30 @@ -596,7 +607,7 @@ test.describe('Sketch tests', () => { await expect(page.getByTestId('segment-overlay')).toHaveCount(2) - // drag startProfieAt handle + // drag startProfileAt handle await page.mouse.move(startPX[0], startPX[1]) await page.mouse.down() await page.mouse.move(startPX[0] + dragPX, startPX[1] - dragPX, step5) @@ -639,14 +650,14 @@ test.describe('Sketch tests', () => { test('Can add multiple sketches', async ({ page }) => { test.skip(process.platform === 'darwin', 'Can add multiple sketches') const u = await getUtils(page) + const viewportSize = { width: 1200, height: 500 } await page.setViewportSize(viewportSize) + await u.waitForAuthSkipAppStart() await u.openDebugPanel() - const center = { x: viewportSize.width / 2, y: viewportSize.height / 2 } - const { toSU, click00r } = getMovementUtils({ center, page }) await expect( page.getByRole('button', { name: 'Start Sketch' }) @@ -662,28 +673,31 @@ test.describe('Sketch tests', () => { 200 ) - let codeStr = "sketch001 = startSketchOn('XY')" + const center = await u.getCenterOfModelViewArea() - await page.mouse.click(center.x, viewportSize.height * 0.55) + let codeStr = "const sketch001 = startSketchOn('XY')" + + await page.mouse.click(center.x - 50, viewportSize.height * 0.55) await expect(u.codeLocator).toHaveText(codeStr) await u.closeDebugPanel() await page.waitForTimeout(500) // TODO detect animation ending, or disable animation - await click00r(0, 0) - codeStr += ` |> startProfileAt(${toSU([0, 0])}, %)` + const { click00r } = await getMovementUtils({ center, page }) + + let coord = await click00r(0, 0) + codeStr += ` |> startProfileAt(${coord.kcl}, %)` await expect(u.codeLocator).toHaveText(codeStr) - await click00r(50, 0) - await page.waitForTimeout(100) - codeStr += ` |> line(${toSU([50, 0])}, %)` + coord = await click00r(50, 0) + codeStr += ` |> line(${coord.kcl}, %)` await expect(u.codeLocator).toHaveText(codeStr) - await click00r(0, 50) - codeStr += ` |> line(${toSU([0, 50])}, %)` + coord = await click00r(0, 50) + codeStr += ` |> line(${coord.kcl}, %)` await expect(u.codeLocator).toHaveText(codeStr) - await click00r(-50, 0) - codeStr += ` |> line(${toSU([-50, 0])}, %)` + coord = await click00r(-50, 0) + codeStr += ` |> line(${coord.kcl}, %)` await expect(u.codeLocator).toHaveText(codeStr) // exit the sketch, reset relative clicker @@ -699,26 +713,26 @@ test.describe('Sketch tests', () => { // when exiting the sketch above the camera is still looking down at XY, // so selecting the plane again is a bit easier. - await page.mouse.click(center.x + 200, center.y + 100) + await page.mouse.click(center.x - 100, center.y + 50) await page.waitForTimeout(600) // TODO detect animation ending, or disable animation codeStr += "sketch002 = startSketchOn('XY')" await expect(u.codeLocator).toHaveText(codeStr) await u.closeDebugPanel() - await click00r(30, 0) - codeStr += ` |> startProfileAt([2.03, 0], %)` + coord = await click00r(30, 0) + codeStr += ` |> startProfileAt(${coord.kcl}, %)` await expect(u.codeLocator).toHaveText(codeStr) - await click00r(30, 0) - codeStr += ` |> line([2.04, 0], %)` + coord = await click00r(30, 0) + codeStr += ` |> line(${coord.kcl}, %)` await expect(u.codeLocator).toHaveText(codeStr) - await click00r(0, 30) - codeStr += ` |> line([0, -2.03], %)` + coord = await click00r(0, 30) + codeStr += ` |> line(${coord.kcl}, %)` await expect(u.codeLocator).toHaveText(codeStr) - await click00r(-30, 0) - codeStr += ` |> line([-2.04, 0], %)` + coord = await click00r(-30, 0) + codeStr += ` |> line(${coord.kcl}, %)` await expect(u.codeLocator).toHaveText(codeStr) await click00r(undefined, undefined) @@ -762,20 +776,21 @@ test.describe('Sketch tests', () => { await u.updateCamPosition(camPos) await u.closeDebugPanel() + const center = await u.getCenterOfModelViewArea() await page.mouse.move(0, 0) // select a plane - await page.mouse.move(700, 200, { steps: 10 }) - await page.mouse.click(700, 200, { delay: 200 }) + await page.mouse.move(center.x + 100, 200, { steps: 10 }) + await page.mouse.click(center.x + 100, 200, { delay: 200 }) await expect(page.locator('.cm-content')).toHaveText( `sketch001 = startSketchOn('-XZ')` ) let prevContent = await page.locator('.cm-content').innerText() - const pointA = [700, 200] - const pointB = [900, 200] - const pointC = [900, 400] + const pointA = [center.x + 100, 200] + const pointB = [center.x + 300, 200] + const pointC = [center.x + 300, 400] // draw three lines await page.waitForTimeout(500) @@ -912,7 +927,9 @@ extrude001 = extrude(5, sketch001) await page.getByRole('button', { name: 'Start Sketch' }).click() - await page.mouse.click(622, 355) + const center = await u.getCenterOfModelViewArea() + + await page.mouse.click(center.x + 22, 355) await page.waitForTimeout(800) await page.getByText(`END')`).click() diff --git a/e2e/playwright/test-utils.ts b/e2e/playwright/test-utils.ts index 7cd2e9844..01bb2f2db 100644 --- a/e2e/playwright/test-utils.ts +++ b/e2e/playwright/test-utils.ts @@ -8,6 +8,18 @@ import { Locator, test, } from '@playwright/test' +import { + OrthographicCamera, + Mesh, + Scene, + Raycaster, + PlaneGeometry, + MeshBasicMaterial, + DoubleSide, + Vector2, + Vector3, +} from 'three' +import { RAYCASTABLE_PLANE, INTERSECTION_PLANE_LAYER } from 'clientSideScene/constants' import { EngineCommand } from 'lang/std/artifactGraph' import fsp from 'fs/promises' import fsSync from 'fs' @@ -238,55 +250,145 @@ export const circleMove = async ( } } -export const getMovementUtils = (opts: any) => { - // The way we truncate is kinda odd apparently, so we need this function - // "[k]itty[c]ad round" - const kcRound = (n: number) => Math.trunc(n * 100) / 100 +export function rollingRound(n: number, digitsAfterDecimal: number) { + const s = String(n).split('.') - // To translate between screen and engine ("[U]nit") coordinates - // NOTE: these pretty much can't be perfect because of screen scaling. - // Handle on a case-by-case. - const toU = (x: number, y: number) => [ - kcRound(x * 0.0678), - kcRound(-y * 0.0678), // Y is inverted in our coordinate system - ] + // There are no decimals, just return the number. + if (s.length === 1) return n - // Turn the array into a string with specific formatting - const fromUToString = (xy: number[]) => `[${xy[0]}, ${xy[1]}]` + // Find the closest 9. We don't care about anything beyond that. + const nineIndex = s[1].indexOf('9') - // Combine because used often - const toSU = (xy: number[]) => fromUToString(toU(xy[0], xy[1])) + const fractStr = nineIndex > 0 ? s[1].slice(0, nineIndex + 1) : s[1] + + let fract = Number(fractStr) / (10 ** fractStr.length) + + for (let i = fractStr.length - 1; i >= 0; i -= 1) { + if (i === digitsAfterDecimal) break + fract = Math.round(fract*(10**i)) / (10 **i) + } + + return (Number(s[0]) + fract).toFixed(digitsAfterDecimal) +} + + +export const getMovementUtils = async (opts: any) => { + const sceneInfra = await opts.page.evaluate(() => window.sceneInfra) + + // Various data for raycasting into the scene to get our XY. + const hundredM = 100_0000 + const planeGeometry = new PlaneGeometry(hundredM, hundredM) + const planeMaterial = new MeshBasicMaterial({ + color: 0xff0000, + side: DoubleSide, + transparent: true, + opacity: 0.5, + }) + const scene = new Scene() + const intersectionPlane = new Mesh(planeGeometry, planeMaterial) + intersectionPlane.userData = { type: RAYCASTABLE_PLANE } + intersectionPlane.name = RAYCASTABLE_PLANE + intersectionPlane.layers.set(INTERSECTION_PLANE_LAYER) + scene.add(intersectionPlane) + const planeRaycaster = new Raycaster() + planeRaycaster.far = Infinity + planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER) + + const kcRound = (n: number) => Math.round(n * 100) / 100 // Make it easier to click around from center ("click [from] zero zero") const click00 = (x: number, y: number) => - opts.page.mouse.click(opts.center.x + x, opts.center.y + y, { delay: 100 }) + opts.page.mouse.click(x, y, { delay: 100 }) // Relative clicker, must keep state let last = { x: 0, y: 0 } + let lastScreenSpace = { x: 0, y: 0 } + const click00r = async (x?: number, y?: number) => { // reset relative coordinates when anything is undefined if (x === undefined || y === undefined) { - last.x = 0 - last.y = 0 - return + last = { x: 0, y: 0 } + lastScreenSpace = { x: 0, y: 0 } + return { + nextXY: [0, 0], + kcl: `[0, 0]`, + } } + const absX = opts.center.x + x + const absY = opts.center.y + y + + const nextX = last.x + x + const nextY = last.y + y + + const targetX = opts.center.x + nextX + const targetY = opts.center.y + -nextY + + // Use the current camera specification + const camera = await opts.page.evaluate(() => { + window.sceneInfra.camControls.onCameraChange(true) + return window.sceneInfra.camControls.camera + }) + + const windowWH = await opts.page.evaluate(() => ({ w: window.innerWidth, h: window.innerHeight })) + + // I didn't write this math, it's copied from sceneInfra.ts, and I understand + // it's just normalizing the point, but why *-2 ± 1 I have no idea. + const mouseVector = new Vector2( + (targetX / windowWH.w) * 2 - 1, + -(targetY / windowWH.h) * 2 + 1, + ) + planeRaycaster.setFromCamera(mouseVector, camera) + const intersections = planeRaycaster.intersectObjects(scene.children, true) + + const planePosition = intersections[0].object.position + const inversePlaneQuaternion = intersections[0].object.quaternion + .clone() + .invert() + let transformedPoint = intersections[0].point.clone() + if (transformedPoint) { + transformedPoint.applyQuaternion(inversePlaneQuaternion) + } + const twoD = new Vector2( + // I think the intersection plane doesn't get scale when nearly everything else does, maybe that should change + transformedPoint.x / sceneInfra._baseUnitMultiplier, + transformedPoint.y / sceneInfra._baseUnitMultiplier + ) // z should be 0 + const planePositionCorrected = new Vector3( + ...planePosition + ).applyQuaternion(inversePlaneQuaternion) + twoD.sub(new Vector2(...planePositionCorrected)) + await circleMove( opts.page, - opts.center.x + last.x + x, - opts.center.y + last.y + y, + targetX, + targetY, 10, 10 ) - await click00(last.x + x, last.y + y) + await click00(targetX, targetY) + last.x += x last.y += y - // Returns the new absolute coordinate if you need it. - return [last.x, last.y] + const relativeScreenSpace = { + x: twoD.x - lastScreenSpace.x, + y: -(twoD.y - lastScreenSpace.y), + } + + lastScreenSpace.x = kcRound(twoD.x) + lastScreenSpace.y = kcRound(twoD.y) + + + // Returns the new absolute coordinate and the screen space coordinate if you need it. + return { + nextXY: [last.x, last.y], + kcl: `[${kcRound(relativeScreenSpace.x)}, ${-kcRound(relativeScreenSpace.y)}]`, + } + } - return { toSU, click00r } + return { click00r } } async function waitForAuthAndLsp(page: Page) { @@ -337,16 +439,28 @@ export async function getUtils(page: Page, test_?: typeof test) { browserType !== 'chromium' ? null : await page.context().newCDPSession(page) const util = { + async getModelViewAreaSize() { + const windowInnerWidth = await page.evaluate(() => window.innerWidth) + const windowInnerHeight = await page.evaluate(() => window.innerHeight) + + const sidebar = page.getByTestId('modeling-sidebar') + const bb = await sidebar.boundingBox() + return { + w: windowInnerWidth - (bb?.width ?? 0), + h: windowInnerHeight - (bb?.height ?? 0) + } + }, async getCenterOfModelViewArea() { const windowInnerWidth = await page.evaluate(() => window.innerWidth) const windowInnerHeight = await page.evaluate(() => window.innerHeight) - const panes = page.getByTestId('pane-section') - const bb = await panes.boundingBox() - const goRightPx = bb.width > 0 ? (windowInnerWidth - bb.width) / 2 : 0 + const sidebar = page.getByTestId('modeling-sidebar') + const bb = await sidebar.boundingBox() + const goRightPx = (bb?.width ?? 0 ) / 2 + const borderWidthsCombined = 2 return { - x: windowInnerWidth / 2 + goRightPx, - y: windowInnerHeight / 2, + x: Math.round(windowInnerWidth / 2 + goRightPx) - borderWidthsCombined, + y: Math.round(windowInnerHeight / 2), } }, waitForAuthSkipAppStart: () => waitForAuthAndLsp(page), diff --git a/e2e/playwright/testing-constraints.spec.ts b/e2e/playwright/testing-constraints.spec.ts index df4b20f3a..d80db6a24 100644 --- a/e2e/playwright/testing-constraints.spec.ts +++ b/e2e/playwright/testing-constraints.spec.ts @@ -43,10 +43,12 @@ test.describe('Testing constraints', () => { await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.waitForTimeout(500) // wait for animation - const startXPx = 500 + const center = await u.getCenterOfModelViewArea() + + const startXPx = center.x - 100 await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10) await page.keyboard.down('Shift') - await page.mouse.click(834, 244) + await page.mouse.click(center.x + 234, 244) await page.keyboard.up('Shift') await page diff --git a/e2e/playwright/various.spec.ts b/e2e/playwright/various.spec.ts index 90604a18d..1c32d6416 100644 --- a/e2e/playwright/various.spec.ts +++ b/e2e/playwright/various.spec.ts @@ -500,8 +500,14 @@ test('Sketch on face', async ({ page }) => { const center = await u.getCenterOfModelViewArea() - await page.mouse.move(center.x, 180) - await page.mouse.click(center.x, 180) + // This basically waits for sketch mode to be ready. + await u.doAndWaitForCmd( + async () => page.mouse.click(center.x, 180), + 'default_camera_get_settings', + true + ) + + await page.waitForTimeout(300) const firstClickPosition = [612, 238] const secondClickPosition = [661, 242] diff --git a/src/clientSideScene/CameraControls.ts b/src/clientSideScene/CameraControls.ts index 30c0160a6..b70f8aa40 100644 --- a/src/clientSideScene/CameraControls.ts +++ b/src/clientSideScene/CameraControls.ts @@ -1,3 +1,4 @@ +import { Models } from '@kittycad/lib' import { MutableRefObject } from 'react' import { cameraMouseDragGuards, MouseGuard } from 'lib/cameraControls' import { @@ -922,7 +923,7 @@ export class CameraControls { }, }) - await this.centerModelRelativeToPanes() + await this.centerModelRelativeToPanes({ zoomToFit: true, resetLastPaneWidth: true }) this.cameraDragStartXY = new Vector2() this.cameraDragStartXY.x = 0 @@ -951,12 +952,20 @@ export class CameraControls { }) } - async centerModelRelativeToPanes(zoomObjectId?: string): Promise { + private lastFramePaneWidth: number = 0 + + async centerModelRelativeToPanes(args?: { zoomObjectId?: string, zoomToFit?: boolean, resetLastPaneWidth?: boolean }): Promise { const panes = this.modelingSidebarRef?.current if (!panes) return const panesWidth = panes.offsetWidth + panes.offsetLeft - const goRightPx = panesWidth > 0 ? panesWidth / 2 : 0 + + if (args?.resetLastPaneWidth) { + this.lastFramePaneWidth = 0 + } + + const goPx = ((panesWidth - this.lastFramePaneWidth) / 2) / window.devicePixelRatio + this.lastFramePaneWidth = panesWidth // Originally I had tried to use the default_camera_look_at endpoint and // some quaternion math to move the camera right, but it ended up being @@ -964,40 +973,45 @@ export class CameraControls { // camera coordinates after a zoom-to-fit... So this is much easier, and // maps better to screen coordinates. + const requests: Models['ModelingCmdReq_type'][] = [ + { + cmd: { + type: 'camera_drag_start', + interaction: 'pan', + window: { x: goPx < 0 ? -goPx : 0, y: 0 }, + }, + cmd_id: uuidv4(), + }, + { + cmd: { + type: 'camera_drag_move', + interaction: 'pan', + window: { + x: goPx < 0 ? 0 : goPx, + y: 0, + }, + }, + cmd_id: uuidv4(), + }, + ] + + if (args?.zoomToFit) { + requests.unshift({ + cmd: { + type: 'zoom_to_fit', + object_ids: args?.zoomObjectId ? [args?.zoomObjectId] : [], // leave empty to zoom to all objects + padding: 0.2, // padding around the objects + }, + cmd_id: uuidv4(), + }) + } + await this.engineCommandManager .sendSceneCommand({ type: 'modeling_cmd_batch_req', batch_id: uuidv4(), responses: true, - requests: [ - { - cmd_id: uuidv4(), - cmd: { - type: 'zoom_to_fit', - object_ids: zoomObjectId ? [zoomObjectId] : [], // leave empty to zoom to all objects - padding: 0.2, // padding around the objects - }, - }, - { - cmd: { - type: 'camera_drag_start', - interaction: 'pan', - window: { x: 0, y: 0 }, - }, - cmd_id: uuidv4(), - }, - { - cmd: { - type: 'camera_drag_move', - interaction: 'pan', - window: { - x: goRightPx, - y: 0, - }, - }, - cmd_id: uuidv4(), - }, - ], + requests, }) // engineCommandManager can't subscribe to batch responses so we'll send // this one off by its lonesome after. @@ -1008,7 +1022,7 @@ export class CameraControls { type: 'camera_drag_end', interaction: 'pan', window: { - x: goRightPx, + x: goPx < 0 ? 0 : goPx, y: 0, }, }, diff --git a/src/clientSideScene/ClientSideSceneComp.tsx b/src/clientSideScene/ClientSideSceneComp.tsx index 7fe4f3a3a..b3fae500b 100644 --- a/src/clientSideScene/ClientSideSceneComp.tsx +++ b/src/clientSideScene/ClientSideSceneComp.tsx @@ -234,7 +234,11 @@ const Overlay = ({ ) // Line labels will cover the constraints overlay if this is not used. - const zIndex = 10 + // For each line label, ThreeJS increments each CSS2DObject z-index as they + // are added. I have looked into overriding renderOrder and depthTest and + // while renderOrder is set, ThreeJS still sets z-index on these 2D objects. + // It is easier to set this to a large number, such as a billion. + const zIndex = 1000000000 return (
@@ -449,7 +453,7 @@ const SegmentMenu = ({ const { send } = useModelingContext() const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode) return ( - + {({ open }) => ( <> - (0.55 * fudgeFactor) / cam.zoom / window.innerHeight -export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) => - (group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) / - 4000 / - window.innerHeight +// Re-export scale.ts +export * from './scale' + export function isQuaternionVertical(q: Quaternion) { const v = new Vector3(0, 0, 1).applyQuaternion(q) diff --git a/src/clientSideScene/scale.ts b/src/clientSideScene/scale.ts new file mode 100644 index 000000000..bf4833d57 --- /dev/null +++ b/src/clientSideScene/scale.ts @@ -0,0 +1,16 @@ +import { + OrthographicCamera, + PerspectiveCamera, + Group, + Mesh, +} from 'three' + +export const fudgeFactor = 72.66985970437086 + +export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera, innerHeight?: number) => + (0.55 * fudgeFactor) / cam.zoom / (innerHeight ?? window.innerHeight) + +export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh, innerHeight?: number) => + (group.position.distanceTo(cam.position) * cam.fov * fudgeFactor) / + 4000 / + (innerHeight ?? window.innerHeight) diff --git a/src/clientSideScene/sceneInfra.ts b/src/clientSideScene/sceneInfra.ts index f2767f3b2..4ca417e36 100644 --- a/src/clientSideScene/sceneInfra.ts +++ b/src/clientSideScene/sceneInfra.ts @@ -30,31 +30,11 @@ import { MouseState, SegmentOverlayPayload } from 'machines/modelingMachine' import { getAngle, throttle } from 'lib/utils' import { Themes } from 'lib/theme' import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer' +import * as constants from './constants' type SendType = ReturnType['send'] -// 63.5 is definitely a bit of a magic number, play with it until it looked right -// if it were 64, that would feel like it's something in the engine where a random -// power of 2 is used, but it's the 0.5 seems to make things look much more correct -export const ZOOM_MAGIC_NUMBER = 63.5 - -export const INTERSECTION_PLANE_LAYER = 1 -export const SKETCH_LAYER = 2 - -// redundant types so that it can be changed temporarily but CI will catch the wrong type -export const DEBUG_SHOW_INTERSECTION_PLANE: false = false -export const DEBUG_SHOW_BOTH_SCENES: false = false - -export const RAYCASTABLE_PLANE = 'raycastable-plane' - -export const X_AXIS = 'xAxis' -export const Y_AXIS = 'yAxis' -export const AXIS_GROUP = 'axisGroup' -export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments' -export const ARROWHEAD = 'arrowhead' -export const SEGMENT_LENGTH_LABEL = 'segment-length-label' -export const SEGMENT_LENGTH_LABEL_TEXT = 'segment-length-label-text' -export const SEGMENT_LENGTH_LABEL_OFFSET_PX = 30 +export * from './constants' export interface OnMouseEnterLeaveArgs { selected: Object3D @@ -279,14 +259,14 @@ export class SceneInfra { engineCommandManager ) this.camControls.subscribeToCamChange(() => this.onCameraChange()) - this.camControls.camera.layers.enable(SKETCH_LAYER) - if (DEBUG_SHOW_INTERSECTION_PLANE) - this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER) + this.camControls.camera.layers.enable(constants.SKETCH_LAYER) + if (constants.DEBUG_SHOW_INTERSECTION_PLANE) + this.camControls.camera.layers.enable(constants.INTERSECTION_PLANE_LAYER) // RAYCASTERS - this.raycaster.layers.enable(SKETCH_LAYER) + this.raycaster.layers.enable(constants.SKETCH_LAYER) this.raycaster.layers.disable(0) - this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER) + this.planeRaycaster.layers.enable(constants.INTERSECTION_PLANE_LAYER) // GRID const size = 100 @@ -321,7 +301,7 @@ export class SceneInfra { this.camControls.target ) const axisGroup = this.scene - .getObjectByName(AXIS_GROUP) + .getObjectByName(constants.AXIS_GROUP) ?.getObjectByName('gridHelper') axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale) } @@ -362,7 +342,7 @@ export class SceneInfra { true ) const recastablePlaneIntersect = planeIntersects.find( - (intersect) => intersect.object.name === RAYCASTABLE_PLANE + (intersect) => intersect.object.name === constants.RAYCASTABLE_PLANE ) if (!planeIntersects.length) return null if (!recastablePlaneIntersect) return { intersection: planeIntersects[0] } @@ -622,11 +602,11 @@ export class SceneInfra { } updateOtherSelectionColors = (otherSelections: Axis[]) => { const axisGroup = this.scene.children.find( - ({ userData }) => userData?.type === AXIS_GROUP + ({ userData }) => userData?.type === constants.AXIS_GROUP ) const axisMap: { [key: string]: Axis } = { - [X_AXIS]: 'x-axis', - [Y_AXIS]: 'y-axis', + [constants.X_AXIS]: 'x-axis', + [constants.Y_AXIS]: 'y-axis', } axisGroup?.children.forEach((_mesh) => { const mesh = _mesh as Mesh diff --git a/src/components/EngineStream.tsx b/src/components/EngineStream.tsx index 45ce64265..457127ce7 100644 --- a/src/components/EngineStream.tsx +++ b/src/components/EngineStream.tsx @@ -252,7 +252,7 @@ export const EngineStream = () => { if (modelingMachineState.matches('Sketch')) return if (modelingMachineState.matches({ idle: 'showPlanes' })) return - if (btnName(e).left) { + if (btnName(e.nativeEvent).left) { // eslint-disable-next-line @typescript-eslint/no-floating-promises sendSelectEventToEngine(e, engineStreamState.context.videoRef.current) } diff --git a/src/components/ModelingMachineProvider.tsx b/src/components/ModelingMachineProvider.tsx index c622098b4..bea656000 100644 --- a/src/components/ModelingMachineProvider.tsx +++ b/src/components/ModelingMachineProvider.tsx @@ -644,6 +644,7 @@ export const ModelingMachineProvider = ({ engineCommandManager, input.faceId ) + await sceneInfra.camControls.centerModelRelativeToPanes({ resetLastPaneWidth: true }) sceneInfra.camControls.syncDirection = 'clientToEngine' return { sketchPathToNode: pathToNewSketchNode, @@ -664,6 +665,7 @@ export const ModelingMachineProvider = ({ engineCommandManager, input.planeId ) + await sceneInfra.camControls.centerModelRelativeToPanes({ resetLastPaneWidth: true }) return { sketchPathToNode: pathToNode, @@ -686,6 +688,7 @@ export const ModelingMachineProvider = ({ engineCommandManager, info?.sketchDetails?.faceId || '' ) + await sceneInfra.camControls.centerModelRelativeToPanes({ resetLastPaneWidth: true }) return { sketchPathToNode: sketchPathToNode || [], zAxis: info.sketchDetails.zAxis || null, diff --git a/src/components/ModelingSidebar/ModelingSidebar.tsx b/src/components/ModelingSidebar/ModelingSidebar.tsx index cddaab77a..fe7a05079 100644 --- a/src/components/ModelingSidebar/ModelingSidebar.tsx +++ b/src/components/ModelingSidebar/ModelingSidebar.tsx @@ -9,6 +9,9 @@ import { useContext, MutableRefObject, forwardRef, + // https://stackoverflow.com/a/77055468 Thank you. + useImperativeHandle, + useRef, } from 'react' import { useHotkeys } from 'react-hotkeys-hook' import { SidebarAction, SidebarType, sidebarPanes } from './ModelingPanes' @@ -41,21 +44,33 @@ function getPlatformString(): 'web' | 'desktop' { } export const ModelingSidebar = forwardRef< - HTMLUListElement | null, + HTMLUListElement, ModelingSidebarProps ->(function ModelingSidebar({ paneOpacity }, ref) { +>(function ModelingSidebar({ paneOpacity }, outerRef) { const machineManager = useContext(MachineManagerContext) const { commandBarSend } = useCommandsContext() const kclContext = useKclContext() const { settings } = useSettingsAuthContext() const onboardingStatus = settings.context.app.onboardingStatus - const { send, context } = useModelingContext() + const { send, state, context } = useModelingContext() const pointerEventsCssClass = onboardingStatus.current === 'camera' || context.store?.openPanes.length === 0 ? 'pointer-events-none ' : 'pointer-events-auto ' const showDebugPanel = settings.context.modeling.showDebugPanel + const innerRef = useRef(null) + + // forwardRef's type causes me to do this type narrowing. + useEffect(() => { + if (typeof outerRef === 'function') { + outerRef(innerRef.current) + } else { + if (outerRef) { + outerRef.current = innerRef.current + } + } + }, [innerRef.current]) const paneCallbackProps = useMemo( () => ({ @@ -178,19 +193,27 @@ export const ModelingSidebar = forwardRef< // If the panes are resized then center the model also useEffect(() => { - let width = ref.current.offsetWidth + if (!innerRef.current) return + let last = Date.now() - new ResizeObserver(() => { - if (width === ref.current.offsetWidth) return + const observer = new ResizeObserver(() => { if (Date.now() - last < REASONABLE_TIME_TO_REFRESH_STREAM_SIZE) return + if (!innerRef.current) return + last = Date.now() - width = ref.current.offsetWidth void sceneInfra.camControls.centerModelRelativeToPanes() - }).observe(ref.current) - }, []) + }) + + observer.observe(innerRef.current) + + return () => { + observer.disconnect() + } + }, [state, innerRef.current]) return (
    = 1 ? 'pr-0.5' : '') @@ -267,7 +291,7 @@ export const ModelingSidebar = forwardRef<
      = 1 diff --git a/src/lang/KclSingleton.ts b/src/lang/KclSingleton.ts index 53746d884..9027c13f7 100644 --- a/src/lang/KclSingleton.ts +++ b/src/lang/KclSingleton.ts @@ -285,7 +285,7 @@ export class KclManager { ) } - await sceneInfra.camControls.centerModelRelativeToPanes(zoomObjectId) + await sceneInfra.camControls.centerModelRelativeToPanes({ zoomToFit: true, zoomObjectId }) } } diff --git a/src/lib/singletons.ts b/src/lib/singletons.ts index a4d1e8d1b..b04050b7d 100644 --- a/src/lib/singletons.ts +++ b/src/lib/singletons.ts @@ -10,8 +10,14 @@ export const codeManager = new CodeManager() export const engineCommandManager = new EngineCommandManager() -// Accessible for tests mostly -// @ts-ignore +declare global { + interface Window { + tearDown: typeof engineCommandManager.tearDown, + sceneInfra: typeof sceneInfra, + } +} + +// Accessible for tests window.tearDown = engineCommandManager.tearDown // This needs to be after codeManager is created. @@ -21,7 +27,9 @@ engineCommandManager.kclManager = kclManager engineCommandManager.getAstCb = () => kclManager.ast export const sceneInfra = new SceneInfra(engineCommandManager) -engineCommandManager.camControlsCameraChange = sceneInfra.onCameraChange + +// Accessible for tests +window.sceneInfra = sceneInfra export const sceneEntitiesManager = new SceneEntities(engineCommandManager) diff --git a/src/machines/modelingMachine.ts b/src/machines/modelingMachine.ts index 156bec8b6..7485a5201 100644 --- a/src/machines/modelingMachine.ts +++ b/src/machines/modelingMachine.ts @@ -539,17 +539,19 @@ export const modelingMachine = setup({ sketchPlaneId: '', }), 'reset camera position': () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - engineCommandManager.sendSceneCommand({ - type: 'modeling_cmd_req', - cmd_id: uuidv4(), - cmd: { - type: 'default_camera_look_at', - center: { x: 0, y: 0, z: 0 }, - vantage: { x: 0, y: -1250, z: 580 }, - up: { x: 0, y: 0, z: 1 }, - }, - }) + ;(async () => { + await engineCommandManager.sendSceneCommand({ + type: 'modeling_cmd_req', + cmd_id: uuidv4(), + cmd: { + type: 'default_camera_look_at', + center: { x: 0, y: 0, z: 0 }, + vantage: { x: 0, y: -1250, z: 580 }, + up: { x: 0, y: 0, z: 1 }, + }, + }) + await sceneInfra.camControls.centerModelRelativeToPanes({ resetLastPaneWidth: true }) + })().catch(reportRejection) }, 'set new sketch metadata': assign(({ event }) => { if (