solve a couple of scene scale bugs (#1496)

* solve a couple of scene scale bugs

* Some cam fixes (#1520)

* rotate and zoom basics working

* intergrate mouse guards, and add pan

* implement orthographic camera again

* implement switch to perspective camera again

* migrate dollyzoom

* make pan robust for differnt FOV and orthographic cam

* tween to quaternion and default plane selection working with quirks

* fix pan

It the up and right was derived from the camera's up, which is a static [0,0,1] not the camera's current cameras real up, which aligns itself as best to [0,0,1] but is not that especially when looking straight up or down, and the pan felt very awkward in these vertical look sintuations

* fix raycastRing to use new camera

* fix tween to quaternion for camera lock situations

And get all playwright tests passing

* fix up CamToggle, even thought this component is not setup properly to use react properties from our scene class

* add animation to cameras back in

* first big clean up of sceneInfra

* move more cam stuff out of sceneInfra

* clean up mouse guard logic

* clean up camera change callbacks

* fix some sitations where animation to xy doesn't work great

* needs to take the target into consideration

* last bits of clean up

* more clean up

* make vitest happ

* fix up remaining interaction guards

* make scrolling less sensative for trackpads

* remove debug cube

* fix snapshot tests
This commit is contained in:
Kurt Hutten
2024-02-26 19:53:44 +11:00
committed by GitHub
parent f0c44d11b3
commit 0d6618b60a
15 changed files with 1090 additions and 1017 deletions

View File

@ -14,6 +14,12 @@ document.addEventListener('mousemove', (e) =>
)
*/
const commonPoints = {
startAt: '[26.38, -35.59]',
num1: 26.63,
num2: 53.01,
}
test.beforeEach(async ({ context, page }) => {
// wait for Vite preview server to be up
await waitOn({
@ -72,35 +78,34 @@ test('Basic sketch', async ({ page }) => {
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.74, -32.03]'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)`)
|> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num = 23.97
const num = 26.63
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num}], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num}], %)
|> line([-47.71, 0], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)
|> line([-${commonPoints.num2}, 0], %)`)
// deselect line tool
await page.getByRole('button', { name: 'Line' }).click()
@ -122,9 +127,9 @@ test('Basic sketch', async ({ page }) => {
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line({ to: [${num}, 0], tag: 'seg01' }, %)
|> line([0, ${num}], %)
|> startProfileAt(${commonPoints.startAt}, %)
|> line({ to: [${commonPoints.num1}, 0], tag: 'seg01' }, %)
|> line([0, ${commonPoints.num1}], %)
|> angledLine([180, segLen('seg01', %)], %)`)
})
@ -305,11 +310,9 @@ test('Can create sketches on all planes and their back sides', async ({
}
const codeTemplate = (
plane = 'XY',
rounded = false,
otherThing = '1'
plane = 'XY'
) => `const part001 = startSketchOn('${plane}')
|> startProfileAt([28.9${otherThing}, -39${rounded ? '' : '.01'}], %)`
|> startProfileAt([32.13, -43.34], %)`
await TestSinglePlane({
viewCmd: camPos,
expectedCode: codeTemplate('XY'),
@ -318,7 +321,7 @@ test('Can create sketches on all planes and their back sides', async ({
})
await TestSinglePlane({
viewCmd: camPos,
expectedCode: codeTemplate('YZ', true),
expectedCode: codeTemplate('YZ'),
clickCoords: { x: 700, y: 300 }, // green plane
})
await TestSinglePlane({
@ -329,7 +332,7 @@ test('Can create sketches on all planes and their back sides', async ({
const camCmdBackSide: [number, number, number] = [-100, -100, -100]
await TestSinglePlane({
viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XY', false, '3'),
expectedCode: codeTemplate('-XY'),
clickCoords: { x: 601, y: 118 }, // back of red plane
})
await TestSinglePlane({
@ -339,7 +342,7 @@ test('Can create sketches on all planes and their back sides', async ({
})
await TestSinglePlane({
viewCmd: camCmdBackSide,
expectedCode: codeTemplate('-XZ', true),
expectedCode: codeTemplate('-XZ'),
clickCoords: { x: 680, y: 427 }, // back of blue plane
})
})
@ -461,35 +464,32 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.74, -32.03]'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)`)
|> startProfileAt(${commonPoints.startAt}, %)`)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
const num = 23.97
const num2 = '47.71'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num}], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num}], %)
|> line([-${num2}, 0], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)
|> line([-${commonPoints.num2}, 0], %)`)
// deselect line tool
await page.getByRole('button', { name: 'Line' }).click()
@ -539,7 +539,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await emptySpaceClick()
// check the same selection again by putting cursor in code first then selecting axis
await page.getByText(` |> line([-${num2}, 0], %)`).click()
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
await page.keyboard.down('Shift')
await expect(absYButton).toBeDisabled()
await xAxisClick()
@ -550,7 +550,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
await emptySpaceClick()
// select segment in editor than another segment in scene and check there are two cursors
await page.getByText(` |> line([-${num2}, 0], %)`).click()
await page.getByText(` |> line([-${commonPoints.num2}, 0], %)`).click()
await page.waitForTimeout(300)
await page.keyboard.down('Shift')
await expect(page.locator('.cm-cursor')).toHaveCount(1)
@ -575,7 +575,7 @@ test('Selections work on fresh and edited sketch', async ({ page }) => {
// select a line
// await topHorzSegmentClick()
await page.getByText(startAt).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.getByText(commonPoints.startAt).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.waitForTimeout(100)
// enter sketch again
@ -737,34 +737,32 @@ test('Can add multiple sketches', async ({ page }) => {
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.74, -32.03]'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)`)
|> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num = 23.97
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`)
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 20)
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num}], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)`)
await page.mouse.click(startXPx, 500 - PUR * 20)
const finalCodeFirstSketch = `const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)
|> line([0, ${num}], %)
|> line([-47.71, 0], %)`
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)
|> line([0, ${commonPoints.num1}], %)
|> line([-${commonPoints.num2}, 0], %)`
await expect(page.locator('.cm-content')).toHaveText(finalCodeFirstSketch)
// exit the sketch
@ -786,7 +784,7 @@ test('Can add multiple sketches', async ({ page }) => {
await u.clearAndCloseDebugPanel()
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt2 = '[23.61, -31.85]'
const startAt2 = '[26.23, -35.39]'
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
@ -800,7 +798,7 @@ const part002 = startSketchOn('XY')
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num2 = 23.83
const num2 = 26.48
await expect(
(await page.locator('.cm-content').innerText()).replace(/\s/g, '')
).toBe(
@ -829,7 +827,7 @@ const part002 = startSketchOn('XY')
|> startProfileAt(${startAt2}, %)
|> line([${num2}, 0], %)
|> line([0, ${num2}], %)
|> line([-47.44, 0], %)`.replace(/\s/g, '')
|> line([-52.71, 0], %)`.replace(/\s/g, '')
)
})

View File

@ -31,6 +31,12 @@ test.beforeEach(async ({ context, page }) => {
test.setTimeout(60000)
const commonPoints = {
startAt: '[26.38, -35.59]',
num1: 26.63,
num2: 53.01,
}
test('change camera, show planes', async ({ page, context }) => {
const u = getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
@ -448,10 +454,9 @@ test('Draft segments should look right', async ({ page }) => {
const startXPx = 600
await page.mouse.click(startXPx + PUR * 10, 500 - PUR * 10)
const startAt = '[23.74, -32.03]'
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)`)
|> startProfileAt(${commonPoints.startAt}, %)`)
await page.waitForTimeout(100)
await u.closeDebugPanel()
@ -463,11 +468,10 @@ test('Draft segments should look right', async ({ page }) => {
await page.mouse.click(startXPx + PUR * 20, 500 - PUR * 10)
await page.waitForTimeout(100)
const num = 23.97
await expect(page.locator('.cm-content'))
.toHaveText(`const part001 = startSketchOn('-XZ')
|> startProfileAt(${startAt}, %)
|> line([${num}, 0], %)`)
|> startProfileAt(${commonPoints.startAt}, %)
|> line([${commonPoints.num1}, 0], %)`)
await page.getByRole('button', { name: 'Tangential Arc' }).click()

View File

