A shit ton of stuff
This commit is contained in:
@ -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,24 +250,49 @@ 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
|
||||
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()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// drag line handle
|
||||
await page.waitForTimeout(100)
|
||||
|
||||
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()
|
||||
@ -294,8 +303,9 @@ test.describe('Sketch tests', () => {
|
||||
await expect(page.locator('.cm-content')).not.toHaveText(prevContent)
|
||||
prevContent = await page.locator('.cm-content').innerText()
|
||||
}
|
||||
})
|
||||
|
||||
// drag tangentialArcTo handle
|
||||
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()
|
||||
@ -305,6 +315,7 @@ test.describe('Sketch tests', () => {
|
||||
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()
|
||||
|
@ -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),
|
||||
}
|
||||
|
||||
return { toSU, click00r }
|
||||
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 { 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),
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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<void> {
|
||||
private lastFramePaneWidth: number = 0
|
||||
|
||||
async centerModelRelativeToPanes(args?: { zoomObjectId?: string, zoomToFit?: boolean, resetLastPaneWidth?: boolean }): Promise<void> {
|
||||
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,25 +973,12 @@ export class CameraControls {
|
||||
// camera coordinates after a zoom-to-fit... So this is much easier, and
|
||||
// maps better to screen coordinates.
|
||||
|
||||
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
|
||||
},
|
||||
},
|
||||
const requests: Models['ModelingCmdReq_type'][] = [
|
||||
{
|
||||
cmd: {
|
||||
type: 'camera_drag_start',
|
||||
interaction: 'pan',
|
||||
window: { x: 0, y: 0 },
|
||||
window: { x: goPx < 0 ? -goPx : 0, y: 0 },
|
||||
},
|
||||
cmd_id: uuidv4(),
|
||||
},
|
||||
@ -991,13 +987,31 @@ export class CameraControls {
|
||||
type: 'camera_drag_move',
|
||||
interaction: 'pan',
|
||||
window: {
|
||||
x: goRightPx,
|
||||
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,
|
||||
})
|
||||
// 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,
|
||||
},
|
||||
},
|
||||
|
@ -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 (
|
||||
<div className={`absolute w-0 h-0`}>
|
||||
@ -449,7 +453,7 @@ const SegmentMenu = ({
|
||||
const { send } = useModelingContext()
|
||||
const dependentSourceRanges = findUsesOfTagInPipe(kclManager.ast, pathToNode)
|
||||
return (
|
||||
<Popover style={{ style }} className="relative">
|
||||
<Popover style={style} className="relative">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
|
22
src/clientSideScene/constants.ts
Normal file
22
src/clientSideScene/constants.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// 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
|
||||
|
||||
export const RAYCASTABLE_PLANE = 'raycastable-plane'
|
||||
|
||||
// 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 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
|
@ -2,10 +2,7 @@ import { compareVec2Epsilon2 } from 'lang/std/sketch'
|
||||
import {
|
||||
GridHelper,
|
||||
LineBasicMaterial,
|
||||
OrthographicCamera,
|
||||
PerspectiveCamera,
|
||||
Group,
|
||||
Mesh,
|
||||
Quaternion,
|
||||
Vector3,
|
||||
} from 'three'
|
||||
@ -28,15 +25,11 @@ export function createGridHelper({
|
||||
gridHelper.rotation.x = Math.PI / 2
|
||||
return gridHelper
|
||||
}
|
||||
const fudgeFactor = 72.66985970437086
|
||||
|
||||
export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
|
||||
(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)
|
||||
|
16
src/clientSideScene/scale.ts
Normal file
16
src/clientSideScene/scale.ts
Normal file
@ -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)
|
@ -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<typeof useModelingContext>['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<Object3DEventMap>
|
||||
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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<HTMLUListElement>(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 (
|
||||
<Resizable
|
||||
data-testid="modeling-sidebar"
|
||||
className={`group flex-1 flex flex-col z-10 my-2 pr-1 ${paneOpacity} ${pointerEventsCssClass}`}
|
||||
defaultSize={{
|
||||
width: '550px',
|
||||
@ -222,6 +245,7 @@ export const ModelingSidebar = forwardRef<
|
||||
>
|
||||
<ul
|
||||
id="pane-buttons-section"
|
||||
data-testid="pane-buttons-section"
|
||||
className={
|
||||
'w-fit p-2 flex flex-col gap-2 ' +
|
||||
(context.store?.openPanes.length >= 1 ? 'pr-0.5' : '')
|
||||
@ -267,7 +291,7 @@ export const ModelingSidebar = forwardRef<
|
||||
<ul
|
||||
id="pane-section"
|
||||
data-testid="pane-section"
|
||||
ref={ref}
|
||||
ref={innerRef}
|
||||
className={
|
||||
'ml-[-1px] col-start-2 col-span-1 flex flex-col gap-2 ' +
|
||||
(context.store?.openPanes.length >= 1
|
||||
|
@ -285,7 +285,7 @@ export class KclManager {
|
||||
)
|
||||
}
|
||||
|
||||
await sceneInfra.camControls.centerModelRelativeToPanes(zoomObjectId)
|
||||
await sceneInfra.camControls.centerModelRelativeToPanes({ zoomToFit: true, zoomObjectId })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -539,8 +539,8 @@ export const modelingMachine = setup({
|
||||
sketchPlaneId: '',
|
||||
}),
|
||||
'reset camera position': () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
engineCommandManager.sendSceneCommand({
|
||||
;(async () => {
|
||||
await engineCommandManager.sendSceneCommand({
|
||||
type: 'modeling_cmd_req',
|
||||
cmd_id: uuidv4(),
|
||||
cmd: {
|
||||
@ -550,6 +550,8 @@ export const modelingMachine = setup({
|
||||
up: { x: 0, y: 0, z: 1 },
|
||||
},
|
||||
})
|
||||
await sceneInfra.camControls.centerModelRelativeToPanes({ resetLastPaneWidth: true })
|
||||
})().catch(reportRejection)
|
||||
},
|
||||
'set new sketch metadata': assign(({ event }) => {
|
||||
if (
|
||||
|
Reference in New Issue
Block a user