A shit ton of stuff

This commit is contained in:
49lf
2024-10-02 12:24:12 -04:00
parent 15b163bba8
commit 89309b6ccd
16 changed files with 427 additions and 222 deletions

View File

@ -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()

View File

@ -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),

View File

@ -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

View File

@ -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]

View File

@ -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,
},
},

View File

@ -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

View 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

View File

@ -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)

View 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)

View File

@ -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

View File

@ -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)
}

View File

@ -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,

View File

@ -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

View File

@ -285,7 +285,7 @@ export class KclManager {
)
}
await sceneInfra.camControls.centerModelRelativeToPanes(zoomObjectId)
await sceneInfra.camControls.centerModelRelativeToPanes({ zoomToFit: true, zoomObjectId })
}
}

View File

@ -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)

View File

@ -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 (