@ -85,7 +85,7 @@
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
"lint": "eslint --fix src",
"bump-jsons": "echo \"$(jq --arg v \"$VERSION\" '.version=$v' package.json --indent 2)\" > package.json && echo \"$(jq --arg v \"$VERSION\" '.package.version=$v' src-tauri/tauri.conf.json --indent 2)\" > src-tauri/tauri.conf.json",
"postinstall": "patch-package && yarn xstate:typegen",
"postinstall": "yarn xstate:typegen",
"xstate:typegen": "yarn xstate typegen \"src/**/*.ts?(x)\""
},
"prettier": {
@ -134,7 +134,6 @@
"eslint-plugin-css-modules": "^2.12.0",
"happy-dom": "^10.8.0",
"husky": "^8.0.3",
"patch-package": "^8.0.0",
"pixelmatch": "^5.3.0",
"pngjs": "^7.0.0",
"postcss": "^8.4.31",

View File

@ -1,138 +0,0 @@
diff --git a/node_modules/three/examples/jsm/controls/OrbitControls.js b/node_modules/three/examples/jsm/controls/OrbitControls.js
index f29e7fe..0ef636b 100644
--- a/node_modules/three/examples/jsm/controls/OrbitControls.js
+++ b/node_modules/three/examples/jsm/controls/OrbitControls.js
@@ -113,6 +113,25 @@ class OrbitControls extends EventDispatcher {
// public methods
//
+ this.interactionGuards = {
+ pan: {
+ description: 'Right click + Shift + drag or middle click + drag',
+ callback: (e) => e.button === 2 && !e.ctrlKey,
+ },
+ zoom: {
+ description: 'Scroll wheel or Right click + Ctrl + drag',
+ dragCallback: (e) => e.button === 2 && e.ctrlKey,
+ scrollCallback: () => true,
+ },
+ rotate: {
+ description: 'Right click + drag',
+ callback: (e) => e.button === 0,
+ },
+ }
+ this.setMouseGuards = (interactionGuards) => {
+ this.interactionGuards = interactionGuards
+ }
+
this.getPolarAngle = function () {
return spherical.phi;
@@ -1057,92 +1076,21 @@ class OrbitControls extends EventDispatcher {
function onMouseDown( event ) {
- let mouseAction;
-
- switch ( event.button ) {
-
- case 0:
-
- mouseAction = scope.mouseButtons.LEFT;
- break;
-
- case 1:
-
- mouseAction = scope.mouseButtons.MIDDLE;
- break;
-
- case 2:
-
- mouseAction = scope.mouseButtons.RIGHT;
- break;
-
- default:
-
- mouseAction = - 1;
-
- }
-
- switch ( mouseAction ) {
-
- case MOUSE.DOLLY:
-
- if ( scope.enableZoom === false ) return;
-
- handleMouseDownDolly( event );
-
- state = STATE.DOLLY;
-
- break;
-
- case MOUSE.ROTATE:
-
- if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
-
- if ( scope.enablePan === false ) return;
-
- handleMouseDownPan( event );
-
- state = STATE.PAN;
-
- } else {
-
- if ( scope.enableRotate === false ) return;
-
- handleMouseDownRotate( event );
-
- state = STATE.ROTATE;
-
- }
-
- break;
-
- case MOUSE.PAN:
-
- if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
-
- if ( scope.enableRotate === false ) return;
-
- handleMouseDownRotate( event );
-
- state = STATE.ROTATE;
-
- } else {
-
- if ( scope.enablePan === false ) return;
-
- handleMouseDownPan( event );
-
- state = STATE.PAN;
-
- }
-
- break;
-
- default:
-
- state = STATE.NONE;
-
- }
+ if (scope.interactionGuards.pan.callback(event)) {
+ if (scope.enablePan === false) return
+ handleMouseDownPan(event)
+ state = STATE.PAN
+ } else if (scope.interactionGuards.rotate.callback(event)) {
+ if (scope.enableRotate === false) return
+ handleMouseDownRotate(event)
+ state = STATE.ROTATE
+ } else if (scope.interactionGuards.zoom.dragCallback(event)) {
+ if (scope.enableZoom === false) return
+ handleMouseDownDolly(event)
+ state = STATE.DOLLY
+ } else {
+ return
+ }
if ( state !== STATE.NONE ) {

View File

@ -0,0 +1,900 @@
import { MouseGuard } from 'lib/cameraControls'
import {
Euler,
MathUtils,
Matrix4,
OrthographicCamera,
PerspectiveCamera,
Quaternion,
Spherical,
Vector2,
Vector3,
} from 'three'
import {
DEBUG_SHOW_INTERSECTION_PLANE,
INTERSECTION_PLANE_LAYER,
SKETCH_LAYER,
ZOOM_MAGIC_NUMBER,
} from './sceneInfra'
import { EngineCommand, engineCommandManager } from 'lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid'
import { deg2Rad } from 'lib/utils2d'
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
import * as TWEEN from '@tweenjs/tween.js'
import { isQuaternionVertical } from './helpers'
const ORTHOGRAPHIC_CAMERA_SIZE = 20
const FRAMES_TO_ANIMATE_IN = 30
const tempQuaternion = new Quaternion() // just used for maths
interface ThreeCamValues {
position: Vector3
quaternion: Quaternion
zoom: number
isPerspective: boolean
target: Vector3
}
export type ReactCameraProperties =
| {
type: 'perspective'
fov?: number
position: [number, number, number]
quaternion: [number, number, number, number]
}
| {
type: 'orthographic'
zoom?: number
position: [number, number, number]
quaternion: [number, number, number, number]
}
const lastCmdDelay = 50
let lastCmd: EngineCommand | null = null
let lastCmdTime: number = Date.now()
let lastCmdTimeoutId: number | null = null
const sendLastReliableChannel = () => {
if (lastCmd && Date.now() - lastCmdTime >= lastCmdDelay) {
engineCommandManager.sendSceneCommand(lastCmd, true)
lastCmdTime = Date.now()
}
}
const throttledUpdateEngineCamera = throttle((threeValues: ThreeCamValues) => {
const cmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
...convertThreeCamValuesToEngineCam(threeValues),
},
}
engineCommandManager.sendSceneCommand(cmd)
lastCmd = cmd
lastCmdTime = Date.now()
if (lastCmdTimeoutId !== null) {
clearTimeout(lastCmdTimeoutId)
}
lastCmdTimeoutId = setTimeout(
sendLastReliableChannel,
lastCmdDelay
) as any as number
}, 1000 / 30)
let lastPerspectiveCmd: EngineCommand | null = null
let lastPerspectiveCmdTime: number = Date.now()
let lastPerspectiveCmdTimeoutId: number | null = null
const sendLastPerspectiveReliableChannel = () => {
if (
lastPerspectiveCmd &&
Date.now() - lastPerspectiveCmdTime >= lastCmdDelay
) {
engineCommandManager.sendSceneCommand(lastPerspectiveCmd, true)
lastPerspectiveCmdTime = Date.now()
}
}
const throttledUpdateEngineFov = throttle(
(vals: {
position: Vector3
quaternion: Quaternion
zoom: number
fov: number
target: Vector3
}) => {
const cmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_perspective_settings',
...convertThreeCamValuesToEngineCam({
...vals,
isPerspective: true,
}),
fov_y: vals.fov,
...calculateNearFarFromFOV(vals.fov),
},
}
engineCommandManager.sendSceneCommand(cmd)
lastPerspectiveCmd = cmd
lastPerspectiveCmdTime = Date.now()
if (lastPerspectiveCmdTimeoutId !== null) {
clearTimeout(lastPerspectiveCmdTimeoutId)
}
lastPerspectiveCmdTimeoutId = setTimeout(
sendLastPerspectiveReliableChannel,
lastCmdDelay
) as any as number
},
1000 / 15
)
export class CameraControls {
camera: PerspectiveCamera | OrthographicCamera
target: Vector3
domElement: HTMLCanvasElement
isDragging: boolean
mouseDownPosition: Vector2
mouseNewPosition: Vector2
rotationSpeed = 0.3
enableRotate = true
enablePan = true
enableZoom = true
lastPerspectiveFov: number = 45
pendingZoom: number | null = null
pendingRotation: Vector2 | null = null
pendingPan: Vector2 | null = null
interactionGuards: MouseGuard = {
pan: {
description: 'Right click + Shift + drag or middle click + drag',
callback: (e) => !!(e.buttons & 4) && !e.ctrlKey,
},
zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag',
dragCallback: (e) => e.button === 2 && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Right click + drag',
callback: (e) => {
console.log('event', e)
return !!(e.buttons & 2)
},
},
}
isFovAnimationInProgress = false
fovBeforeOrtho = 45
get isPerspective() {
return this.camera instanceof PerspectiveCamera
}
// reacts hooks into some of this singleton's properties
reactCameraProperties: ReactCameraProperties = {
type: 'perspective',
fov: 12,
position: [0, 0, 0],
quaternion: [0, 0, 0, 1],
}
setCam = (camProps: ReactCameraProperties) => {
if (
camProps.type === 'perspective' &&
this.camera instanceof OrthographicCamera
) {
this.usePerspectiveCamera()
} else if (
camProps.type === 'orthographic' &&
this.camera instanceof PerspectiveCamera
) {
this.useOrthographicCamera()
}
this.camera.position.set(...camProps.position)
this.camera.quaternion.set(...camProps.quaternion)
if (
camProps.type === 'perspective' &&
this.camera instanceof PerspectiveCamera
) {
// not sure what to do here, calling dollyZoom here is buggy because it updates the position
// at the same time
} else if (
camProps.type === 'orthographic' &&
this.camera instanceof OrthographicCamera
) {
this.camera.zoom = camProps.zoom || 1
}
this.camera.updateProjectionMatrix()
this.update(true)
}
constructor(isOrtho = false, domElement: HTMLCanvasElement) {
this.camera = isOrtho ? new OrthographicCamera() : new PerspectiveCamera()
this.camera.up.set(0, 0, 1)
this.camera.far = 20000
this.target = new Vector3()
this.domElement = domElement
this.isDragging = false
this.mouseDownPosition = new Vector2()
this.mouseNewPosition = new Vector2()
this.domElement.addEventListener('pointerdown', this.onMouseDown)
this.domElement.addEventListener('pointermove', this.onMouseMove)
this.domElement.addEventListener('pointerup', this.onMouseUp)
this.domElement.addEventListener('wheel', this.onMouseWheel)
window.addEventListener('resize', this.onWindowResize)
this.onWindowResize()
this.update()
}
private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void =
() => {}
setIsCamMovingCallback(cb: (isMoving: boolean, isTween: boolean) => void) {
this._isCamMovingCallback = cb
}
private _camChangeCallbacks: { [key: string]: () => void } = {}
subscribeToCamChange(cb: () => void) {
const cbId = uuidv4()
this._camChangeCallbacks[cbId] = cb
const unsubscribe = () => {
delete this._camChangeCallbacks[cbId]
}
return unsubscribe
}
onWindowResize = () => {
if (this.camera instanceof PerspectiveCamera) {
this.camera.aspect = window.innerWidth / window.innerHeight
} else if (this.camera instanceof OrthographicCamera) {
const aspect = window.innerWidth / window.innerHeight
this.camera.left = -ORTHOGRAPHIC_CAMERA_SIZE * aspect
this.camera.right = ORTHOGRAPHIC_CAMERA_SIZE * aspect
this.camera.top = ORTHOGRAPHIC_CAMERA_SIZE
this.camera.bottom = -ORTHOGRAPHIC_CAMERA_SIZE
}
this.camera.updateProjectionMatrix()
}
onMouseDown = (event: MouseEvent) => {
this.isDragging = true
this.mouseDownPosition.set(event.clientX, event.clientY)
}
onMouseMove = (event: MouseEvent) => {
if (this.isDragging) {
this.mouseNewPosition.set(event.clientX, event.clientY)
const deltaMove = this.mouseNewPosition
.clone()
.sub(this.mouseDownPosition)
this.mouseDownPosition.copy(this.mouseNewPosition)
let state: 'pan' | 'rotate' | 'zoom' = 'pan'
if (this.interactionGuards.pan.callback(event as any)) {
if (this.enablePan === false) return
// handleMouseDownPan(event)
state = 'pan'
} else if (this.interactionGuards.rotate.callback(event as any)) {
if (this.enableRotate === false) return
// handleMouseDownRotate(event)
state = 'rotate'
} else if (this.interactionGuards.zoom.dragCallback(event as any)) {
if (this.enableZoom === false) return
// handleMouseDownDolly(event)
state = 'zoom'
} else {
return
}
// Implement camera movement logic here based on deltaMove
// For example, for rotating the camera around the target:
if (state === 'rotate') {
this.pendingRotation = this.pendingRotation
? this.pendingRotation
: new Vector2()
this.pendingRotation.x += deltaMove.x
this.pendingRotation.y += deltaMove.y
} else if (state === 'zoom') {
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
this.pendingZoom *= 1 + deltaMove.y * 0.01
} else if (state === 'pan') {
this.pendingPan = this.pendingPan ? this.pendingPan : new Vector2()
let distance = this.camera.position.distanceTo(this.target)
if (this.camera instanceof OrthographicCamera) {
const zoomFudgeFactor = 2280
distance = zoomFudgeFactor / (this.camera.zoom * 45)
}
const panSpeed = (distance / 1000 / 45) * this.fovBeforeOrtho
this.pendingPan.x += -deltaMove.x * panSpeed
this.pendingPan.y += deltaMove.y * panSpeed
}
}
}
onMouseUp = (event: MouseEvent) => {
this.isDragging = false
}
onMouseWheel = (event: WheelEvent) => {
// Assume trackpad if the deltas are small and integers
const isTrackpad = Math.abs(event.deltaY) <= 1 || event.deltaY % 1 === 0
const zoomSpeed = isTrackpad ? 0.02 : 0.1 // Reduced zoom speed for trackpad
this.pendingZoom = this.pendingZoom ? this.pendingZoom : 1
this.pendingZoom *= 1 + (event.deltaY > 0 ? zoomSpeed : -zoomSpeed)
}
useOrthographicCamera = () => {
if (this.camera instanceof OrthographicCamera) return
const { x: px, y: py, z: pz } = this.camera.position
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
const aspect = window.innerWidth / window.innerHeight
this.lastPerspectiveFov = this.camera.fov
const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov)
this.camera = new OrthographicCamera(
-ORTHOGRAPHIC_CAMERA_SIZE * aspect,
ORTHOGRAPHIC_CAMERA_SIZE * aspect,
ORTHOGRAPHIC_CAMERA_SIZE,
-ORTHOGRAPHIC_CAMERA_SIZE,
z_near,
z_far
)
this.camera.up.set(0, 0, 1)
this.camera.layers.enable(SKETCH_LAYER)
if (DEBUG_SHOW_INTERSECTION_PLANE)
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
this.camera.position.set(px, py, pz)
const distance = this.camera.position.distanceTo(this.target.clone())
const fovFactor = 45 / this.lastPerspectiveFov
this.camera.zoom = (ZOOM_MAGIC_NUMBER * fovFactor * 0.8) / distance
this.camera.quaternion.set(qx, qy, qz, qw)
this.camera.updateProjectionMatrix()
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_orthographic',
},
})
this.onCameraChange()
}
private createPerspectiveCamera = () => {
const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov)
this.camera = new PerspectiveCamera(
this.lastPerspectiveFov,
window.innerWidth / window.innerHeight,
z_near,
z_far
)
this.camera.up.set(0, 0, 1)
this.camera.layers.enable(SKETCH_LAYER)
if (DEBUG_SHOW_INTERSECTION_PLANE)
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
return this.camera
}
usePerspectiveCamera = () => {
const { x: px, y: py, z: pz } = this.camera.position
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
const zoom = this.camera.zoom
this.camera = this.createPerspectiveCamera()
this.camera.position.set(px, py, pz)
this.camera.quaternion.set(qx, qy, qz, qw)
const zoomFudgeFactor = 2280
const distance = zoomFudgeFactor / (zoom * this.lastPerspectiveFov)
const direction = new Vector3().subVectors(
this.camera.position,
this.target
)
direction.normalize()
this.camera.position.copy(this.target).addScaledVector(direction, distance)
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_perspective',
parameters: {
fov_y: this.camera.fov,
...calculateNearFarFromFOV(this.lastPerspectiveFov),
},
},
})
this.onCameraChange()
this.update()
return this.camera
}
dollyZoom = (newFov: number) => {
if (!(this.camera instanceof PerspectiveCamera)) {
console.warn('Dolly zoom is only applicable to perspective cameras.')
return
}
this.lastPerspectiveFov = newFov
// Calculate the direction vector from the camera towards the controls target
const direction = new Vector3()
.subVectors(this.target, this.camera.position)
.normalize()
// Calculate the distance to the controls target before changing the FOV
const distanceBefore = this.camera.position.distanceTo(this.target)
// Calculate the scale factor for the new FOV compared to the old one
// This needs to be calculated before updating the camera's FOV
const oldFov = this.camera.fov
const viewHeightFactor = (fov: number) => {
/* *
/|
/ |
/ |
/ |
/ | viewHeight/2
/ |
/ |
/↙fov/2 |
/________|
\ |
\._._._.|
*/
return Math.tan(deg2Rad(fov / 2))
}
const scaleFactor = viewHeightFactor(oldFov) / viewHeightFactor(newFov)
this.camera.fov = newFov
this.camera.updateProjectionMatrix()
const distanceAfter = distanceBefore * scaleFactor
const newPosition = this.target
.clone()
.add(direction.multiplyScalar(-distanceAfter))
this.camera.position.copy(newPosition)
const { z_near, z_far } = calculateNearFarFromFOV(this.lastPerspectiveFov)
this.camera.near = z_near
this.camera.far = z_far
throttledUpdateEngineFov({
fov: newFov,
position: newPosition,
quaternion: this.camera.quaternion,
zoom: this.camera.zoom,
target: this.target,
})
}
update = (forceUpdate = false) => {
// If there are any changes that need to be applied to the camera, apply them here.
let didChange = forceUpdate
if (this.pendingRotation) {
this.rotateCamera(this.pendingRotation.x, this.pendingRotation.y)
this.pendingRotation = null // Clear the pending rotation after applying it
didChange = true
}
if (this.pendingZoom) {
if (this.camera instanceof PerspectiveCamera) {
// move camera towards or away from the target
const distance = this.camera.position.distanceTo(this.target)
const newDistance = distance * this.pendingZoom
const direction = this.camera.position
.clone()
.sub(this.target)
.normalize()
const newPosition = this.target
.clone()
.add(direction.multiplyScalar(newDistance))
this.camera.position.copy(newPosition)
this.camera.updateProjectionMatrix()
this.pendingZoom = null // Clear the pending zoom after applying it
} else {
// TODO change ortho zoom
this.camera.zoom = this.camera.zoom / this.pendingZoom
this.pendingZoom = null
}
didChange = true
}
if (this.pendingPan) {
// move camera left/right and up/down
const offset = this.camera.position.clone().sub(this.target)
const direction = offset.clone().normalize()
const cameraQuaternion = this.camera.quaternion
const up = new Vector3(0, 1, 0).applyQuaternion(cameraQuaternion)
const right = new Vector3().crossVectors(up, direction)
right.multiplyScalar(this.pendingPan.x)
up.multiplyScalar(this.pendingPan.y)
const newPosition = this.camera.position.clone().add(right).add(up)
this.target.add(right)
this.target.add(up)
this.camera.position.copy(newPosition)
this.pendingPan = null
didChange = true
}
this.safeLookAtTarget()
// Update the camera's matrices
this.camera.updateMatrixWorld()
if (didChange) {
this.onCameraChange()
}
// damping would be implemented here in update if we choose to add it.
}
rotateCamera = (deltaX: number, deltaY: number) => {
const quat = new Quaternion().setFromUnitVectors(
new Vector3(0, 0, 1),
new Vector3(0, 1, 0)
)
const quatInverse = quat.clone().invert()
const angleX = deltaX * this.rotationSpeed // rotationSpeed is a constant that defines how fast the camera rotates
const angleY = deltaY * this.rotationSpeed
// Convert angles to radians
const radianX = MathUtils.degToRad(angleX)
const radianY = MathUtils.degToRad(angleY)
// Get the offset from the camera to the target
const offset = new Vector3().subVectors(this.camera.position, this.target)
// spherical is a y-up paradigm, need to conform to that for now
offset.applyQuaternion(quat)
// Convert offset to spherical coordinates
const spherical = new Spherical().setFromVector3(offset)
// Apply the rotations
spherical.theta -= radianX
spherical.phi -= radianY
// Restrict the phi angle to avoid the camera flipping at the poles
spherical.phi = Math.max(0.1, Math.min(Math.PI - 0.1, spherical.phi))
// Convert back to Cartesian coordinates
offset.setFromSpherical(spherical)
// put the offset back into the z-up paradigm
offset.applyQuaternion(quatInverse)
// Update the camera's position
this.camera.position.copy(this.target).add(offset)
// Look at the target
this.camera.updateMatrixWorld()
}
safeLookAtTarget(up = new Vector3(0, 0, 1)) {
const quaternion = _lookAt(this.camera.position, this.target, up)
this.camera.quaternion.copy(quaternion)
this.camera.updateMatrixWorld()
}
tweenCamToNegYAxis(
// -90 degrees from the x axis puts the camera on the negative y axis
targetAngle = -Math.PI / 2,
duration = 500
): Promise<void> {
// should tween the camera so that it has an xPosition of 0, and forcing it's yPosition to be negative
// zPosition should stay the same
const xyRadius = Math.sqrt(
(this.target.x - this.camera.position.x) ** 2 +
(this.target.y - this.camera.position.y) ** 2
)
const xyAngle = Math.atan2(
this.camera.position.y - this.target.y,
this.camera.position.x - this.target.x
)
this._isCamMovingCallback(true, true)
return new Promise((resolve) => {
new TWEEN.Tween({ angle: xyAngle })
.to({ angle: targetAngle }, duration)
.onUpdate((obj) => {
const x = xyRadius * Math.cos(obj.angle)
const y = xyRadius * Math.sin(obj.angle)
this.camera.position.set(
this.target.x + x,
this.target.y + y,
this.camera.position.z
)
this.update()
this.onCameraChange()
})
.onComplete((obj) => {
const x = xyRadius * Math.cos(obj.angle)
const y = xyRadius * Math.sin(obj.angle)
this.camera.position.set(
this.target.x + x,
this.target.y + y,
this.camera.position.z
)
this.update()
this.onCameraChange()
this._isCamMovingCallback(false, true)
// resolve after a couple of frames
requestAnimationFrame(() => {
requestAnimationFrame(() => resolve())
})
})
.start()
})
}
async tweenCameraToQuaternion(
targetQuaternion: Quaternion,
duration = 500,
toOrthographic = true
): Promise<void> {
const isVertical = isQuaternionVertical(targetQuaternion)
let remainingDuration = duration
if (isVertical) {
remainingDuration = duration * 0.5
const orbitRotationDuration = duration * 0.65
let targetAngle = -Math.PI / 2
const v = new Vector3(0, 0, 1).applyQuaternion(targetQuaternion)
if (v.z < 0) targetAngle = Math.PI / 2
await this.tweenCamToNegYAxis(targetAngle, orbitRotationDuration)
}
await this._tweenCameraToQuaternion(
targetQuaternion,
remainingDuration,
toOrthographic
)
}
_tweenCameraToQuaternion(
targetQuaternion: Quaternion,
duration = 500,
toOrthographic = false
): Promise<void> {
return new Promise((resolve) => {
const camera = this.camera
this._isCamMovingCallback(true, true)
const initialQuaternion = camera.quaternion.clone()
const isVertical = isQuaternionVertical(targetQuaternion)
let tweenEnd = isVertical ? 0.99 : 1
const controlsTarget = this.target.clone()
const initialDistance = controlsTarget.distanceTo(camera.position.clone())
const cameraAtTime = (animationProgress: number /* 0 - 1 */) => {
const currentQ = tempQuaternion.slerpQuaternions(
initialQuaternion,
targetQuaternion,
animationProgress
)
if (this.camera instanceof PerspectiveCamera)
// changing the camera position back when it's orthographic doesn't do anything
// and it messes up animating back to perspective later
this.camera.position
.set(0, 0, 1)
.applyQuaternion(currentQ)
.multiplyScalar(initialDistance)
.add(controlsTarget)
this.camera.up.set(0, 1, 0).applyQuaternion(currentQ).normalize()
this.camera.quaternion.copy(currentQ)
this.target.copy(controlsTarget)
// this.controls.update()
this.camera.updateProjectionMatrix()
this.update()
this.onCameraChange()
}
const onComplete = async () => {
if (isReducedMotion() && toOrthographic) {
cameraAtTime(0.99)
this.useOrthographicCamera()
} else if (toOrthographic) {
await this.animateToOrthographic()
}
this.enableRotate = false
this._isCamMovingCallback(false, true)
resolve()
}
if (isReducedMotion()) {
onComplete()
return
}
new TWEEN.Tween({ t: 0 })
.to({ t: tweenEnd }, duration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(({ t }) => cameraAtTime(t))
.onComplete(onComplete)
.start()
})
}
animateToOrthographic = () =>
new Promise((resolve) => {
this.isFovAnimationInProgress = true
let currentFov = this.lastPerspectiveFov
this.fovBeforeOrtho = currentFov
const targetFov = 4
const fovAnimationStep = (currentFov - targetFov) / FRAMES_TO_ANIMATE_IN
let frameWaitOnFinish = 10
const animateFovChange = () => {
if (this.camera instanceof PerspectiveCamera) {
if (this.camera.fov > targetFov) {
// Decrease the FOV
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
this.camera.updateProjectionMatrix()
this.dollyZoom(currentFov)
requestAnimationFrame(animateFovChange) // Continue the animation
} else if (frameWaitOnFinish > 0) {
frameWaitOnFinish--
requestAnimationFrame(animateFovChange) // Continue the animation
} else {
// Once the target FOV is reached, switch to the orthographic camera
// Needs to wait a couple frames after the FOV animation is complete
this.useOrthographicCamera()
this.isFovAnimationInProgress = false
resolve(true)
}
}
}
animateFovChange() // Start the animation
})
animateToPerspective = () =>
new Promise((resolve) => {
this.isFovAnimationInProgress = true
// Immediately set the camera to perspective with a very low FOV
const targetFov = this.fovBeforeOrtho // Target FOV for perspective
this.lastPerspectiveFov = 4
let currentFov = 4
this.camera.updateProjectionMatrix()
const fovAnimationStep = (targetFov - currentFov) / FRAMES_TO_ANIMATE_IN
this.usePerspectiveCamera()
const animateFovChange = () => {
if (this.camera instanceof OrthographicCamera) return
if (this.camera.fov < targetFov) {
// Increase the FOV
currentFov = Math.min(currentFov + fovAnimationStep, targetFov)
// this.camera.fov = currentFov
this.camera.updateProjectionMatrix()
this.dollyZoom(currentFov)
requestAnimationFrame(animateFovChange) // Continue the animation
} else {
// Set the flag to false as the FOV animation is complete
this.isFovAnimationInProgress = false
resolve(true)
}
}
animateFovChange() // Start the animation
})
reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {}
setReactCameraPropertiesCallback = (
cb: (a: ReactCameraProperties) => void
) => {
this.reactCameraPropertiesCallback = cb
}
deferReactUpdate = throttle((a: ReactCameraProperties) => {
this.reactCameraPropertiesCallback(a)
}, 200)
onCameraChange = () => {
const distance = this.target.distanceTo(this.camera.position)
if (this.camera.far / 2.1 < distance || this.camera.far / 1.9 > distance) {
this.camera.far = distance * 2
this.camera.near = distance / 10
this.camera.updateProjectionMatrix()
}
throttledUpdateEngineCamera({
quaternion: this.camera.quaternion,
position: this.camera.position,
zoom: this.camera.zoom,
isPerspective: this.isPerspective,
target: this.target,
})
this.deferReactUpdate({
type: this.isPerspective ? 'perspective' : 'orthographic',
[this.isPerspective ? 'fov' : 'zoom']:
this.camera instanceof PerspectiveCamera
? this.camera.fov
: this.camera.zoom,
position: [
roundOff(this.camera.position.x, 2),
roundOff(this.camera.position.y, 2),
roundOff(this.camera.position.z, 2),
],
quaternion: [
roundOff(this.camera.quaternion.x, 2),
roundOff(this.camera.quaternion.y, 2),
roundOff(this.camera.quaternion.z, 2),
roundOff(this.camera.quaternion.w, 2),
],
})
Object.values(this._camChangeCallbacks).forEach((cb) => cb())
}
}
// currently duplicated, delete one
function calculateNearFarFromFOV(fov: number) {
const nearFarRatio = (fov - 3) / (45 - 3)
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
const z_far = 1000 + nearFarRatio * (100000 - 1000)
return { z_near: 0.1, z_far }
}
// currently duplicated, delete one
function convertThreeCamValuesToEngineCam({
target,
position,
quaternion,
zoom,
isPerspective,
}: ThreeCamValues): {
center: Vector3
up: Vector3
vantage: Vector3
} {
// Something to consider is that the orbit controls have a target,
// we're kind of deriving the target/lookAtVector here when it might not be needed
// leaving for now since it's working but maybe revisit later
const euler = new Euler().setFromQuaternion(quaternion, 'XYZ')
const lookAtVector = new Vector3(0, 0, -1)
.applyEuler(euler)
.normalize()
.add(position)
const upVector = new Vector3(0, 1, 0).applyEuler(euler).normalize()
if (isPerspective) {
return {
center: target,
up: upVector,
vantage: position,
}
}
const fudgeFactor2 = zoom * 0.9979224466814468 - 0.03473692325839295
const zoomFactor = (-ZOOM_MAGIC_NUMBER + fudgeFactor2) / zoom
const direction = lookAtVector.clone().sub(position).normalize()
const newVantage = position.clone().add(direction.multiplyScalar(zoomFactor))
return {
center: lookAtVector,
up: upVector,
vantage: newVantage,
}
}
// Pure function helpers
function _lookAt(position: Vector3, target: Vector3, up: Vector3): Quaternion {
// Direction from position to target, normalized.
let direction = new Vector3().subVectors(target, position).normalize()
// Calculate a new "effective" up vector that is orthogonal to the direction.
// This step ensures that the up vector does not affect the direction the camera is looking.
let right = new Vector3().crossVectors(direction, up).normalize()
let orthogonalUp = new Vector3().crossVectors(right, direction).normalize()
// Create a lookAt matrix using the position, and the recalculated orthogonal up vector.
let lookAtMatrix = new Matrix4()
lookAtMatrix.lookAt(position, target, orthogonalUp)
// Create a quaternion from the lookAt matrix.
let quaternion = new Quaternion().setFromRotationMatrix(lookAtMatrix)
return quaternion
}

