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:
@ -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, '')
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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 ) {
|
||||
|
900
src/clientSideScene/CameraControls.ts
Normal file
900
src/clientSideScene/CameraControls.ts
Normal 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
|
||||
}
|
@ -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],
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Quaternion } from 'three'
|
||||
import { isQuaternionVertical } from './sceneInfra'
|
||||
import { isQuaternionVertical } from './helpers'
|
||||
|
||||
describe('isQuaternionVertical', () => {
|
||||
it('should identify vertical quaternions', () => {
|
@ -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])
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
if (!this.isFovAnimationInProgress) {
|
||||
// console.log('animation frame', this.cameraControls.camera)
|
||||
this.camControls.update()
|
||||
this.renderer.render(this.scene, this.camControls.camera)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
|
@ -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)
|
||||
}}
|
||||
|
@ -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,
|
||||
|
@ -1017,7 +1017,7 @@ export class EngineCommandManager {
|
||||
gizmo_mode: true,
|
||||
},
|
||||
})
|
||||
sceneInfra.onStreamStart()
|
||||
sceneInfra.camControls.onCameraChange()
|
||||
|
||||
this.initPlanes().then(() => {
|
||||
executeCode(undefined, true)
|
||||
|
@ -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
124
yarn.lock
@ -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"
|
||||
|
Reference in New Issue
Block a user