View File

@ -4,11 +4,8 @@ import { useModelingContext } from 'hooks/useModelingContext'
import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useStore } from 'useStore'
import {
DEBUG_SHOW_BOTH_SCENES,
ReactCameraProperties,
sceneInfra,
} from './sceneInfra'
import { DEBUG_SHOW_BOTH_SCENES, sceneInfra } from './sceneInfra'
import { ReactCameraProperties } from './CameraControls'
import { throttle } from 'lib/utils'
function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
@ -18,7 +15,7 @@ function useShouldHideScene(): { hideClient: boolean; hideServer: boolean } {
const { state } = useModelingContext()
useEffect(() => {
sceneInfra.setIsCamMovingCallback((isMoving, isTween) => {
sceneInfra.camControls.setIsCamMovingCallback((isMoving, isTween) => {
setIsCamMoving(isMoving)
setIsTween(isTween)
})
@ -52,7 +49,8 @@ export const ClientSideScene = ({
// Listen for changes to the camera controls setting
// and update the client-side scene's controls accordingly.
useEffect(() => {
sceneInfra.setInteractionGuards(cameraMouseDragGuards[cameraControls])
sceneInfra.camControls.interactionGuards =
cameraMouseDragGuards[cameraControls]
}, [cameraControls])
useEffect(() => {
sceneInfra.updateOtherSelectionColors(
@ -93,7 +91,7 @@ export const ClientSideScene = ({
const throttled = throttle((a: ReactCameraProperties) => {
if (a.type === 'perspective' && a.fov) {
sceneInfra.dollyZoom(a.fov)
sceneInfra.camControls.dollyZoom(a.fov)
}
}, 1000 / 15)
@ -107,7 +105,7 @@ export const CamDebugSettings = () => {
const [fov, setFov] = useState(12)
useEffect(() => {
sceneInfra.setReactCameraPropertiesCallback(setCamSettings)
sceneInfra.camControls.setReactCameraPropertiesCallback(setCamSettings)
}, [sceneInfra])
useEffect(() => {
if (camSettings.type === 'perspective' && camSettings.fov) {
@ -124,9 +122,9 @@ export const CamDebugSettings = () => {
checked={camSettings.type === 'perspective'}
onChange={(e) => {
if (camSettings.type === 'perspective') {
sceneInfra.useOrthographicCamera()
sceneInfra.camControls.useOrthographicCamera()
} else {
sceneInfra.usePerspectiveCamera()
sceneInfra.camControls.usePerspectiveCamera()
}
}}
/>
@ -156,7 +154,7 @@ export const CamDebugSettings = () => {
value={camSettings.fov}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
sceneInfra.camControls.setCam({
...camSettings,
fov: parseFloat(e.target.value),
})
@ -173,7 +171,7 @@ export const CamDebugSettings = () => {
value={camSettings.zoom}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
sceneInfra.camControls.setCam({
...camSettings,
zoom: parseFloat(e.target.value),
})
@ -194,7 +192,7 @@ export const CamDebugSettings = () => {
value={camSettings.position[0]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
sceneInfra.camControls.setCam({
...camSettings,
position: [
parseFloat(e.target.value),
@ -214,7 +212,7 @@ export const CamDebugSettings = () => {
value={camSettings.position[1]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
sceneInfra.camControls.setCam({
...camSettings,
position: [
camSettings.position[0],
@ -234,7 +232,7 @@ export const CamDebugSettings = () => {
value={camSettings.position[2]}
className="text-black w-16"
onChange={(e) => {
sceneInfra.setCam({
sceneInfra.camControls.setCam({
...camSettings,
position: [
camSettings.position[0],

View File

@ -1,5 +1,5 @@
import { Quaternion } from 'three'
import { isQuaternionVertical } from './sceneInfra'
import { isQuaternionVertical } from './helpers'
describe('isQuaternionVertical', () => {
it('should identify vertical quaternions', () => {

View File

@ -1,3 +1,4 @@
import { compareVec2Epsilon2 } from 'lang/std/sketch'
import {
GridHelper,
LineBasicMaterial,
@ -5,6 +6,8 @@ import {
PerspectiveCamera,
Group,
Mesh,
Quaternion,
Vector3,
} from 'three'
export function createGridHelper({
@ -31,3 +34,9 @@ export const orthoScale = (cam: OrthographicCamera | PerspectiveCamera) =>
export const perspScale = (cam: PerspectiveCamera, group: Group | Mesh) =>
(group.position.distanceTo(cam.position) * cam.fov) / 4000
export function isQuaternionVertical(q: Quaternion) {
const v = new Vector3(0, 0, 1).applyQuaternion(q)
// no x or y components means it's vertical
return compareVec2Epsilon2([v.x, v.y], [0, 0])
}

View File

@ -24,7 +24,6 @@ import {
defaultPlaneColor,
getSceneScale,
INTERSECTION_PLANE_LAYER,
isQuaternionVertical,
RAYCASTABLE_PLANE,
sceneInfra,
SKETCH_GROUP_SEGMENTS,
@ -34,6 +33,7 @@ import {
Y_AXIS,
YZ_PLANE,
} from './sceneInfra'
import { isQuaternionVertical } from './helpers'
import {
CallExpression,
getTangentialArcToInfo,
@ -98,17 +98,17 @@ class SceneEntities {
currentSketchQuaternion: Quaternion | null = null
constructor() {
this.scene = sceneInfra?.scene
sceneInfra?.setOnCamChange(this.onCamChange)
sceneInfra?.camControls.subscribeToCamChange(this.onCamChange)
}
onCamChange = () => {
const orthoFactor = orthoScale(sceneInfra.camera)
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
Object.values(this.activeSegments).forEach((segment) => {
const factor =
sceneInfra.camera instanceof OrthographicCamera
sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camera, segment)
: perspScale(sceneInfra.camControls.camera, segment)
if (
segment.userData.from &&
segment.userData.to &&
@ -139,9 +139,9 @@ class SceneEntities {
})
if (this.axisGroup) {
const factor =
sceneInfra.camera instanceof OrthographicCamera
sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camera, this.axisGroup)
: perspScale(sceneInfra.camControls.camera, this.axisGroup)
const x = this.axisGroup.getObjectByName(X_AXIS)
x?.scale.set(1, factor, 1)
const y = this.axisGroup.getObjectByName(Y_AXIS)
@ -150,7 +150,12 @@ class SceneEntities {
}
createIntersectionPlane() {
const planeGeometry = new PlaneGeometry(100000, 100000)
if (sceneInfra.scene.getObjectByName(RAYCASTABLE_PLANE)) {
console.warn('createIntersectionPlane called when it already exists')
return
}
const hundredM = 1000000
const planeGeometry = new PlaneGeometry(hundredM, hundredM)
const planeMaterial = new MeshBasicMaterial({
color: 0xff0000,
side: DoubleSide,
@ -195,11 +200,12 @@ class SceneEntities {
this.axisGroup = new Group()
const gridHelper = createGridHelper({ size: 100, divisions: 10 })
gridHelper.position.z = -0.01
gridHelper.renderOrder = -3 // is this working?
gridHelper.name = 'gridHelper'
const sceneScale = getSceneScale(
sceneInfra.camera,
sceneInfra.controls.target
sceneInfra.camControls.camera,
sceneInfra.camControls.target
)
gridHelper.scale.set(sceneScale, sceneScale, sceneScale)
this.axisGroup.add(xAxisMesh, yAxisMesh, gridHelper)
@ -240,15 +246,6 @@ class SceneEntities {
}) {
sceneInfra.resetMouseListeners()
this.createIntersectionPlane()
const distance = sceneInfra.controls.target.distanceTo(
sceneInfra.camera.position
)
// TODO this should probably be distance to the sketch group, more important after sketch on face
// since sketches won't always so close to the origin
// is this the best place to adjust camera far?
if (sceneInfra.camera.far < distance * 1.5) {
sceneInfra.camera.far = distance * 2
}
const { truncatedAst, programMemoryOverride, variableDeclarationName } =
this.prepareTruncatedMemoryAndAst(
@ -280,11 +277,11 @@ class SceneEntities {
sketchGroup.position[1],
sketchGroup.position[2]
)
const orthoFactor = orthoScale(sceneInfra.camera)
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
const factor =
sceneInfra.camera instanceof OrthographicCamera
sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camera, dummy)
: perspScale(sceneInfra.camControls.camera, dummy)
sketchGroup.value.forEach((segment, index) => {
let segPathToNode = getNodePathFromSourceRange(
draftSegment ? truncatedAst : kclManager.ast,
@ -451,7 +448,7 @@ class SceneEntities {
},
})
}
sceneInfra.controls.enableRotate = false
sceneInfra.camControls.enableRotate = false
}
updateAstAndRejigSketch = async (
sketchPathToNode: PathToNode,
@ -554,7 +551,7 @@ class SceneEntities {
this.sceneProgramMemory = programMemory
const sketchGroup = programMemory.root[variableDeclarationName]
.value as Path[]
const orthoFactor = orthoScale(sceneInfra.camera)
const orthoFactor = orthoScale(sceneInfra.camControls.camera)
sketchGroup.forEach((segment, index) => {
const segPathToNode = getNodePathFromSourceRange(
modifiedAst,
@ -570,9 +567,9 @@ class SceneEntities {
// const prevSegment = sketchGroup.slice(index - 1)[0]
const type = group?.userData?.type
const factor =
sceneInfra.camera instanceof OrthographicCamera
sceneInfra.camControls.camera instanceof OrthographicCamera
? orthoFactor
: perspScale(sceneInfra.camera, group)
: perspScale(sceneInfra.camControls.camera, group)
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
this.updateTangentialArcToSegment({
prevSegment: sketchGroup[index - 1],
@ -729,10 +726,10 @@ class SceneEntities {
}
async animateAfterSketch() {
if (isReducedMotion()) {
sceneInfra.usePerspectiveCamera()
} else {
await sceneInfra.animateToPerspective()
sceneInfra.camControls.usePerspectiveCamera()
return
}
await sceneInfra.camControls.animateToPerspective()
}
removeSketchGrid() {
if (this.axisGroup) this.scene.remove(this.axisGroup)
@ -764,7 +761,7 @@ class SceneEntities {
reject()
}
}
sceneInfra.controls.enableRotate = true
sceneInfra.camControls.enableRotate = true
this.activeSegments = {}
// maybe should reset onMove etc handlers
if (shouldResolve) resolve(true)
@ -797,9 +794,8 @@ class SceneEntities {
onClick: (args) => {
if (!args || !args.object) return
if (args.event.which !== 1) return
const { object, intersection } = args
const type = object?.userData?.type || ''
console.log('intersection.normal?.z', intersection)
const { intersection } = args
const type = intersection.object.name || ''
const posNorm = Number(intersection.normal?.z) > 0
let planeString: DefaultPlaneStr = posNorm ? 'XY' : '-XY'
let normal: [number, number, number] = posNorm ? [0, 0, 1] : [0, 0, -1]

View File

@ -1,12 +1,10 @@
import {
AmbientLight,
Color,
Euler,
GridHelper,
LineBasicMaterial,
OrthographicCamera,
PerspectiveCamera,
Quaternion,
Scene,
Vector3,
WebGLRenderer,
@ -20,32 +18,26 @@ import {
Intersection,
Object3D,
Object3DEventMap,
BoxGeometry,
} from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { EngineCommand, engineCommandManager } from 'lang/std/engineConnection'
import { v4 as uuidv4 } from 'uuid'
import { isReducedMotion, roundOff, throttle } from 'lib/utils'
import { compareVec2Epsilon2 } from 'lang/std/sketch'
import { useModelingContext } from 'hooks/useModelingContext'
import { deg2Rad } from 'lib/utils2d'
import * as TWEEN from '@tweenjs/tween.js'
import { MouseGuard, cameraMouseDragGuards } from 'lib/cameraControls'
import { SourceRange } from 'lang/wasm'
import { Axis } from 'lib/selections'
import { BaseUnit, SETTINGS_PERSIST_KEY } from 'machines/settingsMachine'
import { CameraControls } from './CameraControls'
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
const ZOOM_MAGIC_NUMBER = 63.5
const FRAMES_TO_ANIMATE_IN = 30
const ORTHOGRAPHIC_CAMERA_SIZE = 20
export const ZOOM_MAGIC_NUMBER = 63.5
export const INTERSECTION_PLANE_LAYER = 1
export const SKETCH_LAYER = 2
const DEBUG_SHOW_INTERSECTION_PLANE = false
export const DEBUG_SHOW_INTERSECTION_PLANE = false
export const DEBUG_SHOW_BOTH_SCENES = false
export const RAYCASTABLE_PLANE = 'raycastable-plane'
@ -57,100 +49,6 @@ export const AXIS_GROUP = 'axisGroup'
export const SKETCH_GROUP_SEGMENTS = 'sketch-group-segments'
export const ARROWHEAD = 'arrowhead'
const tempQuaternion = new Quaternion() // just used for maths
interface ThreeCamValues {
position: Vector3
quaternion: Quaternion
zoom: number
isPerspective: boolean
target: Vector3
}
const lastCmdDelay = 50
let lastCmd: EngineCommand | null = null
let lastCmdTime: number = Date.now()
let lastCmdTimeoutId: number | null = null
const sendLastReliableChannel = () => {
if (lastCmd && Date.now() - lastCmdTime >= lastCmdDelay) {
engineCommandManager.sendSceneCommand(lastCmd, true)
lastCmdTime = Date.now()
}
}
const throttledUpdateEngineCamera = throttle((threeValues: ThreeCamValues) => {
const cmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_look_at',
...convertThreeCamValuesToEngineCam(threeValues),
},
}
engineCommandManager.sendSceneCommand(cmd)
lastCmd = cmd
lastCmdTime = Date.now()
if (lastCmdTimeoutId !== null) {
clearTimeout(lastCmdTimeoutId)
}
lastCmdTimeoutId = setTimeout(
sendLastReliableChannel,
lastCmdDelay
) as any as number
}, 1000 / 30)
let lastPerspectiveCmd: EngineCommand | null = null
let lastPerspectiveCmdTime: number = Date.now()
let lastPerspectiveCmdTimeoutId: number | null = null
const sendLastPerspectiveReliableChannel = () => {
if (
lastPerspectiveCmd &&
Date.now() - lastPerspectiveCmdTime >= lastCmdDelay
) {
engineCommandManager.sendSceneCommand(lastPerspectiveCmd, true)
lastPerspectiveCmdTime = Date.now()
}
}
const throttledUpdateEngineFov = throttle(
(vals: {
position: Vector3
quaternion: Quaternion
zoom: number
fov: number
target: Vector3
}) => {
const cmd: EngineCommand = {
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_perspective_settings',
...convertThreeCamValuesToEngineCam({
...vals,
isPerspective: true,
}),
fov_y: vals.fov,
...calculateNearFarFromFOV(vals.fov),
},
}
engineCommandManager.sendSceneCommand(cmd)
lastPerspectiveCmd = cmd
lastPerspectiveCmdTime = Date.now()
if (lastPerspectiveCmdTimeoutId !== null) {
clearTimeout(lastPerspectiveCmdTimeoutId)
}
lastPerspectiveCmdTimeoutId = setTimeout(
sendLastPerspectiveReliableChannel,
lastCmdDelay
) as any as number
},
1000 / 15
)
interface BaseCallbackArgs2 {
object: any
event: any
@ -178,34 +76,18 @@ interface onMoveCallbackArgs {
intersection: Intersection<Object3D<Object3DEventMap>>
}
export type ReactCameraProperties =
| {
type: 'perspective'
fov?: number
position: [number, number, number]
quaternion: [number, number, number, number]
}
| {
type: 'orthographic'
zoom?: number
position: [number, number, number]
quaternion: [number, number, number, number]
}
// This singleton class is responsible for all of the under the hood setup for the client side scene.
// That is the cameras and switching between them, raycasters for click mouse events and their abstractions (onClick etc), setting up controls.
// Anything that added the the scene for the user to interact with is probably in SceneEntities.ts
class SceneInfra {
static instance: SceneInfra
scene: Scene
camera: PerspectiveCamera | OrthographicCamera
renderer: WebGLRenderer
controls: OrbitControls
camControls: CameraControls
isPerspective = true
fov = 45
fovBeforeAnimate = 45
isFovAnimationInProgress = false
interactionGuards: MouseGuard = cameraMouseDragGuards.KittyCAD
onDragCallback: (arg: OnDragCallbackArgs) => void = () => {}
onMoveCallback: (arg: onMoveCallbackArgs) => void = () => {}
onClickCallback: (arg?: OnClickCallbackArgs) => void = () => {}
@ -256,55 +138,18 @@ class SceneInfra {
selectedObject: null | any = null
mouseDownVector: null | Vector2 = null
// reacts hooks into some of this singleton's properties
reactCameraProperties: ReactCameraProperties = {
type: 'perspective',
fov: 12,
position: [0, 0, 0],
quaternion: [0, 0, 0, 1],
}
reactCameraPropertiesCallback: (a: ReactCameraProperties) => void = () => {}
setReactCameraPropertiesCallback = (
cb: (a: ReactCameraProperties) => void
) => {
this.reactCameraPropertiesCallback = cb
}
setCam = (camProps: ReactCameraProperties) => {
if (
camProps.type === 'perspective' &&
this.camera instanceof OrthographicCamera
) {
this.usePerspectiveCamera()
} else if (
camProps.type === 'orthographic' &&
this.camera instanceof PerspectiveCamera
) {
this.useOrthographicCamera()
}
this.camera.position.set(...camProps.position)
this.camera.quaternion.set(...camProps.quaternion)
if (
camProps.type === 'perspective' &&
this.camera instanceof PerspectiveCamera
) {
// not sure what to do here, calling dollyZoom here is buggy because it updates the position
// at the same time
} else if (
camProps.type === 'orthographic' &&
this.camera instanceof OrthographicCamera
) {
this.camera.zoom = camProps.zoom || 1
}
this.camera.updateProjectionMatrix()
this.controls.update()
}
constructor() {
// SCENE
this.scene = new Scene()
this.scene.background = new Color(0x000000)
this.scene.background = null
// RENDERER
this.renderer = new WebGLRenderer({ antialias: true, alpha: true }) // Enable transparency
this.renderer.setSize(window.innerWidth, window.innerHeight)
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
window.addEventListener('resize', this.onWindowResize)
// CAMERA
const camHeightDistanceRatio = 0.5
const baseUnit: BaseUnit =
@ -315,25 +160,19 @@ class SceneInfra {
const ang = Math.atan(camHeightDistanceRatio)
const x = Math.cos(ang) * length
const y = Math.sin(ang) * length
this.camera = this.createPerspectiveCamera()
this.camera.position.set(0, -x, y)
if (DEBUG_SHOW_INTERSECTION_PLANE)
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
// RENDERER
this.renderer = new WebGLRenderer({ antialias: true, alpha: true }) // Enable transparency
this.renderer.setSize(window.innerWidth, window.innerHeight)
this.renderer.setClearColor(0x000000, 0) // Set clear color to black with 0 alpha (fully transparent)
window.addEventListener('resize', this.onWindowResize)
this.camControls = new CameraControls(false, this.renderer.domElement)
this.camControls.subscribeToCamChange(() => this.onCameraChange())
this.camControls.camera.layers.enable(SKETCH_LAYER)
this.camControls.camera.position.set(0, -x, y)
if (DEBUG_SHOW_INTERSECTION_PLANE)
this.camControls.camera.layers.enable(INTERSECTION_PLANE_LAYER)
// RAYCASTERS
this.raycaster.layers.enable(SKETCH_LAYER)
this.raycaster.layers.disable(0)
this.planeRaycaster.layers.enable(INTERSECTION_PLANE_LAYER)
// CONTROLS
this.controls = this.setupOrbitControls()
// GRID
const size = 100
const divisions = 10
@ -353,415 +192,40 @@ class SceneInfra {
SceneInfra.instance = this
}
private _isCamMovingCallback: (isMoving: boolean, isTween: boolean) => void =
() => {}
setIsCamMovingCallback(cb: (isMoving: boolean, isTween: boolean) => void) {
this._isCamMovingCallback = cb
}
private _onCamChange: () => void = () => {}
setOnCamChange(cb: () => void) {
this._onCamChange = cb
}
setInteractionGuards = (guard: MouseGuard) => {
this.interactionGuards = guard
// setMouseGuards is oun patch-package patch to orbit controls
// see patches/three+0.160.0.patch
;(this.controls as any).setMouseGuards(guard)
}
private createPerspectiveCamera = () => {
const { z_near, z_far } = calculateNearFarFromFOV(this.fov)
this.camera = new PerspectiveCamera(
this.fov,
window.innerWidth / window.innerHeight,
z_near,
z_far
)
this.camera.up.set(0, 0, 1)
this.camera.layers.enable(SKETCH_LAYER)
if (DEBUG_SHOW_INTERSECTION_PLANE)
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
return this.camera
}
setupOrbitControls = (target?: [number, number, number]): OrbitControls => {
if (this.controls) this.controls.dispose()
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
if (target) {
// if we're swapping from perspective to orthographic,
// we'll need to recreate the orbit controls
// and most likely want the target to be the same
this.controls.target.set(...target)
}
this.controls.update()
this.controls.addEventListener('change', this.onCameraChange)
// debounce is needed because the start and end events are fired too often for zoom on scroll
let debounceTimer = 0
const handleStart = () => {
if (debounceTimer) clearTimeout(debounceTimer)
this._isCamMovingCallback(true, false)
}
const handleEnd = () => {
debounceTimer = setTimeout(() => {
this._isCamMovingCallback(false, false)
}, 400) as any as number
}
this.controls.addEventListener('start', handleStart)
this.controls.addEventListener('end', handleEnd)
// setMouseGuards is oun patch-package patch to orbit controls
// see patches/three+0.160.0.patch
;(this.controls as any).setMouseGuards(this.interactionGuards)
return this.controls
}
onStreamStart = () => this.onCameraChange()
deferReactUpdate = throttle((a: ReactCameraProperties) => {
this.reactCameraPropertiesCallback(a)
}, 200)
onCameraChange = () => {
const scale = getSceneScale(this.camera, this.controls.target)
const scale = getSceneScale(
this.camControls.camera,
this.camControls.target
)
const planesGroup = this.scene.getObjectByName(DEFAULT_PLANES)
const axisGroup = this.scene
.getObjectByName(AXIS_GROUP)
?.getObjectByName('gridHelper')
planesGroup && planesGroup.scale.set(scale, scale, scale)
axisGroup?.name === 'gridHelper' && axisGroup.scale.set(scale, scale, scale)
throttledUpdateEngineCamera({
quaternion: this.camera.quaternion,
position: this.camera.position,
zoom: this.camera.zoom,
isPerspective: this.isPerspective,
target: this.controls.target,
})
this.deferReactUpdate({
type:
this.camera instanceof PerspectiveCamera
? 'perspective'
: 'orthographic',
[this.camera instanceof PerspectiveCamera ? 'fov' : 'zoom']:
this.camera instanceof PerspectiveCamera
? this.camera.fov
: this.camera.zoom,
position: [
roundOff(this.camera.position.x, 2),
roundOff(this.camera.position.y, 2),
roundOff(this.camera.position.z, 2),
],
quaternion: [
roundOff(this.camera.quaternion.x, 2),
roundOff(this.camera.quaternion.y, 2),
roundOff(this.camera.quaternion.z, 2),
roundOff(this.camera.quaternion.w, 2),
],
})
this._onCamChange()
}
onWindowResize = () => {
if (this.camera instanceof PerspectiveCamera) {
this.camera.aspect = window.innerWidth / window.innerHeight
} else if (this.camera instanceof OrthographicCamera) {
const aspect = window.innerWidth / window.innerHeight
this.camera.left = -ORTHOGRAPHIC_CAMERA_SIZE * aspect
this.camera.right = ORTHOGRAPHIC_CAMERA_SIZE * aspect
this.camera.top = ORTHOGRAPHIC_CAMERA_SIZE
this.camera.bottom = -ORTHOGRAPHIC_CAMERA_SIZE
}
this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight)
}
animate = () => {
requestAnimationFrame(this.animate)
TWEEN.update() // This will update all tweens during the animation loop
if (!this.isFovAnimationInProgress)
this.renderer.render(this.scene, this.camera)
}
async tweenCameraToQuaternion(
targetQuaternion: Quaternion,
duration = 500,
toOrthographic = true
): Promise<void> {
const isVertical = isQuaternionVertical(targetQuaternion)
let _duration = duration
if (isVertical) {
_duration = duration * 0.6
await this._tweenCameraToQuaternion(new Quaternion(), _duration, false)
}
await this._tweenCameraToQuaternion(
targetQuaternion,
_duration,
toOrthographic
)
}
_tweenCameraToQuaternion(
targetQuaternion: Quaternion,
duration = 500,
toOrthographic = false
): Promise<void> {
return new Promise((resolve) => {
const camera = this.camera
this._isCamMovingCallback(true, true)
const initialQuaternion = camera.quaternion.clone()
const isVertical = isQuaternionVertical(targetQuaternion)
let tweenEnd = isVertical ? 0.99 : 1
const controlsTarget = this.controls.target.clone()
const initialDistance = controlsTarget.distanceTo(camera.position.clone())
const cameraAtTime = (animationProgress: number /* 0 - 1 */) => {
const currentQ = tempQuaternion.slerpQuaternions(
initialQuaternion,
targetQuaternion,
animationProgress
)
if (this.camera instanceof PerspectiveCamera)
// changing the camera position back when it's orthographic doesn't do anything
// and it messes up animating back to perspective later
this.camera.position
.set(0, 0, 1)
.applyQuaternion(currentQ)
.multiplyScalar(initialDistance)
.add(controlsTarget)
this.camera.up.set(0, 1, 0).applyQuaternion(currentQ).normalize()
this.camera.quaternion.copy(currentQ)
this.controls.target.copy(controlsTarget)
this.controls.update()
this.camera.updateProjectionMatrix()
}
const onComplete = async () => {
if (isReducedMotion() && toOrthographic) {
cameraAtTime(0.99)
this.useOrthographicCamera()
} else if (toOrthographic) {
await this.animateToOrthographic()
}
if (isVertical) cameraAtTime(1)
this.camera.up.set(0, 0, 1)
this.controls.enableRotate = false
this._isCamMovingCallback(false, true)
resolve()
}
if (isReducedMotion()) {
onComplete()
return
}
new TWEEN.Tween({ t: 0 })
.to({ t: tweenEnd }, duration)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(({ t }) => cameraAtTime(t))
.onComplete(onComplete)
.start()
})
}
animateToOrthographic = () =>
new Promise((resolve) => {
this.isFovAnimationInProgress = true
let currentFov = this.fov
this.fovBeforeAnimate = this.fov
const targetFov = 4
const fovAnimationStep = (currentFov - targetFov) / FRAMES_TO_ANIMATE_IN
let frameWaitOnFinish = 10
const animateFovChange = () => {
if (this.camera instanceof PerspectiveCamera) {
if (this.camera.fov > targetFov) {
// Decrease the FOV
currentFov = Math.max(currentFov - fovAnimationStep, targetFov)
this.camera.updateProjectionMatrix()
this.dollyZoom(currentFov)
requestAnimationFrame(animateFovChange) // Continue the animation
} else if (frameWaitOnFinish > 0) {
frameWaitOnFinish--
requestAnimationFrame(animateFovChange) // Continue the animation
} else {
// Once the target FOV is reached, switch to the orthographic camera
// Needs to wait a couple frames after the FOV animation is complete
this.useOrthographicCamera()
this.isFovAnimationInProgress = false
resolve(true)
}
if (!this.isFovAnimationInProgress) {
// console.log('animation frame', this.cameraControls.camera)
this.camControls.update()
this.renderer.render(this.scene, this.camControls.camera)
}
}
animateFovChange() // Start the animation
})
animateToPerspective = () =>
new Promise((resolve) => {
this.isFovAnimationInProgress = true
// Immediately set the camera to perspective with a very low FOV
this.fov = 4
let currentFov = 4
this.camera.updateProjectionMatrix()
const targetFov = this.fovBeforeAnimate // Target FOV for perspective
const fovAnimationStep = (targetFov - currentFov) / FRAMES_TO_ANIMATE_IN
this.usePerspectiveCamera()
const animateFovChange = () => {
if (this.camera instanceof OrthographicCamera) return
if (this.camera.fov < targetFov) {
// Increase the FOV
currentFov = Math.min(currentFov + fovAnimationStep, targetFov)
// this.camera.fov = currentFov
this.camera.updateProjectionMatrix()
this.dollyZoom(currentFov)
requestAnimationFrame(animateFovChange) // Continue the animation
} else {
// Set the flag to false as the FOV animation is complete
this.isFovAnimationInProgress = false
resolve(true)
}
}
animateFovChange() // Start the animation
})
dispose = () => {
// Dispose of scene resources, renderer, and controls
this.renderer.dispose()
window.removeEventListener('resize', this.onWindowResize)
// Dispose of any other resources like geometries, materials, textures
}
useOrthographicCamera = () => {
this.isPerspective = false
const { x: px, y: py, z: pz } = this.camera.position
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
const { x: tx, y: ty, z: tz } = this.controls.target
const aspect = window.innerWidth / window.innerHeight
const { z_near, z_far } = calculateNearFarFromFOV(this.fov)
this.camera = new OrthographicCamera(
-ORTHOGRAPHIC_CAMERA_SIZE * aspect,
ORTHOGRAPHIC_CAMERA_SIZE * aspect,
ORTHOGRAPHIC_CAMERA_SIZE,
-ORTHOGRAPHIC_CAMERA_SIZE,
z_near,
z_far
)
this.camera.up.set(0, 0, 1)
this.camera.layers.enable(SKETCH_LAYER)
if (DEBUG_SHOW_INTERSECTION_PLANE)
this.camera.layers.enable(INTERSECTION_PLANE_LAYER)
this.camera.position.set(px, py, pz)
const distance = this.camera.position.distanceTo(new Vector3(tx, ty, tz))
const fovFactor = 45 / this.fov
this.camera.zoom = (ZOOM_MAGIC_NUMBER * fovFactor * 0.8) / distance
this.setupOrbitControls([tx, ty, tz])
this.camera.quaternion.set(qx, qy, qz, qw)
this.camera.updateProjectionMatrix()
this.controls.update()
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_orthographic',
},
})
}
usePerspectiveCamera = () => {
this.isPerspective = true
const { x: px, y: py, z: pz } = this.camera.position
const { x: qx, y: qy, z: qz, w: qw } = this.camera.quaternion
const { x: tx, y: ty, z: tz } = this.controls.target
const zoom = this.camera.zoom
this.camera = this.createPerspectiveCamera()
this.camera.position.set(px, py, pz)
this.camera.quaternion.set(qx, qy, qz, qw)
const zoomFudgeFactor = 2280
const distance = zoomFudgeFactor / (zoom * this.fov)
const direction = new Vector3().subVectors(
this.camera.position,
this.controls.target
)
direction.normalize()
this.camera.position
.copy(this.controls.target)
.addScaledVector(direction, distance)
this.setupOrbitControls([tx, ty, tz])
engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_set_perspective',
parameters: {
fov_y: this.camera.fov,
...calculateNearFarFromFOV(this.fov),
},
},
})
this.onCameraChange()
return this.camera
}
dollyZoom = (newFov: number) => {
if (!(this.camera instanceof PerspectiveCamera)) {
console.warn('Dolly zoom is only applicable to perspective cameras.')
return
}
this.fov = newFov
// Calculate the direction vector from the camera towards the controls target
const direction = new Vector3()
.subVectors(this.controls.target, this.camera.position)
.normalize()
// Calculate the distance to the controls target before changing the FOV
const distanceBefore = this.camera.position.distanceTo(this.controls.target)
// Calculate the scale factor for the new FOV compared to the old one
// This needs to be calculated before updating the camera's FOV
const oldFov = this.camera.fov
const viewHeightFactor = (fov: number) => {
/* *
/|
/ |
/ |
/ |
/ | viewHeight/2
/ |
/ |
/↙fov/2 |
/________|
\ |
\._._._.|
*/
return Math.tan(deg2Rad(fov / 2))
}
const scaleFactor = viewHeightFactor(oldFov) / viewHeightFactor(newFov)
this.camera.fov = newFov
this.camera.updateProjectionMatrix()
const distanceAfter = distanceBefore * scaleFactor
const newPosition = this.controls.target
.clone()
.add(direction.multiplyScalar(-distanceAfter))
this.camera.position.copy(newPosition)
const { z_near, z_far } = calculateNearFarFromFOV(this.fov)
this.camera.near = z_near
this.camera.far = z_far
throttledUpdateEngineFov({
fov: newFov,
position: newPosition,
quaternion: this.camera.quaternion,
zoom: this.camera.zoom,
target: this.controls.target,
})
}
getPlaneIntersectPoint = (): {
intersection2d?: Vector2
intersectPoint: Vector3
@ -769,7 +233,7 @@ class SceneInfra {
} | null => {
this.planeRaycaster.setFromCamera(
this.currentMouseVector,
sceneInfra.camera
sceneInfra.camControls.camera
)
const planeIntersects = this.planeRaycaster.intersectObjects(
this.scene.children,
@ -907,7 +371,7 @@ class SceneInfra {
}
// Check the center point
this.raycaster.setFromCamera(mouseDownVector, this.camera)
this.raycaster.setFromCamera(mouseDownVector, this.camControls.camera)
updateClosestIntersection(
this.raycaster.intersectObjects(this.scene.children, true)
)
@ -922,7 +386,7 @@ class SceneInfra {
mouseDownVector.x + offsetX,
mouseDownVector.y - offsetY
)
this.raycaster.setFromCamera(ringVector, this.camera)
this.raycaster.setFromCamera(ringVector, this.camControls.camera)
updateClosestIntersection(
this.raycaster.intersectObjects(this.scene.children, true)
)
@ -1015,7 +479,10 @@ class SceneInfra {
}
})
planesGroup.layers.enable(SKETCH_LAYER)
const sceneScale = getSceneScale(this.camera, this.controls.target)
const sceneScale = getSceneScale(
this.camControls.camera,
this.camControls.target
)
planesGroup.scale.set(sceneScale, sceneScale, sceneScale)
this.scene.add(planesGroup)
}
@ -1050,52 +517,6 @@ class SceneInfra {
export const sceneInfra = new SceneInfra()
function convertThreeCamValuesToEngineCam({
target,
position,
quaternion,
zoom,
isPerspective,
}: ThreeCamValues): {
center: Vector3
up: Vector3
vantage: Vector3
} {
// Something to consider is that the orbit controls have a target,
// we're kind of deriving the target/lookAtVector here when it might not be needed
// leaving for now since it's working but maybe revisit later
const euler = new Euler().setFromQuaternion(quaternion, 'XYZ')
const lookAtVector = new Vector3(0, 0, -1)
.applyEuler(euler)
.normalize()
.add(position)
const upVector = new Vector3(0, 1, 0).applyEuler(euler).normalize()
if (isPerspective) {
return {
center: target,
up: upVector,
vantage: position,
}
}
const zoomFactor = -ZOOM_MAGIC_NUMBER / zoom
const direction = lookAtVector.clone().sub(position).normalize()
const newVantage = position.clone().add(direction.multiplyScalar(zoomFactor))
return {
center: lookAtVector,
up: upVector,
vantage: newVantage,
}
}
function calculateNearFarFromFOV(fov: number) {
const nearFarRatio = (fov - 3) / (45 - 3)
// const z_near = 0.1 + nearFarRatio * (5 - 0.1)
const z_far = 1000 + nearFarRatio * (100000 - 1000)
return { z_near: 0.1, z_far }
}
export function getSceneScale(
camera: PerspectiveCamera | OrthographicCamera,
target: Vector3
@ -1131,12 +552,6 @@ function baseUnitTomm(baseUnit: BaseUnit) {
}
}
export function isQuaternionVertical(q: Quaternion) {
const v = new Vector3(0, 0, 1).applyQuaternion(q)
// no x or y components means it's vertical
return compareVec2Epsilon2([v.x, v.y], [0, 0])
}
export type DefaultPlane =
| 'xy-default-plane'
| 'xz-default-plane'

View File

@ -4,7 +4,7 @@ import { engineCommandManager } from 'lang/std/engineConnection'
import { throttle, isReducedMotion } from 'lib/utils'
const updateDollyZoom = throttle(
(newFov: number) => sceneInfra.dollyZoom(newFov),
(newFov: number) => sceneInfra.camControls.dollyZoom(newFov),
1000 / 15
)
@ -15,19 +15,19 @@ export const CamToggle = () => {
useEffect(() => {
engineCommandManager.waitForReady.then(async () => {
sceneInfra.dollyZoom(fov)
sceneInfra.camControls.dollyZoom(fov)
})
}, [])
const toggleCamera = () => {
if (isPerspective) {
isReducedMotion()
? sceneInfra.useOrthographicCamera()
: sceneInfra.animateToOrthographic()
? sceneInfra.camControls.useOrthographicCamera()
: sceneInfra.camControls.animateToOrthographic()
} else {
isReducedMotion()
? sceneInfra.usePerspectiveCamera()
: sceneInfra.animateToPerspective()
? sceneInfra.camControls.usePerspectiveCamera()
: sceneInfra.camControls.animateToPerspective()
}
setIsPerspective(!isPerspective)
}
@ -60,9 +60,9 @@ export const CamToggle = () => {
<button
onClick={() => {
if (enableRotate) {
sceneInfra.controls.enableRotate = false
sceneInfra.camControls.enableRotate = false
} else {
sceneInfra.controls.enableRotate = true
sceneInfra.camControls.enableRotate = true
}
setEnableRotate(!enableRotate)
}}

View File

@ -213,7 +213,7 @@ export const ModelingMachineProvider = ({
)
await kclManager.updateAst(modifiedAst, false)
const quaternion = getSketchQuaternion(pathToNode, normal)
await sceneInfra.tweenCameraToQuaternion(quaternion)
await sceneInfra.camControls.tweenCameraToQuaternion(quaternion)
return {
sketchPathToNode: pathToNode,
sketchNormalBackUp: normal,
@ -227,7 +227,7 @@ export const ModelingMachineProvider = ({
sketchPathToNode || [],
sketchNormalBackUp
)
await sceneInfra.tweenCameraToQuaternion(quaternion)
await sceneInfra.camControls.tweenCameraToQuaternion(quaternion)
},
'Get horizontal info': async ({
selectionRanges,

View File

@ -1017,7 +1017,7 @@ export class EngineCommandManager {
gizmo_mode: true,
},
})
sceneInfra.onStreamStart()
sceneInfra.camControls.onCameraChange()
this.initPlanes().then(() => {
executeCode(undefined, true)

View File

@ -39,30 +39,36 @@ export interface MouseGuard {
rotate: MouseGuardHandler
}
const butName = (e: React.MouseEvent) => ({
middle: !!(e.buttons & 4),
right: !!(e.buttons & 2),
left: !!(e.buttons & 1),
})
export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
KittyCAD: {
pan: {
description: 'Right click + Shift + drag or middle click + drag',
callback: (e) =>
(e.button === 1 && noModifiersPressed(e)) ||
(e.button === 2 && e.shiftKey),
(butName(e).middle && noModifiersPressed(e)) ||
(butName(e).right && e.shiftKey),
},
zoom: {
description: 'Scroll wheel or Right click + Ctrl + drag',
dragCallback: (e) => e.button === 2 && e.ctrlKey,
dragCallback: (e) => !!(e.buttons & 2) && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Right click + drag',
callback: (e) => e.button === 2 && noModifiersPressed(e),
callback: (e) => butName(e).right && noModifiersPressed(e),
},
},
OnShape: {
pan: {
description: 'Right click + Ctrl + drag or middle click + drag',
callback: (e) =>
(e.button === 2 && e.ctrlKey) ||
(e.button === 1 && noModifiersPressed(e)),
(butName(e).right && e.ctrlKey) ||
(butName(e).middle && noModifiersPressed(e)),
},
zoom: {
description: 'Scroll wheel',
@ -71,77 +77,77 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
},
rotate: {
description: 'Right click + drag',
callback: (e) => e.button === 2 && noModifiersPressed(e),
callback: (e) => butName(e).right && noModifiersPressed(e),
},
},
'Trackpad Friendly': {
pan: {
description: 'Left click + Alt + Shift + drag or middle click + drag',
callback: (e) =>
(e.button === 0 && e.altKey && e.shiftKey && !e.metaKey) ||
(e.button === 1 && noModifiersPressed(e)),
(butName(e).left && e.altKey && e.shiftKey && !e.metaKey) ||
(butName(e).middle && noModifiersPressed(e)),
},
zoom: {
description: 'Scroll wheel or Left click + Alt + OS + drag',
dragCallback: (e) => e.button === 0 && e.altKey && e.metaKey,
dragCallback: (e) => butName(e).left && e.altKey && e.metaKey,
scrollCallback: () => true,
},
rotate: {
description: 'Left click + Alt + drag',
callback: (e) => e.button === 0 && e.altKey && !e.shiftKey && !e.metaKey,
callback: (e) => butName(e).left && e.altKey && !e.shiftKey && !e.metaKey,
lenientDragStartButton: 0,
},
},
Solidworks: {
pan: {
description: 'Right click + Ctrl + drag',
callback: (e) => e.button === 2 && e.ctrlKey,
callback: (e) => butName(e).right && e.ctrlKey,
lenientDragStartButton: 2,
},
zoom: {
description: 'Scroll wheel or Middle click + Shift + drag',
dragCallback: (e) => e.button === 1 && e.shiftKey,
dragCallback: (e) => butName(e).middle && e.shiftKey,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + drag',
callback: (e) => e.button === 1 && noModifiersPressed(e),
callback: (e) => butName(e).middle && noModifiersPressed(e),
},
},
NX: {
pan: {
description: 'Middle click + Shift + drag',
callback: (e) => e.button === 1 && e.shiftKey,
callback: (e) => butName(e).middle && e.shiftKey,
},
zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag',
dragCallback: (e) => e.button === 1 && e.ctrlKey,
dragCallback: (e) => butName(e).middle && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + drag',
callback: (e) => e.button === 1 && noModifiersPressed(e),
callback: (e) => butName(e).middle && noModifiersPressed(e),
},
},
Creo: {
pan: {
description: 'Middle click + Shift + drag',
callback: (e) => e.button === 1 && e.shiftKey,
callback: (e) => butName(e).middle && e.shiftKey,
},
zoom: {
description: 'Scroll wheel or Middle click + Ctrl + drag',
dragCallback: (e) => e.button === 1 && e.ctrlKey,
dragCallback: (e) => butName(e).middle && e.ctrlKey,
scrollCallback: () => true,
},
rotate: {
description: 'Middle click + drag',
callback: (e) => e.button === 1 && noModifiersPressed(e),
callback: (e) => butName(e).middle && noModifiersPressed(e),
},
},
AutoCAD: {
pan: {
description: 'Middle click + drag',
callback: (e) => e.button === 1 && noModifiersPressed(e),
callback: (e) => butName(e).middle && noModifiersPressed(e),
},
zoom: {
description: 'Scroll wheel',
@ -150,7 +156,7 @@ export const cameraMouseDragGuards: Record<CameraSystem, MouseGuard> = {
},
rotate: {
description: 'Middle click + Shift + drag',
callback: (e) => e.button === 1 && e.shiftKey,
callback: (e) => butName(e).middle && e.shiftKey,
},
},
}

124
yarn.lock
View File

@ -2943,11 +2943,6 @@
dependencies:
"@xstate/machine-extractor" "^0.16.0"
"@yarnpkg/lockfile@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@ -3243,11 +3238,6 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
at-least-node@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
autoprefixer@^10.4.13:
version "10.4.14"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d"
@ -3670,7 +3660,7 @@ chromium-bidi@0.4.16:
dependencies:
mitt "3.0.0"
ci-info@^3.2.0, ci-info@^3.7.0:
ci-info@^3.2.0:
version "3.9.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4"
integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==
@ -4890,13 +4880,6 @@ find-up@^6.3.0:
locate-path "^7.1.0"
path-exists "^5.0.0"
find-yarn-workspace-root@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd"
integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==
dependencies:
micromatch "^4.0.2"
flat-cache@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
@ -4991,16 +4974,6 @@ fs-extra@^8.1.0:
jsonfile "^4.0.0"
universalify "^0.1.0"
fs-extra@^9.0.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d"
integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==
dependencies:
at-least-node "^1.0.0"
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
@ -5341,7 +5314,7 @@ got@^13.0.0:
p-cancelable "^3.0.0"
responselike "^3.0.0"
graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.9:
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.9:
version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
@ -5703,11 +5676,6 @@ is-date-object@^1.0.1, is-date-object@^1.0.5:
dependencies:
has-tostringtag "^1.0.0"
is-docker@^2.0.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@ -5870,13 +5838,6 @@ is-weakset@^2.0.1:
call-bind "^1.0.2"
get-intrinsic "^1.1.1"
is-wsl@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
dependencies:
is-docker "^2.0.0"
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
@ -6106,16 +6067,6 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json-stable-stringify@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz#52d4361b47d49168bcc4e564189a42e5a7439454"
integrity sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==
dependencies:
call-bind "^1.0.5"
isarray "^2.0.5"
jsonify "^0.0.1"
object-keys "^1.1.1"
json5@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
@ -6140,20 +6091,6 @@ jsonfile@^4.0.0:
optionalDependencies:
graceful-fs "^4.1.6"
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
dependencies:
universalify "^2.0.0"
optionalDependencies:
graceful-fs "^4.1.6"
jsonify@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978"
integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3:
version "3.3.5"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a"
@ -6171,13 +6108,6 @@ keyv@^4.5.3:
dependencies:
json-buffer "3.0.1"
klaw-sync@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c"
integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==
dependencies:
graceful-fs "^4.1.11"
ky@^0.33.0:
version "0.33.3"
resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543"
@ -6421,7 +6351,7 @@ meshoptimizer@~0.18.1:
resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz#cdb90907f30a7b5b1190facd3b7ee6b7087797d8"
integrity sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==
micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
micromatch@^4.0.4, micromatch@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
@ -6850,14 +6780,6 @@ onetime@^6.0.0:
dependencies:
mimic-fn "^4.0.0"
open@^7.4.2:
version "7.4.2"
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
dependencies:
is-docker "^2.0.0"
is-wsl "^2.1.1"
openapi-types@^12.0.0:
version "12.1.3"
resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3"
@ -7003,27 +6925,6 @@ parse-ms@^2.1.0:
resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==
patch-package@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.0.tgz#d191e2f1b6e06a4624a0116bcb88edd6714ede61"
integrity sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==
dependencies:
"@yarnpkg/lockfile" "^1.1.0"
chalk "^4.1.2"
ci-info "^3.7.0"
cross-spawn "^7.0.3"
find-yarn-workspace-root "^2.0.0"
fs-extra "^9.0.0"
json-stable-stringify "^1.0.2"
klaw-sync "^6.0.0"
minimist "^1.2.6"
open "^7.4.2"
rimraf "^2.6.3"
semver "^7.5.3"
slash "^2.0.0"
tmp "^0.0.33"
yaml "^2.2.2"
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@ -7721,7 +7622,7 @@ rgb2hex@0.2.5:
resolved "https://registry.yarnpkg.com/rgb2hex/-/rgb2hex-0.2.5.tgz#f82230cd3ab1364fa73c99be3a691ed688f8dbdc"
integrity sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==
rimraf@2, rimraf@^2.6.3:
rimraf@2:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@ -7839,7 +7740,7 @@ semver@^6.3.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.3.5, semver@^7.3.7, semver@^7.5.3:
semver@^7.3.5, semver@^7.3.7:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
@ -7917,11 +7818,6 @@ sketch-helpers@^0.0.4:
resolved "https://registry.yarnpkg.com/sketch-helpers/-/sketch-helpers-0.0.4.tgz#c6e4257451cd65483ab99ff7d3b10da04e98374d"
integrity sha512-xSt+Ku4VFDk4fBW3kRj+raZ49fFSJ32q1ph05GKQvZ9mIUI+W2/3iJJSBfBWwIdxlNiMx6RoUe2O+5vwtkPT3A==
slash@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
@ -8587,11 +8483,6 @@ universalify@^0.1.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
universalify@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
unzipper@^0.10.14:
version "0.10.14"
resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.10.14.tgz#d2b33c977714da0fbc0f82774ad35470a7c962b1"
@ -9084,11 +8975,6 @@ yaml@^2.1.1:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
yaml@^2.2.2:
version "2.3.4"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"
integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==
yargs-parser@20.2.4:
version "20.2.4"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"