From d012877e8a3ea4efc783a67ad8658fe526a229ca Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Thu, 21 Nov 2024 12:11:57 +0200 Subject: [PATCH 01/28] Updated the fly controller to use the z up basis --- packages/viewer-sandbox/src/main.ts | 5 +++-- .../modules/extensions/HybridCameraController.ts | 1 + .../src/modules/extensions/controls/FlyControls.ts | 14 +++++++------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index b8e50f6775..445d5c8f9d 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -5,7 +5,8 @@ import { ViewerEvent, Viewer, CameraController, - ViewModes + ViewModes, + HybridCameraController } from '@speckle/viewer' import './style.css' @@ -43,7 +44,7 @@ const createViewer = async (containerName: string, stream: string) => { const viewer: Viewer = new Viewer(container, params) await viewer.init() - const cameraController = viewer.createExtension(CameraController) + const cameraController = viewer.createExtension(HybridCameraController) const selection = viewer.createExtension(SelectionExtension) const sections = viewer.createExtension(SectionTool) viewer.createExtension(SectionOutlines) diff --git a/packages/viewer/src/modules/extensions/HybridCameraController.ts b/packages/viewer/src/modules/extensions/HybridCameraController.ts index b8ebf500c9..914d039fa5 100644 --- a/packages/viewer/src/modules/extensions/HybridCameraController.ts +++ b/packages/viewer/src/modules/extensions/HybridCameraController.ts @@ -15,6 +15,7 @@ export class HybridCameraController extends CameraController { public constructor(viewer: IViewer) { super(viewer) document.addEventListener('keydown', this.onKeyDown.bind(this)) + document.addEventListener('keyup', this.onKeyUp.bind(this)) } protected onKeyDown(event: KeyboardEvent) { diff --git a/packages/viewer/src/modules/extensions/controls/FlyControls.ts b/packages/viewer/src/modules/extensions/controls/FlyControls.ts index cc4fa8ee81..5e630ae8c0 100644 --- a/packages/viewer/src/modules/extensions/controls/FlyControls.ts +++ b/packages/viewer/src/modules/extensions/controls/FlyControls.ts @@ -192,14 +192,14 @@ class FlyControls extends SpeckleControls { this.goalPosition.copy(pos) } - /** The input position and target will be in a basis with (0,1,0) as up */ + /** The input position and target will be in a basis with (0,0,1) as up */ public fromPositionAndTarget(position: Vector3, target: Vector3): void { const cPos = this.getPosition() const cTarget = this.getTarget() if (cPos.equals(position) && cTarget.equals(target)) return - const tPosition = new Vector3().copy(position).applyMatrix4(this._basisTransform) - const tTarget = new Vector3().copy(target).applyMatrix4(this._basisTransform) + const tPosition = new Vector3().copy(position) + const tTarget = new Vector3().copy(target) const matrix = new Matrix4() .lookAt(tPosition, tTarget, this._up) .premultiply(this._basisTransformInv) @@ -208,7 +208,7 @@ class FlyControls extends SpeckleControls { this.goalPosition.copy(tPosition) } - /** The returned vector needs to be in a basis with (0,1,0) as up */ + /** The returned vector needs to be in a basis with (0,0,1) as up */ public getTarget(): Vector3 { const target = new Vector3().copy(this.goalPosition) const matrix = new Matrix4().makeRotationFromEuler(this.goalEuler) @@ -217,12 +217,12 @@ class FlyControls extends SpeckleControls { .applyMatrix4(this._basisTransform) .normalize() target.addScaledVector(forward, -this.world.getRelativeOffset(0.2)) - return target.applyMatrix4(this._basisTransformInv) + return target } - /** The returned vector needs to be in a basis with (0,1,0) as up */ + /** The returned vector needs to be in a basis with (0,0,1) as up */ public getPosition(): Vector3 { - return new Vector3().copy(this.goalPosition).applyMatrix4(this._basisTransformInv) + return new Vector3().copy(this.goalPosition) } /** From e3a20bd9ce66391218f546899df7e10715aebabe Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Thu, 21 Nov 2024 13:03:23 +0200 Subject: [PATCH 02/28] Fixed very important compiler error --- packages/viewer-sandbox/src/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 445d5c8f9d..a9c6157c30 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -4,7 +4,6 @@ import { SelectionEvent, ViewerEvent, Viewer, - CameraController, ViewModes, HybridCameraController } from '@speckle/viewer' From 83063b048dbc2c62ce27af76a93997868986b6d6 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Thu, 21 Nov 2024 17:30:42 +0200 Subject: [PATCH 03/28] Removed the annoying delay when first holding down WASD keys before movement started --- packages/viewer/src/modules/Viewer.ts | 2 +- .../modules/extensions/CameraController.ts | 4 ++-- .../extensions/controls/FlyControls.ts | 22 ++++++++++++------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/viewer/src/modules/Viewer.ts b/packages/viewer/src/modules/Viewer.ts index 47a3f2954a..c814436439 100644 --- a/packages/viewer/src/modules/Viewer.ts +++ b/packages/viewer/src/modules/Viewer.ts @@ -204,7 +204,7 @@ export class Viewer extends EventEmitter implements IViewer { } private update() { - const delta = this.clock.getDelta() + const delta = this.clock.getDelta() * 1000 // turn to miliseconds const extensions = Object.values(this.extensions) extensions.forEach((ext: Extension) => { ext.onEarlyUpdate(delta) diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index 2f7adf2f45..ed18eed5ed 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -295,8 +295,8 @@ export class CameraController extends Extension implements SpeckleCamera { this.emit(CameraEvent.Dynamic) } - public onEarlyUpdate() { - const changed = this._activeControls.update() + public onEarlyUpdate(_delta?: number) { + const changed = this._activeControls.update(_delta) if (changed !== this._lastCameraChanged) { this.emit(changed ? CameraEvent.Dynamic : CameraEvent.Stationary) } diff --git a/packages/viewer/src/modules/extensions/controls/FlyControls.ts b/packages/viewer/src/modules/extensions/controls/FlyControls.ts index 5e630ae8c0..ebe8ef0623 100644 --- a/packages/viewer/src/modules/extensions/controls/FlyControls.ts +++ b/packages/viewer/src/modules/extensions/controls/FlyControls.ts @@ -63,8 +63,6 @@ class FlyControls extends SpeckleControls { } public set enabled(value: boolean) { - if (value) this.connect() - else this.disconnect() this._enabled = value } @@ -109,6 +107,8 @@ class FlyControls extends SpeckleControls { this.container = container this.world = world this._options = Object.assign({}, options) + + this.connect() } public isStationary(): boolean { @@ -123,9 +123,12 @@ class FlyControls extends SpeckleControls { const now = performance.now() delta = delta !== undefined ? delta : now - this._lastTick this._lastTick = now - const deltaSeconds = delta / 1000 + if (!this._enabled) return false + + const deltaSeconds = delta / 1000 const scaledWalkingSpeed = this.world.getRelativeOffset(0.2) * walkingSpeed + if (this.keyMap.forward) this.velocity.z = -scaledWalkingSpeed * this._options.moveSpeed * deltaSeconds if (this.keyMap.back) @@ -145,9 +148,14 @@ class FlyControls extends SpeckleControls { if (!this.keyMap.down && !this.keyMap.up) this.velocity.y = 0 if (this.isStationary()) return false - this.moveBy(this.velocity) + this.updatePositionRotation(delta) + + return true + } + + protected updatePositionRotation(delta: number) { const diagonal = this.world.worldBox.min.distanceTo(this.world.worldBox.max) const minMaxRange = diagonal < 1 ? diagonal : 1 this.position.x = this.positionXDamper.update( @@ -175,12 +183,10 @@ class FlyControls extends SpeckleControls { this.rotate(this.euler) this._targetCamera.position.copy(this.position) - - return true } public jumpToGoal(): void { - this.update(SETTLING_TIME) + this.updatePositionRotation(SETTLING_TIME) } public fitToSphere(sphere: Sphere): void { @@ -292,7 +298,7 @@ class FlyControls extends SpeckleControls { // event listeners protected onMouseMove = (event: PointerEvent) => { - if (event.buttons !== 1) return + if (event.buttons !== 1 || !this._enabled) return const movementX = event.movementX || 0 const movementY = event.movementY || 0 From 59ae40d4aadbca9e6bdb062f5d050c0740b59f54 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Fri, 22 Nov 2024 17:15:55 +0200 Subject: [PATCH 04/28] Updated LegacyViewer to use the hybrid camera controls --- packages/viewer/src/modules/LegacyViewer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/viewer/src/modules/LegacyViewer.ts b/packages/viewer/src/modules/LegacyViewer.ts index c9c6b2f177..4af3f9a499 100644 --- a/packages/viewer/src/modules/LegacyViewer.ts +++ b/packages/viewer/src/modules/LegacyViewer.ts @@ -44,6 +44,7 @@ import { BatchObject } from './batching/BatchObject.js' import { SpeckleLoader } from './loaders/Speckle/SpeckleLoader.js' import Logger from './utils/Logger.js' import { ViewModes } from './extensions/ViewModes.js' +import { HybridCameraController } from './extensions/HybridCameraController.js' class LegacySelectionExtension extends SelectionExtension { /** FE2 'manually' selects objects pon it's own, so we're disabling the extension's event handler @@ -120,7 +121,7 @@ export class LegacyViewer extends Viewer { params: ViewerParams = DefaultViewerParams ) { super(container, params) - this.cameraController = this.createExtension(CameraController) + this.cameraController = this.createExtension(HybridCameraController) this.selection = this.createExtension(LegacySelectionExtension) this.sections = this.createExtension(SectionTool) this.createExtension(SectionOutlines) From d1da6a7ee606b350a3c5d2e903440b98d469dde1 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Tue, 26 Nov 2024 13:23:40 +0200 Subject: [PATCH 05/28] Added big baker --- packages/viewer-sandbox/src/main.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index a9c6157c30..a3f97cece6 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -450,6 +450,9 @@ const getStream = () => { // Perfectly flat // 'https://app.speckle.systems/projects/344f803f81/models/5582ab673e' + + // big baker + // 'https://latest.speckle.systems/projects/126cd4b7bb/models/032d09f716' ) } From 0ad6c4466f87b705245ae667a312b03a6f00dd66 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Wed, 11 Dec 2024 10:34:33 +0200 Subject: [PATCH 06/28] Trying to figure out the essence of this --- packages/viewer-sandbox/src/main.ts | 12 +- .../controls/SmoothOrbitControls.ts | 275 ++++++++++++++++-- 2 files changed, 265 insertions(+), 22 deletions(-) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index a3f97cece6..eda7e8e990 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -5,7 +5,8 @@ import { ViewerEvent, Viewer, ViewModes, - HybridCameraController + HybridCameraController, + CameraController } from '@speckle/viewer' import './style.css' @@ -44,7 +45,7 @@ const createViewer = async (containerName: string, stream: string) => { await viewer.init() const cameraController = viewer.createExtension(HybridCameraController) - const selection = viewer.createExtension(SelectionExtension) + // const selection = viewer.createExtension(SelectionExtension) const sections = viewer.createExtension(SectionTool) viewer.createExtension(SectionOutlines) const measurements = viewer.createExtension(MeasurementsExtension) @@ -56,7 +57,7 @@ const createViewer = async (containerName: string, stream: string) => { // const boxSelect = viewer.createExtension(BoxSelection) // const rotateCamera = viewer.createExtension(RotateCamera) cameraController // use it - selection // use it + // selection // use it sections // use it measurements // use it filtering // use it @@ -80,6 +81,7 @@ const createViewer = async (containerName: string, stream: string) => { Object.assign(sandbox.sceneParams.worldSize, viewer.World.worldSize) Object.assign(sandbox.sceneParams.worldOrigin, viewer.World.worldOrigin) sandbox.refresh() + viewer.getExtension(CameraController).setCameraView('front', false) }) viewer.on(ViewerEvent.UnloadComplete, () => { @@ -108,7 +110,7 @@ const getStream = () => { // prettier-ignore // 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D' // Revit sample house (good for bim-like stuff with many display meshes) - 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' + // 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' // 'https://latest.speckle.systems/streams/c1faab5c62/commits/ab1a1ab2b6' // 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' // 'https://latest.speckle.systems/streams/58b5648c4d/commits/60371ecb2d' @@ -182,7 +184,7 @@ const getStream = () => { // Alex cubes // 'https://latest.speckle.systems/streams/4658eb53b9/commits/d8ec9cccf7' // Alex more cubes - // 'https://latest.speckle.systems/streams/4658eb53b9/commits/31a8d5ff2b' + 'https://latest.speckle.systems/streams/4658eb53b9/commits/31a8d5ff2b' // Tekla // 'https://latest.speckle.systems/streams/caec6d6676/commits/588c731104' // Purple market square diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index ed254e6bbe..274d7f2c2b 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ /* @license * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -24,7 +25,11 @@ import { OrthographicCamera, Quaternion, Euler, - Scene + Scene, + Mesh, + MeshBasicMaterial, + SphereGeometry, + Object3D } from 'three' import { Damper, SETTLING_TIME } from '../../utils/Damper.js' @@ -34,6 +39,8 @@ import { SpeckleControls } from './SpeckleControls.js' import { Intersections } from '../../Intersections.js' import { lerp } from 'three/src/math/MathUtils.js' import { computeOrthographicSize } from '../CameraController.js' +import { ObjectLayers } from '../../../IViewer.js' +import { multiply } from 'lodash-es' /** * @param {Number} value @@ -167,6 +174,13 @@ export class SmoothOrbitControls extends SpeckleControls { private world: World private intersections: Intersections + private orbitSphere: Mesh + private originSphere: Mesh + private pivotPoint: Vector3 = new Vector3() + private lastCameraPos: Vector3 + + private forceUpdate = false + public get enabled(): boolean { return this._enabled } @@ -206,8 +220,20 @@ export class SmoothOrbitControls extends SpeckleControls { this.scene = scene this._options = Object.assign({}, options) this.setDamperDecayTime(this._options.damperDecay) - this.scene - this.intersections + + this.orbitSphere = new Mesh( + new SphereGeometry(0.5, 32, 16), + new MeshBasicMaterial({ color: 0xff00000 }) + ) + this.orbitSphere.layers.set(ObjectLayers.OVERLAY) + this.scene.add(this.orbitSphere) + + this.originSphere = new Mesh( + new SphereGeometry(0.5, 32, 16), + new MeshBasicMaterial({ color: 0x00ff00 }) + ) + this.originSphere.layers.set(ObjectLayers.OVERLAY) + this.scene.add(this.originSphere) } /** @@ -285,7 +311,8 @@ export class SmoothOrbitControls extends SpeckleControls { this.goalSpherical.phi === this.spherical.phi && this.goalSpherical.radius === this.spherical.radius && this.goalLogFov === this.logFov && - this.goalOrigin.equals(this.origin) + this.goalOrigin.equals(this.origin) && + !this.forceUpdate ) } @@ -592,18 +619,206 @@ export class SmoothOrbitControls extends SpeckleControls { normalization ) this.origin.set(x, y, z) - + const v = new Vector3().set(x, y, z) + this.originSphere.position.copy(v.applyMatrix4(this._basisTransform)) this.moveCamera() + this.forceUpdate = false return true } + protected transformTo(outParentToTarget: Matrix4, parent: Matrix4, target: Matrix4) { + outParentToTarget.copy(parent) + outParentToTarget.invert() + outParentToTarget.multiply(target) + } + + protected rotateAboutPoint( + obj: Object3D, + point: Vector3, + axis: Vector3, + theta: number + ) { + obj.position.sub(point) // remove the offset + obj.position.applyAxisAngle(axis, theta) // rotate the POSITION + obj.position.add(point) // re-add the offset + + obj.rotateOnAxis(axis, theta) // rotate the OBJECT + } + + protected hereToThere = new Vector3() + protected hereToThereMat = new Matrix4() + protected lastPivot = new Vector3() + protected relativeCamPos = new Vector3() + + protected getPivotTransform(pivot: Vector3, quaternion: Quaternion) { + const translateToOrigin = new Matrix4().makeTranslation( + -pivot.x, + -pivot.y, + -pivot.z + ) + const translateBack = new Matrix4().makeTranslation(pivot.x, pivot.y, pivot.z) + + // Rotation matrix from quaternion + const rotationMatrix = new Matrix4().makeRotationFromQuaternion(quaternion) + + // Combine the matrices + return new Matrix4() + .multiply(translateBack) // Translate back to the pivot + .multiply(rotationMatrix) // Apply rotation + .multiply(translateToOrigin) // Translate pivot to origin + // .multiply(new Matrix4().makeScale(1, 1, this.spherical.radius)) + } + + protected change = false protected moveCamera() { + const pivotPoint = new Vector3() + .copy(this.pivotPoint) + .applyMatrix4(this._basisTransformInv) + const prevPivotPoint = new Vector3() + .copy(this.lastPivot) + .applyMatrix4(this._basisTransformInv) + const camPos = new Vector3() + .copy(this._targetCamera.position) + .applyMatrix4(this._basisTransformInv) + // const position = new Vector3(0, 50, this.spherical.radius) + // const quaternion = new Quaternion().setFromEuler( + // new Euler(0, this.spherical.theta, 0) + // ) + + // position.sub(pivotPoint) + // position.applyQuaternion(quaternion) + // position.add(pivotPoint) + + // this._targetCamera.position.copy(position) + // this._targetCamera.quaternion.copy(quaternion) + + // this._targetCamera.position.set(0, 50, this.spherical.radius) + // this._targetCamera.quaternion.identity() + // this._targetCamera.updateMatrixWorld(true) + + // this.rotateAboutPoint( + // this._targetCamera, + // pivotPoint, + // new Vector3(0, 1, 0), + // this.spherical.theta + // ) + + // // Step 1: Calculate the camera's position relative to the pivot using spherical coordinates + // const cameraOffset = new Vector3(0, 50, this.spherical.radius) // Camera offset (distance and height) + // const rotationMatrix = new Matrix4().makeRotationFromEuler( + // new Euler(0, this.spherical.theta, 0) + // ) // Apply yaw rotation + + // cameraOffset.sub(pivotPoint) + // cameraOffset.applyMatrix4(rotationMatrix) // Apply the rotation + // cameraOffset.add(pivotPoint) + // this._targetCamera.position.copy(cameraOffset) // Set the camera position relative to the pivot + + // // Step 2: Update the camera orientation so it faces away from the pivot point + // const direction = new Vector3() + // .subVectors(this._targetCamera.position, pivotPoint) + // .normalize() // Camera direction from pivot + // const up = new Vector3(0, 1, 0) // Keep the 'up' vector stable + // const right = new Vector3().crossVectors(up, direction).normalize() // Right vector (cross product of up and direction) + // const newUp = new Vector3().crossVectors(direction, right).normalize() // Recompute up vector based on new direction + + // // Step 3: Create a new quaternion based on the updated right, up, and direction vectors + // const rotationMatrixFinal = new Matrix4().makeBasis(right, newUp, direction) + // this._targetCamera.quaternion.setFromRotationMatrix(rotationMatrixFinal) + + // this._targetCamera.position.applyMatrix4(this._basisTransform) + // this._targetCamera.quaternion.premultiply( + // new Quaternion().setFromRotationMatrix(this._basisTransform) + // ) + // Derive the new camera position from the updated spherical: this.spherical.makeSafe() - const position = this.positionFromSpherical(this.spherical, this.origin) + + // // Compute direction vector: from camera to target (origin) + // const pos = new Vector3().copy(this._targetCamera.position) + // const direction = new Vector3().subVectors(pos, new Vector3(0, 0, 0)).normalize() + + // // Compute a "right" vector using cross product with world up (0, 1, 0) + // const worldUp = new Vector3(0, 1, 0) + // const right = new Vector3().crossVectors(worldUp, direction).normalize() + + // // Recompute the up vector to ensure orthogonality + // const up = new Vector3().crossVectors(direction, right).normalize() + + // // Construct the rotation matrix using the orthogonal basis vectors + // const rotationMatrix = new Matrix4().makeBasis(right, up, direction.negate()) + + // // Extract the quaternion from the rotation matrix + // const fakeQuaternion = new Quaternion().setFromRotationMatrix(rotationMatrix) + // // const sphericalPosition = this.positionFromSpherical(this.spherical, this.origin) + const quaternion = this.quaternionFromSpherical(this.spherical) + const position = new Vector3() + const tPivot = this.getPivotTransform(pivotPoint, quaternion) + const invTPivot = new Matrix4().copy(tPivot).invert() + + const deltaPivot = new Vector3().copy(pivotPoint).sub(prevPivotPoint) + + // const tPrevPivot = this.getPivotTransform(prevPivotPoint, quaternion) + // const tPrevPointInv = new Matrix4().copy(tPrevPivot).invert() + // const objectToPrevPiot = new Vector3().copy(camPos).sub(prevPivotPoint) + // const objectToNewPivot = new Vector3().copy(camPos).sub(pivotPoint) + // const offset = new Vector3().copy(objectToPrevPiot).sub(objectToNewPivot) + const delta = new Vector3() + if (deltaPivot.length() > 0) { + /** New pos */ + const newPos = new Vector3() + newPos.sub(pivotPoint) + newPos.applyQuaternion(quaternion) + newPos.add(pivotPoint) + + const dir = new Vector3().setFromMatrixColumn( + new Matrix4().makeRotationFromQuaternion(quaternion), + 2 + ) + dir.multiplyScalar(this.spherical.radius) + newPos.add(dir) + + /** Old Pos */ + const oldPos = new Vector3() + oldPos.sub(prevPivotPoint) + oldPos.applyQuaternion(quaternion) + oldPos.add(prevPivotPoint) + + const dir2 = new Vector3().setFromMatrixColumn( + new Matrix4().makeRotationFromQuaternion(quaternion), + 2 + ) + dir2.multiplyScalar(this.spherical.radius) + oldPos.add(dir2) + delta.copy(oldPos.sub(newPos)) + console.warn('Delta -> ', delta) + console.warn('Delta pivot -> ', deltaPivot) + } + position.copy(new Vector3(0, 0, 0)) + position.sub(pivotPoint) + position.applyQuaternion(quaternion) + position.add(pivotPoint) + + const dir = new Vector3().setFromMatrixColumn( + new Matrix4().makeRotationFromQuaternion(quaternion), + 2 + ) + dir.multiplyScalar(this.spherical.radius) + position.add(dir) + position.sub(delta) + + // } else { + // position.copy(this.positionFromSpherical(this.spherical, this.origin)) + // } + + position.applyQuaternion( + new Quaternion().setFromRotationMatrix(this._basisTransform) + ) + quaternion.premultiply(new Quaternion().setFromRotationMatrix(this._basisTransform)) + if (this._targetCamera instanceof OrthographicCamera) { const cameraDirection = new Vector3() .setFromSpherical(this.spherical) @@ -619,7 +834,6 @@ export class SmoothOrbitControls extends SpeckleControls { } this._targetCamera.position.copy(position) this._targetCamera.quaternion.copy(quaternion) - if (this._targetCamera instanceof PerspectiveCamera) if (this._targetCamera.fov !== Math.exp(this.logFov)) { this._targetCamera.fov = Math.exp(this.logFov) @@ -638,6 +852,8 @@ export class SmoothOrbitControls extends SpeckleControls { this._targetCamera.bottom = orthographicSize.y / -2 this._targetCamera.updateProjectionMatrix() } + + this.lastPivot.copy(this.pivotPoint) } /* Ortho height to distance functions @@ -654,9 +870,6 @@ export class SmoothOrbitControls extends SpeckleControls { position.setFromSpherical(spherical) if (origin) position.add(origin) - position.applyQuaternion( - new Quaternion().setFromRotationMatrix(this._basisTransform) - ) return position } @@ -666,7 +879,7 @@ export class SmoothOrbitControls extends SpeckleControls { quaternion.setFromEuler( new Euler(spherical.phi - Math.PI / 2, spherical.theta, 0, 'YXZ') ) - quaternion.premultiply(new Quaternion().setFromRotationMatrix(this._basisTransform)) + return quaternion } @@ -836,6 +1049,40 @@ export class SmoothOrbitControls extends SpeckleControls { } protected onPointerDown = (event: PointerEvent) => { + const x = + ((event.clientX - this._container.offsetLeft) / this._container.offsetWidth) * 2 - + 1 + + const y = + ((event.clientY - this._container.offsetTop) / this._container.offsetHeight) * + -2 + + 1 + const res = this.intersections.intersect( + this.scene, + this._targetCamera as PerspectiveCamera, + new Vector2(x, y), + ObjectLayers.STREAM_CONTENT_MESH, + true, + this.world.worldBox, + true, + false + ) + if (res) { + this.lastPivot.copy(this.pivotPoint) + this.pivotPoint.copy(res[0].point) + this.orbitSphere.position.copy(res[0].point) + // console.log('Radius -> ', this.spherical.radius) + // console.log('Distance -> ', this._targetCamera.position.distanceTo(res[0].point)) + // this.goalSpherical.radius += + // this.goalSpherical.radius - this._targetCamera.position.distanceTo(res[0].point) + + this.hereToThere + .copy(this._targetCamera.position) + .applyMatrix4(this._basisTransformInv) + this.forceUpdate = true + this.change = true + } + if (this.pointers.length > 2) { return } @@ -849,11 +1096,6 @@ export class SmoothOrbitControls extends SpeckleControls { this.startPointerPosition.clientY = event.clientY } - // try { - // this._container.setPointerCapture(event.pointerId) - // } catch (e) { - // e - // } this.pointers.push({ clientX: event.clientX, clientY: event.clientY, @@ -971,7 +1213,6 @@ export class SmoothOrbitControls extends SpeckleControls { (event.button === 2 || event.ctrlKey || event.metaKey || event.shiftKey) ) { this.initializePan() - // ;(this.scene.element as any)[$panElement].style.opacity = 1 } // this.element.style.cursor = 'grabbing' } From d3d6d5205b0018f340a2136d78da0a517e3b30fd Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Wed, 11 Dec 2024 23:51:30 +0200 Subject: [PATCH 07/28] Partly works --- .../controls/SmoothOrbitControls.ts | 64 +++++++------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index 274d7f2c2b..d4fa0c0b4a 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -650,6 +650,9 @@ export class SmoothOrbitControls extends SpeckleControls { protected hereToThereMat = new Matrix4() protected lastPivot = new Vector3() protected relativeCamPos = new Vector3() + protected lastInitialPosition = new Vector3() + protected lastQuat = new Quaternion() + protected lastDelta = new Vector3() protected getPivotTransform(pivot: Vector3, quaternion: Quaternion) { const translateToOrigin = new Matrix4().makeTranslation( @@ -759,60 +762,36 @@ export class SmoothOrbitControls extends SpeckleControls { const tPivot = this.getPivotTransform(pivotPoint, quaternion) const invTPivot = new Matrix4().copy(tPivot).invert() - const deltaPivot = new Vector3().copy(pivotPoint).sub(prevPivotPoint) - + const deltaPivot = new Vector3().copy(prevPivotPoint).sub(pivotPoint) + // console.log('Pivot -> ', pivotPoint) // const tPrevPivot = this.getPivotTransform(prevPivotPoint, quaternion) // const tPrevPointInv = new Matrix4().copy(tPrevPivot).invert() // const objectToPrevPiot = new Vector3().copy(camPos).sub(prevPivotPoint) // const objectToNewPivot = new Vector3().copy(camPos).sub(pivotPoint) // const offset = new Vector3().copy(objectToPrevPiot).sub(objectToNewPivot) - const delta = new Vector3() + const dir = new Vector3().setFromMatrixColumn( + new Matrix4().makeRotationFromQuaternion(quaternion), + 2 + ) + dir.multiplyScalar(this.spherical.radius) + if (deltaPivot.length() > 0) { - /** New pos */ - const newPos = new Vector3() - newPos.sub(pivotPoint) - newPos.applyQuaternion(quaternion) - newPos.add(pivotPoint) - - const dir = new Vector3().setFromMatrixColumn( - new Matrix4().makeRotationFromQuaternion(quaternion), - 2 + this.lastDelta.copy(pivotPoint) + this.lastDelta.add( + new Vector3() + .copy(pivotPoint) + .negate() + .applyQuaternion(new Quaternion().copy(quaternion).invert()) ) - dir.multiplyScalar(this.spherical.radius) - newPos.add(dir) - - /** Old Pos */ - const oldPos = new Vector3() - oldPos.sub(prevPivotPoint) - oldPos.applyQuaternion(quaternion) - oldPos.add(prevPivotPoint) - - const dir2 = new Vector3().setFromMatrixColumn( - new Matrix4().makeRotationFromQuaternion(quaternion), - 2 - ) - dir2.multiplyScalar(this.spherical.radius) - oldPos.add(dir2) - delta.copy(oldPos.sub(newPos)) - console.warn('Delta -> ', delta) - console.warn('Delta pivot -> ', deltaPivot) + this.lastInitialPosition.copy(this.lastDelta) } - position.copy(new Vector3(0, 0, 0)) + + console.warn(this.lastInitialPosition) + position.copy(this.lastInitialPosition) position.sub(pivotPoint) position.applyQuaternion(quaternion) position.add(pivotPoint) - - const dir = new Vector3().setFromMatrixColumn( - new Matrix4().makeRotationFromQuaternion(quaternion), - 2 - ) - dir.multiplyScalar(this.spherical.radius) position.add(dir) - position.sub(delta) - - // } else { - // position.copy(this.positionFromSpherical(this.spherical, this.origin)) - // } position.applyQuaternion( new Quaternion().setFromRotationMatrix(this._basisTransform) @@ -834,6 +813,7 @@ export class SmoothOrbitControls extends SpeckleControls { } this._targetCamera.position.copy(position) this._targetCamera.quaternion.copy(quaternion) + this._targetCamera.updateMatrixWorld(true) if (this._targetCamera instanceof PerspectiveCamera) if (this._targetCamera.fov !== Math.exp(this.logFov)) { this._targetCamera.fov = Math.exp(this.logFov) From dabeefc824efc7288e35a2d799261fa77e4a6464 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Wed, 18 Dec 2024 14:16:58 +0200 Subject: [PATCH 08/28] Pivotal coordinates now work --- packages/viewer-sandbox/src/main.ts | 4 +- .../extensions/controls/PivotalControls.ts | 53 +++++ .../controls/SmoothOrbitControls.ts | 204 ++++++++---------- 3 files changed, 140 insertions(+), 121 deletions(-) create mode 100644 packages/viewer/src/modules/extensions/controls/PivotalControls.ts diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index eda7e8e990..1ec2f06a61 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -110,7 +110,7 @@ const getStream = () => { // prettier-ignore // 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D' // Revit sample house (good for bim-like stuff with many display meshes) - // 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' + 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' // 'https://latest.speckle.systems/streams/c1faab5c62/commits/ab1a1ab2b6' // 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' // 'https://latest.speckle.systems/streams/58b5648c4d/commits/60371ecb2d' @@ -184,7 +184,7 @@ const getStream = () => { // Alex cubes // 'https://latest.speckle.systems/streams/4658eb53b9/commits/d8ec9cccf7' // Alex more cubes - 'https://latest.speckle.systems/streams/4658eb53b9/commits/31a8d5ff2b' + // 'https://latest.speckle.systems/streams/4658eb53b9/commits/31a8d5ff2b' // Tekla // 'https://latest.speckle.systems/streams/caec6d6676/commits/588c731104' // Purple market square diff --git a/packages/viewer/src/modules/extensions/controls/PivotalControls.ts b/packages/viewer/src/modules/extensions/controls/PivotalControls.ts new file mode 100644 index 0000000000..5806ef33bb --- /dev/null +++ b/packages/viewer/src/modules/extensions/controls/PivotalControls.ts @@ -0,0 +1,53 @@ +// import { PerspectiveCamera, OrthographicCamera, Sphere, Vector3 } from 'three' +// import { SpeckleControls } from './SpeckleControls.js' + +// export interface PivotalControlsOptions {} + +// export class PivotalControls extends SpeckleControls { +// private _enabled: boolean = false +// private _options: Required = {} + +// get options(): Partial { +// return this._options +// } +// set options(value: Partial) { +// Object.assign(this._options, value) +// } + +// get enabled(): boolean { +// return this._enabled +// } +// set enabled(value: boolean) { +// this._enabled = value +// } + +// set targetCamera(target: PerspectiveCamera | OrthographicCamera) { +// throw new Error('Method not implemented.') +// } + +// isStationary(): boolean { +// throw new Error('Method not implemented.') +// } + +// update(delta?: number): boolean { +// throw new Error('Method not implemented.') +// } +// jumpToGoal(): void { +// throw new Error('Method not implemented.') +// } +// fitToSphere(sphere: Sphere): void { +// throw new Error('Method not implemented.') +// } +// dispose(): void { +// throw new Error('Method not implemented.') +// } +// fromPositionAndTarget(position: Vector3, target: Vector3): void { +// throw new Error('Method not implemented.') +// } +// getTarget(): Vector3 { +// throw new Error('Method not implemented.') +// } +// getPosition(): Vector3 { +// throw new Error('Method not implemented.') +// } +// } diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index d4fa0c0b4a..5556b596c9 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -40,7 +40,6 @@ import { Intersections } from '../../Intersections.js' import { lerp } from 'three/src/math/MathUtils.js' import { computeOrthographicSize } from '../CameraController.js' import { ObjectLayers } from '../../../IViewer.js' -import { multiply } from 'lodash-es' /** * @param {Number} value @@ -651,147 +650,110 @@ export class SmoothOrbitControls extends SpeckleControls { protected lastPivot = new Vector3() protected relativeCamPos = new Vector3() protected lastInitialPosition = new Vector3() - protected lastQuat = new Quaternion() + protected lastQuat: Quaternion = new Quaternion() protected lastDelta = new Vector3() - protected getPivotTransform(pivot: Vector3, quaternion: Quaternion) { - const translateToOrigin = new Matrix4().makeTranslation( - -pivot.x, - -pivot.y, - -pivot.z + protected polarFromPivotal() { + const quaternion = this.quaternionFromSpherical(this.spherical) + const dir = new Vector3().setFromMatrixColumn( + new Matrix4().makeRotationFromQuaternion(quaternion), + 2 ) - const translateBack = new Matrix4().makeTranslation(pivot.x, pivot.y, pivot.z) + const camPos = new Vector3() + .copy(this._targetCamera.position) + .applyMatrix4(this._basisTransformInv) - // Rotation matrix from quaternion - const rotationMatrix = new Matrix4().makeRotationFromQuaternion(quaternion) + const pivotPoint = new Vector3() + .copy(this.pivotPoint) + .applyMatrix4(this._basisTransformInv) - // Combine the matrices - return new Matrix4() - .multiply(translateBack) // Translate back to the pivot - .multiply(rotationMatrix) // Apply rotation - .multiply(translateToOrigin) // Translate pivot to origin - // .multiply(new Matrix4().makeScale(1, 1, this.spherical.radius)) + const cameraPivotDist = camPos.distanceTo(pivotPoint) + const cameraPivotDir = new Vector3().copy(camPos).sub(pivotPoint) + cameraPivotDir.normalize() + + const dot = Math.min(Math.max(dir.dot(cameraPivotDir), -1), 1) + const angle = Math.acos(dot) + const polarRadius = cameraPivotDist //cameraPivotDist * Math.cos(angle) + const polarOrigin = new Vector3().copy(dir).multiplyScalar(polarRadius) + this.setTarget(polarOrigin.x, polarOrigin.y, polarOrigin.z) + this.setRadius(polarRadius) + this.jumpToGoal() } - protected change = false - protected moveCamera() { + protected positionFromPivotal(origin: Vector3, quaternion: Quaternion) { const pivotPoint = new Vector3() .copy(this.pivotPoint) .applyMatrix4(this._basisTransformInv) - const prevPivotPoint = new Vector3() - .copy(this.lastPivot) - .applyMatrix4(this._basisTransformInv) - const camPos = new Vector3() - .copy(this._targetCamera.position) - .applyMatrix4(this._basisTransformInv) - // const position = new Vector3(0, 50, this.spherical.radius) - // const quaternion = new Quaternion().setFromEuler( - // new Euler(0, this.spherical.theta, 0) - // ) - // position.sub(pivotPoint) - // position.applyQuaternion(quaternion) - // position.add(pivotPoint) + const dir = new Vector3().setFromMatrixColumn( + new Matrix4().makeRotationFromQuaternion(quaternion), + 2 + ) + dir.multiplyScalar(this.spherical.radius) - // this._targetCamera.position.copy(position) - // this._targetCamera.quaternion.copy(quaternion) + const position = new Vector3() + position.copy(origin) - // this._targetCamera.position.set(0, 50, this.spherical.radius) - // this._targetCamera.quaternion.identity() - // this._targetCamera.updateMatrixWorld(true) + position.sub(pivotPoint) + position.applyQuaternion(quaternion) + position.add(pivotPoint) + position.add(dir) - // this.rotateAboutPoint( - // this._targetCamera, - // pivotPoint, - // new Vector3(0, 1, 0), - // this.spherical.theta - // ) + return position + } - // // Step 1: Calculate the camera's position relative to the pivot using spherical coordinates - // const cameraOffset = new Vector3(0, 50, this.spherical.radius) // Camera offset (distance and height) - // const rotationMatrix = new Matrix4().makeRotationFromEuler( - // new Euler(0, this.spherical.theta, 0) - // ) // Apply yaw rotation - - // cameraOffset.sub(pivotPoint) - // cameraOffset.applyMatrix4(rotationMatrix) // Apply the rotation - // cameraOffset.add(pivotPoint) - // this._targetCamera.position.copy(cameraOffset) // Set the camera position relative to the pivot - - // // Step 2: Update the camera orientation so it faces away from the pivot point - // const direction = new Vector3() - // .subVectors(this._targetCamera.position, pivotPoint) - // .normalize() // Camera direction from pivot - // const up = new Vector3(0, 1, 0) // Keep the 'up' vector stable - // const right = new Vector3().crossVectors(up, direction).normalize() // Right vector (cross product of up and direction) - // const newUp = new Vector3().crossVectors(direction, right).normalize() // Recompute up vector based on new direction - - // // Step 3: Create a new quaternion based on the updated right, up, and direction vectors - // const rotationMatrixFinal = new Matrix4().makeBasis(right, newUp, direction) - // this._targetCamera.quaternion.setFromRotationMatrix(rotationMatrixFinal) - - // this._targetCamera.position.applyMatrix4(this._basisTransform) - // this._targetCamera.quaternion.premultiply( - // new Quaternion().setFromRotationMatrix(this._basisTransform) - // ) + protected isPivotal = false + protected pivotalOrigin: Vector3 = new Vector3() - // Derive the new camera position from the updated spherical: + protected change = false + protected moveCamera() { this.spherical.makeSafe() - // // Compute direction vector: from camera to target (origin) - // const pos = new Vector3().copy(this._targetCamera.position) - // const direction = new Vector3().subVectors(pos, new Vector3(0, 0, 0)).normalize() - - // // Compute a "right" vector using cross product with world up (0, 1, 0) - // const worldUp = new Vector3(0, 1, 0) - // const right = new Vector3().crossVectors(worldUp, direction).normalize() - - // // Recompute the up vector to ensure orthogonality - // const up = new Vector3().crossVectors(direction, right).normalize() - - // // Construct the rotation matrix using the orthogonal basis vectors - // const rotationMatrix = new Matrix4().makeBasis(right, up, direction.negate()) - - // // Extract the quaternion from the rotation matrix - // const fakeQuaternion = new Quaternion().setFromRotationMatrix(rotationMatrix) - // // const sphericalPosition = this.positionFromSpherical(this.spherical, this.origin) + const pivotPoint = new Vector3() + .copy(this.pivotPoint) + .applyMatrix4(this._basisTransformInv) + const prevPivotPoint = new Vector3() + .copy(this.lastPivot) + .applyMatrix4(this._basisTransformInv) const quaternion = this.quaternionFromSpherical(this.spherical) - const position = new Vector3() - const tPivot = this.getPivotTransform(pivotPoint, quaternion) - const invTPivot = new Matrix4().copy(tPivot).invert() - - const deltaPivot = new Vector3().copy(prevPivotPoint).sub(pivotPoint) - // console.log('Pivot -> ', pivotPoint) - // const tPrevPivot = this.getPivotTransform(prevPivotPoint, quaternion) - // const tPrevPointInv = new Matrix4().copy(tPrevPivot).invert() - // const objectToPrevPiot = new Vector3().copy(camPos).sub(prevPivotPoint) - // const objectToNewPivot = new Vector3().copy(camPos).sub(pivotPoint) - // const offset = new Vector3().copy(objectToPrevPiot).sub(objectToNewPivot) - const dir = new Vector3().setFromMatrixColumn( - new Matrix4().makeRotationFromQuaternion(quaternion), - 2 - ) - dir.multiplyScalar(this.spherical.radius) + const prevQuaternion = new Quaternion() + .copy(this.lastQuat) + .premultiply(new Quaternion().setFromRotationMatrix(this._basisTransformInv)) + const deltaPivot = prevPivotPoint.sub(pivotPoint) + let deltaQuat = prevQuaternion.angleTo(quaternion) + + if (deltaQuat === 2.3948158271083724) deltaQuat = 0 if (deltaPivot.length() > 0) { - this.lastDelta.copy(pivotPoint) - this.lastDelta.add( - new Vector3() - .copy(pivotPoint) - .negate() - .applyQuaternion(new Quaternion().copy(quaternion).invert()) + const camPos = new Vector3() + .copy(this._targetCamera.position) + .applyMatrix4(this._basisTransformInv) + const dir = new Vector3().setFromMatrixColumn( + new Matrix4().makeRotationFromQuaternion(quaternion), + 2 ) - this.lastInitialPosition.copy(this.lastDelta) + dir.multiplyScalar(this.spherical.radius) + + camPos.sub(dir) + camPos.sub(pivotPoint) + camPos.applyQuaternion(new Quaternion().copy(quaternion).invert()) + camPos.add(pivotPoint) + this.pivotalOrigin.copy(camPos) } - console.warn(this.lastInitialPosition) - position.copy(this.lastInitialPosition) - position.sub(pivotPoint) - position.applyQuaternion(quaternion) - position.add(pivotPoint) - position.add(dir) + if (deltaQuat > 0.00001) { + // pivotalOrigin.add(pivotPoint) + // pivotalOrigin.add( + // new Vector3() + // .copy(pivotPoint) + // .negate() + // .applyQuaternion(new Quaternion().copy(quaternion).invert()) + // ) + } + + const position = this.positionFromPivotal(this.pivotalOrigin, quaternion) position.applyQuaternion( new Quaternion().setFromRotationMatrix(this._basisTransform) @@ -834,6 +796,7 @@ export class SmoothOrbitControls extends SpeckleControls { } this.lastPivot.copy(this.pivotPoint) + this.lastQuat.copy(quaternion) } /* Ortho height to distance functions @@ -1043,9 +1006,7 @@ export class SmoothOrbitControls extends SpeckleControls { new Vector2(x, y), ObjectLayers.STREAM_CONTENT_MESH, true, - this.world.worldBox, - true, - false + this.world.worldBox ) if (res) { this.lastPivot.copy(this.pivotPoint) @@ -1061,6 +1022,7 @@ export class SmoothOrbitControls extends SpeckleControls { .applyMatrix4(this._basisTransformInv) this.forceUpdate = true this.change = true + this.isPivotal = true } if (this.pointers.length > 2) { @@ -1157,6 +1119,10 @@ export class SmoothOrbitControls extends SpeckleControls { if (this.isUserPointing) { this.emit(PointerChangeEvent.PointerChangeEnd) } + // this.polarFromPivotal() + // this.isPivotal = false + // console.log('Pivotal -> ', this.positionFromPivotal()) + // console.log('Polar -> ', this.positionFromSpherical(this.spherical, this.origin)) } protected onTouchChange(event: PointerEvent) { From fbab733b21204cfa9f3a38d0087235ab416f4072 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Thu, 19 Dec 2024 22:13:40 +0200 Subject: [PATCH 09/28] Smoothened out the math abit --- .../modules/extensions/CameraController.ts | 1 + .../controls/SmoothOrbitControls.ts | 148 ++++++------------ 2 files changed, 53 insertions(+), 96 deletions(-) diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index ed18eed5ed..87a3231695 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -94,6 +94,7 @@ export const DefaultOrbitControlsOptions: Required = { touchAction: 'none', infiniteZoom: true, zoomToCursor: true, + orbitAroundCursor: true, lookSpeed: 1, moveSpeed: 1, damperDecay: 30, diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index 5556b596c9..309bd064e4 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /* @license * Licensed under the Apache License, Version 2.0 (the 'License'); * you may not use this file except in compliance with the License. @@ -28,8 +27,7 @@ import { Scene, Mesh, MeshBasicMaterial, - SphereGeometry, - Object3D + SphereGeometry } from 'three' import { Damper, SETTLING_TIME } from '../../utils/Damper.js' @@ -99,6 +97,8 @@ export interface SmoothOrbitControlsOptions { infiniteZoom?: boolean // Zoom to cursor zoomToCursor?: boolean + // Orbit around cursor + orbitAroundCursor?: boolean // Dampening damperDecay?: number } @@ -143,6 +143,7 @@ export class SmoothOrbitControls extends SpeckleControls { public spherical = new Spherical() private goalSpherical = new Spherical() private origin = new Vector3() + private pivotalOrigin: Vector3 = new Vector3() private goalOrigin = new Vector3() private targetDamperX = new Damper() private targetDamperY = new Damper() @@ -176,7 +177,7 @@ export class SmoothOrbitControls extends SpeckleControls { private orbitSphere: Mesh private originSphere: Mesh private pivotPoint: Vector3 = new Vector3() - private lastCameraPos: Vector3 + private lastPivot: Vector3 = new Vector3() private forceUpdate = false @@ -294,7 +295,9 @@ export class SmoothOrbitControls extends SpeckleControls { * Gets the current goal position */ public getPosition(): Vector3 { - return this.positionFromSpherical(this.goalSpherical, this.goalOrigin) + return this.positionFromSpherical(this.goalSpherical, this.goalOrigin).applyMatrix4( + this._basisTransform + ) } /** @@ -626,33 +629,6 @@ export class SmoothOrbitControls extends SpeckleControls { return true } - protected transformTo(outParentToTarget: Matrix4, parent: Matrix4, target: Matrix4) { - outParentToTarget.copy(parent) - outParentToTarget.invert() - outParentToTarget.multiply(target) - } - - protected rotateAboutPoint( - obj: Object3D, - point: Vector3, - axis: Vector3, - theta: number - ) { - obj.position.sub(point) // remove the offset - obj.position.applyAxisAngle(axis, theta) // rotate the POSITION - obj.position.add(point) // re-add the offset - - obj.rotateOnAxis(axis, theta) // rotate the OBJECT - } - - protected hereToThere = new Vector3() - protected hereToThereMat = new Matrix4() - protected lastPivot = new Vector3() - protected relativeCamPos = new Vector3() - protected lastInitialPosition = new Vector3() - protected lastQuat: Quaternion = new Quaternion() - protected lastDelta = new Vector3() - protected polarFromPivotal() { const quaternion = this.quaternionFromSpherical(this.spherical) const dir = new Vector3().setFromMatrixColumn( @@ -671,41 +647,52 @@ export class SmoothOrbitControls extends SpeckleControls { const cameraPivotDir = new Vector3().copy(camPos).sub(pivotPoint) cameraPivotDir.normalize() - const dot = Math.min(Math.max(dir.dot(cameraPivotDir), -1), 1) - const angle = Math.acos(dot) - const polarRadius = cameraPivotDist //cameraPivotDist * Math.cos(angle) - const polarOrigin = new Vector3().copy(dir).multiplyScalar(polarRadius) - this.setTarget(polarOrigin.x, polarOrigin.y, polarOrigin.z) - this.setRadius(polarRadius) - this.jumpToGoal() + // const dot = Math.min(Math.max(dir.dot(cameraPivotDir), -1), 1) + // const angle = Math.acos(dot) + // console.log('Angle -> ', angle) + const polarRadius = cameraPivotDist // * Math.cos(angle) + const polarOrigin = camPos.sub(new Vector3().copy(dir).multiplyScalar(polarRadius)) + + this.goalOrigin.copy(polarOrigin) + this.origin.copy(polarOrigin) + this.goalSpherical.radius = polarRadius + this.spherical.radius = polarRadius + + this.originSphere.position.copy(polarOrigin) + // console.log('Origin -> ', polarOrigin) + // console.log('Dist -> ', cameraPivotDist, ' Radius -> ', polarRadius) } + /** Function expects the origin in a CS where Y is up */ protected positionFromPivotal(origin: Vector3, quaternion: Quaternion) { const pivotPoint = new Vector3() .copy(this.pivotPoint) .applyMatrix4(this._basisTransformInv) - const dir = new Vector3().setFromMatrixColumn( - new Matrix4().makeRotationFromQuaternion(quaternion), - 2 - ) - dir.multiplyScalar(this.spherical.radius) - const position = new Vector3() position.copy(origin) position.sub(pivotPoint) position.applyQuaternion(quaternion) position.add(pivotPoint) - position.add(dir) return position } - protected isPivotal = false - protected pivotalOrigin: Vector3 = new Vector3() + /** Function expects the pivotPoint in a CS where Y is up */ + protected getPivotalOrigin(pivotPoint: Vector3, quaternion: Quaternion) { + const pivotalOrigin = new Vector3() + .copy(this._targetCamera.position) + .applyMatrix4(this._basisTransformInv) + + pivotalOrigin.sub(pivotPoint) + pivotalOrigin.applyQuaternion(new Quaternion().copy(quaternion).invert()) + pivotalOrigin.add(pivotPoint) - protected change = false + return pivotalOrigin + } + + protected usePivotal = false protected moveCamera() { this.spherical.makeSafe() @@ -716,44 +703,24 @@ export class SmoothOrbitControls extends SpeckleControls { .copy(this.lastPivot) .applyMatrix4(this._basisTransformInv) - const quaternion = this.quaternionFromSpherical(this.spherical) - - const prevQuaternion = new Quaternion() - .copy(this.lastQuat) - .premultiply(new Quaternion().setFromRotationMatrix(this._basisTransformInv)) const deltaPivot = prevPivotPoint.sub(pivotPoint) - let deltaQuat = prevQuaternion.angleTo(quaternion) - - if (deltaQuat === 2.3948158271083724) deltaQuat = 0 + const quaternion = this.quaternionFromSpherical(this.spherical) if (deltaPivot.length() > 0) { - const camPos = new Vector3() - .copy(this._targetCamera.position) - .applyMatrix4(this._basisTransformInv) - const dir = new Vector3().setFromMatrixColumn( - new Matrix4().makeRotationFromQuaternion(quaternion), - 2 - ) - dir.multiplyScalar(this.spherical.radius) - - camPos.sub(dir) - camPos.sub(pivotPoint) - camPos.applyQuaternion(new Quaternion().copy(quaternion).invert()) - camPos.add(pivotPoint) - this.pivotalOrigin.copy(camPos) + this.pivotalOrigin.copy(this.getPivotalOrigin(pivotPoint, quaternion)) } - if (deltaQuat > 0.00001) { - // pivotalOrigin.add(pivotPoint) - // pivotalOrigin.add( - // new Vector3() - // .copy(pivotPoint) - // .negate() - // .applyQuaternion(new Quaternion().copy(quaternion).invert()) + let position + if (this.usePivotal) { + position = this.positionFromPivotal(this.pivotalOrigin, quaternion) + this.polarFromPivotal() + // position = this.positionFromSpherical(this.spherical, this.origin) + // console.log( + // 'Pivotal -> ', + // this.positionFromPivotal(this.pivotalOrigin, quaternion) // ) - } - - const position = this.positionFromPivotal(this.pivotalOrigin, quaternion) + // console.log('Polar -> ', this.positionFromSpherical(this.spherical, this.origin)) + } else position = this.positionFromSpherical(this.spherical, this.origin) position.applyQuaternion( new Quaternion().setFromRotationMatrix(this._basisTransform) @@ -796,7 +763,6 @@ export class SmoothOrbitControls extends SpeckleControls { } this.lastPivot.copy(this.pivotPoint) - this.lastQuat.copy(quaternion) } /* Ortho height to distance functions @@ -989,6 +955,7 @@ export class SmoothOrbitControls extends SpeckleControls { const target = this.getTarget().applyMatrix4(this._basisTransformInv) target.add(dxy.applyMatrix3(this.panProjection)) this.setTarget(target.x, target.y, target.z) + this.usePivotal = false } protected onPointerDown = (event: PointerEvent) => { @@ -1011,18 +978,10 @@ export class SmoothOrbitControls extends SpeckleControls { if (res) { this.lastPivot.copy(this.pivotPoint) this.pivotPoint.copy(res[0].point) + this.polarFromPivotal() this.orbitSphere.position.copy(res[0].point) - // console.log('Radius -> ', this.spherical.radius) - // console.log('Distance -> ', this._targetCamera.position.distanceTo(res[0].point)) - // this.goalSpherical.radius += - // this.goalSpherical.radius - this._targetCamera.position.distanceTo(res[0].point) - - this.hereToThere - .copy(this._targetCamera.position) - .applyMatrix4(this._basisTransformInv) this.forceUpdate = true - this.change = true - this.isPivotal = true + this.usePivotal = this._options.orbitAroundCursor && true } if (this.pointers.length > 2) { @@ -1119,10 +1078,7 @@ export class SmoothOrbitControls extends SpeckleControls { if (this.isUserPointing) { this.emit(PointerChangeEvent.PointerChangeEnd) } - // this.polarFromPivotal() - // this.isPivotal = false - // console.log('Pivotal -> ', this.positionFromPivotal()) - // console.log('Polar -> ', this.positionFromSpherical(this.spherical, this.origin)) + this.usePivotal = false } protected onTouchChange(event: PointerEvent) { From 8fab92e472db501853c6837f8c95ab721685ebe7 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Thu, 19 Dec 2024 23:35:26 +0200 Subject: [PATCH 10/28] Fixed sandbox error --- packages/viewer-sandbox/src/main.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 48257dc911..da35d00e35 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -6,7 +6,6 @@ import { Viewer, ViewModes, HybridCameraController, - CameraController, SelectionExtension } from '@speckle/viewer' @@ -21,9 +20,8 @@ import { import { SectionTool } from '@speckle/viewer' import { SectionOutlines } from '@speckle/viewer' import { ViewModesKeys } from './Extensions/ViewModesKeys' -import { JSONSpeckleStream } from './JSONSpeckleStream' +// import { JSONSpeckleStream } from './JSONSpeckleStream' import { BoxSelection } from './Extensions/BoxSelection' -import { ExtendedSelection } from './Extensions/ExtendedSelection' const createViewer = async (containerName: string, _stream: string) => { const container = document.querySelector(containerName) From 28fff50fb4cfb57b91ffd36fafb475e5f87bae8b Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Fri, 20 Dec 2024 12:04:30 +0200 Subject: [PATCH 11/28] Enabled the pivot sphere --- .../src/modules/extensions/controls/SmoothOrbitControls.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index 821241c9d2..dbbfc68401 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -223,10 +223,11 @@ export class SmoothOrbitControls extends SpeckleControls { this.orbitSphere = new Mesh( new SphereGeometry(0.5, 32, 16), - new MeshBasicMaterial({ color: 0xff00000 }) + new MeshBasicMaterial({ color: 0x43af11 }) ) this.orbitSphere.layers.set(ObjectLayers.OVERLAY) - // this.scene.add(this.orbitSphere) + this.orbitSphere.visible = false + this.scene.add(this.orbitSphere) this.originSphere = new Mesh( new SphereGeometry(0.5, 32, 16), @@ -982,6 +983,7 @@ export class SmoothOrbitControls extends SpeckleControls { this.orbitSphere.position.copy(res[0].point) this.forceUpdate = true this.usePivotal = this._options.orbitAroundCursor && true + this.orbitSphere.visible = true } if (this.pointers.length > 2) { @@ -1079,6 +1081,7 @@ export class SmoothOrbitControls extends SpeckleControls { this.emit(PointerChangeEvent.PointerChangeEnd) } this.usePivotal = false + this.orbitSphere.visible = false } protected onTouchChange(event: PointerEvent) { From 3ded61feb58801b8c1a0c0aa40efa2a7c34152a6 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Thu, 9 Jan 2025 22:33:04 +0200 Subject: [PATCH 12/28] feat(viewer-lib): Fixed some issues with orbiting around cursor --- packages/viewer-sandbox/src/main.ts | 4 +- .../modules/extensions/CameraController.ts | 1 + .../controls/SmoothOrbitControls.ts | 158 +++++++++--------- .../modules/materials/SpeckleBasicMaterial.ts | 1 + 4 files changed, 86 insertions(+), 78 deletions(-) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index a687096d02..1022615ba3 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -112,7 +112,7 @@ const getStream = () => { // prettier-ignore // 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D' // Revit sample house (good for bim-like stuff with many display meshes) - // 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' + 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' // 'https://latest.speckle.systems/streams/c1faab5c62/commits/ab1a1ab2b6' // 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8' // 'https://latest.speckle.systems/streams/58b5648c4d/commits/60371ecb2d' @@ -462,7 +462,7 @@ const getStream = () => { // 'https://speckle.xyz/streams/27e89d0ad6/commits/5ed4b74252' //Gingerbread - 'https://latest.speckle.systems/projects/387050bffe/models/48f7eb26fb' + // 'https://latest.speckle.systems/projects/387050bffe/models/48f7eb26fb' // DUI3 Mesh Colors // 'https://app.speckle.systems/projects/93200a735d/models/cbacd3eaeb@344a397239' diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index 1cf97813d0..92b066edc2 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -95,6 +95,7 @@ export const DefaultOrbitControlsOptions: Required = { infiniteZoom: true, zoomToCursor: true, orbitAroundCursor: true, + showOrbitPoint: true, lookSpeed: 1, moveSpeed: 1, damperDecay: 30, diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index dbbfc68401..14c5862306 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -26,7 +26,6 @@ import { Euler, Scene, Mesh, - MeshBasicMaterial, SphereGeometry } from 'three' @@ -38,6 +37,7 @@ import { Intersections } from '../../Intersections.js' import { lerp } from 'three/src/math/MathUtils.js' import { computeOrthographicSize } from '../CameraController.js' import { ObjectLayers } from '../../../IViewer.js' +import SpeckleBasicMaterial from '../../materials/SpeckleBasicMaterial.js' /** * @param {Number} value @@ -99,6 +99,8 @@ export interface SmoothOrbitControlsOptions { zoomToCursor?: boolean // Orbit around cursor orbitAroundCursor?: boolean + // Show orbit point + showOrbitPoint?: boolean // Dampening damperDecay?: number } @@ -175,11 +177,8 @@ export class SmoothOrbitControls extends SpeckleControls { private intersections: Intersections private orbitSphere: Mesh - private originSphere: Mesh private pivotPoint: Vector3 = new Vector3() - private lastPivot: Vector3 = new Vector3() - - private forceUpdate = false + private lastPivotPoint: Vector3 = new Vector3() public get enabled(): boolean { return this._enabled @@ -221,20 +220,20 @@ export class SmoothOrbitControls extends SpeckleControls { this._options = Object.assign({}, options) this.setDamperDecayTime(this._options.damperDecay) - this.orbitSphere = new Mesh( - new SphereGeometry(0.5, 32, 16), - new MeshBasicMaterial({ color: 0x43af11 }) - ) + const billboardMaterial = new SpeckleBasicMaterial({ color: 0x43af11 }, [ + 'BILLBOARD_FIXED' + ]) + billboardMaterial.opacity = 0.75 + billboardMaterial.transparent = true + billboardMaterial.color.convertSRGBToLinear() + billboardMaterial.toneMapped = false + billboardMaterial.depthTest = false + billboardMaterial.billboardPixelHeight = 15 * window.devicePixelRatio + + this.orbitSphere = new Mesh(new SphereGeometry(0.5, 32, 16), billboardMaterial) this.orbitSphere.layers.set(ObjectLayers.OVERLAY) this.orbitSphere.visible = false this.scene.add(this.orbitSphere) - - this.originSphere = new Mesh( - new SphereGeometry(0.5, 32, 16), - new MeshBasicMaterial({ color: 0x00ff00 }) - ) - this.originSphere.layers.set(ObjectLayers.OVERLAY) - // this.scene.add(this.originSphere) } /** @@ -315,7 +314,7 @@ export class SmoothOrbitControls extends SpeckleControls { this.goalSpherical.radius === this.spherical.radius && this.goalLogFov === this.logFov && this.goalOrigin.equals(this.origin) && - !this.forceUpdate + this.pivotPoint.equals(this.lastPivotPoint) ) } @@ -331,6 +330,7 @@ export class SmoothOrbitControls extends SpeckleControls { // polar, azimuth and radius: this.setOrbit() this.setFieldOfView(Math.exp(this.goalLogFov)) + this.orbitSphere.visible = this._options.showOrbitPoint } /** Computes min/max radius values based on the current world size */ @@ -622,11 +622,8 @@ export class SmoothOrbitControls extends SpeckleControls { normalization ) this.origin.set(x, y, z) - const v = new Vector3().set(x, y, z) - this.originSphere.position.copy(v.applyMatrix4(this._basisTransform)) this.moveCamera() - this.forceUpdate = false return true } @@ -644,24 +641,27 @@ export class SmoothOrbitControls extends SpeckleControls { .copy(this.pivotPoint) .applyMatrix4(this._basisTransformInv) + // let cameraPivotDist + // if (this._targetCamera instanceof OrthographicCamera) { + // const offset = dir.multiplyScalar( + // this._options.maximumRadius - this.options.minimumRadius - this.spherical.radius + // ) + // const plm = new Vector3().copy(camPos).sub(offset) + // cameraPivotDist = camPos.distanceTo(pivotPoint) + // } else cameraPivotDist = camPos.distanceTo(pivotPoint) const cameraPivotDist = camPos.distanceTo(pivotPoint) const cameraPivotDir = new Vector3().copy(camPos).sub(pivotPoint) cameraPivotDir.normalize() - // const dot = Math.min(Math.max(dir.dot(cameraPivotDir), -1), 1) - // const angle = Math.acos(dot) - // console.log('Angle -> ', angle) - const polarRadius = cameraPivotDist // * Math.cos(angle) + const dot = Math.min(Math.max(dir.dot(cameraPivotDir), -1), 1) + const angle = Math.acos(dot) + const polarRadius = cameraPivotDist * Math.cos(angle) const polarOrigin = camPos.sub(new Vector3().copy(dir).multiplyScalar(polarRadius)) this.goalOrigin.copy(polarOrigin) this.origin.copy(polarOrigin) this.goalSpherical.radius = polarRadius this.spherical.radius = polarRadius - - this.originSphere.position.copy(polarOrigin) - // console.log('Origin -> ', polarOrigin) - // console.log('Dist -> ', cameraPivotDist, ' Radius -> ', polarRadius) } /** Function expects the origin in a CS where Y is up */ @@ -694,6 +694,11 @@ export class SmoothOrbitControls extends SpeckleControls { } protected usePivotal = false + /** This flag decides if full pivotal movement is going to be used or a 'softer' inbetween pivotal and polar one, + * where the polar origin is moved at the same depth as the pivot point. We don't expose this (yet) + */ + protected fullPivotal = true + protected moveCamera() { this.spherical.makeSafe() @@ -701,7 +706,7 @@ export class SmoothOrbitControls extends SpeckleControls { .copy(this.pivotPoint) .applyMatrix4(this._basisTransformInv) const prevPivotPoint = new Vector3() - .copy(this.lastPivot) + .copy(this.lastPivotPoint) .applyMatrix4(this._basisTransformInv) const deltaPivot = prevPivotPoint.sub(pivotPoint) @@ -712,15 +717,9 @@ export class SmoothOrbitControls extends SpeckleControls { } let position - if (this.usePivotal) { + if (this.usePivotal && this.fullPivotal) { position = this.positionFromPivotal(this.pivotalOrigin, quaternion) this.polarFromPivotal() - // position = this.positionFromSpherical(this.spherical, this.origin) - // console.log( - // 'Pivotal -> ', - // this.positionFromPivotal(this.pivotalOrigin, quaternion) - // ) - // console.log('Polar -> ', this.positionFromSpherical(this.spherical, this.origin)) } else position = this.positionFromSpherical(this.spherical, this.origin) position.applyQuaternion( @@ -728,19 +727,19 @@ export class SmoothOrbitControls extends SpeckleControls { ) quaternion.premultiply(new Quaternion().setFromRotationMatrix(this._basisTransform)) - if (this._targetCamera instanceof OrthographicCamera) { - const cameraDirection = new Vector3() - .setFromSpherical(this.spherical) - .applyQuaternion(new Quaternion().setFromRotationMatrix(this._basisTransform)) - .normalize() - position.add( - cameraDirection.multiplyScalar( - this._options.maximumRadius - - this.options.minimumRadius - - this.spherical.radius - ) - ) - } + // if (this._targetCamera instanceof OrthographicCamera) { + // const cameraDirection = new Vector3() + // .setFromSpherical(this.spherical) + // .applyQuaternion(new Quaternion().setFromRotationMatrix(this._basisTransform)) + // .normalize() + // position.add( + // cameraDirection.multiplyScalar( + // this._options.maximumRadius - + // this.options.minimumRadius - + // this.spherical.radius + // ) + // ) + // } this._targetCamera.position.copy(position) this._targetCamera.quaternion.copy(quaternion) this._targetCamera.updateMatrixWorld(true) @@ -763,16 +762,22 @@ export class SmoothOrbitControls extends SpeckleControls { this._targetCamera.updateProjectionMatrix() } - this.lastPivot.copy(this.pivotPoint) + this.lastPivotPoint.copy(this.pivotPoint) + + this.orbitSphere.position.copy( + this._options.orbitAroundCursor + ? this.pivotPoint + : new Vector3().copy(this.origin).applyMatrix4(this._basisTransform) + ) } - /* Ortho height to distance functions + // Ortho height to distance functions private orthographicHeightToDistance(height: number) { if (!(this._targetCamera instanceof OrthographicCamera)) return this.spherical.radius return height / (Math.tan(MathUtils.DEG2RAD * Math.exp(this.logFov) * 0.5) * 2) - }*/ + } /** Three.js Spherical assumes (0, 1, 0) as up... */ protected positionFromSpherical(spherical: Spherical, origin?: Vector3) { @@ -960,31 +965,32 @@ export class SmoothOrbitControls extends SpeckleControls { } protected onPointerDown = (event: PointerEvent) => { - const x = - ((event.clientX - this._container.offsetLeft) / this._container.offsetWidth) * 2 - - 1 + if (this._options.orbitAroundCursor) { + const x = + ((event.clientX - this._container.offsetLeft) / this._container.offsetWidth) * + 2 - + 1 + + const y = + ((event.clientY - this._container.offsetTop) / this._container.offsetHeight) * + -2 + + 1 + const res = this.intersections.intersect( + this.scene, + this._targetCamera as PerspectiveCamera, + new Vector2(x, y), + ObjectLayers.STREAM_CONTENT_MESH, + true, + this.world.worldBox // TO DO: This does not account for transformed objects + ) + if (res) { + this.pivotPoint.copy(res[0].point) + this.polarFromPivotal() - const y = - ((event.clientY - this._container.offsetTop) / this._container.offsetHeight) * - -2 + - 1 - const res = this.intersections.intersect( - this.scene, - this._targetCamera as PerspectiveCamera, - new Vector2(x, y), - ObjectLayers.STREAM_CONTENT_MESH, - true, - this.world.worldBox // TO DO: This does not account for transformed objects - ) - if (res) { - this.lastPivot.copy(this.pivotPoint) - this.pivotPoint.copy(res[0].point) - this.polarFromPivotal() - this.orbitSphere.position.copy(res[0].point) - this.forceUpdate = true - this.usePivotal = this._options.orbitAroundCursor && true - this.orbitSphere.visible = true + this.usePivotal = true + } } + this.orbitSphere.visible = this._options.showOrbitPoint if (this.pointers.length > 2) { return @@ -1080,7 +1086,6 @@ export class SmoothOrbitControls extends SpeckleControls { if (this.isUserPointing) { this.emit(PointerChangeEvent.PointerChangeEnd) } - this.usePivotal = false this.orbitSphere.visible = false } @@ -1143,6 +1148,7 @@ export class SmoothOrbitControls extends SpeckleControls { 60 this.userAdjustOrbit(0, 0, deltaZoom) event.preventDefault() + this.usePivotal = false // TO DO // this.dispatchEvent({ type: 'user-interaction' }) } diff --git a/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts b/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts index f336252047..d9868d5c20 100644 --- a/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts +++ b/packages/viewer/src/modules/materials/SpeckleBasicMaterial.ts @@ -95,6 +95,7 @@ class SpeckleBasicMaterial extends ExtendedMeshBasicMaterial { this.userData.billboardSize.value.copy(SpeckleBasicMaterial.vecBuff) SpeckleBasicMaterial.matBuff.copy(camera.projectionMatrix).invert() this.userData.invProjection.value.copy(SpeckleBasicMaterial.matBuff) + this.userData.billboardPos.value.copy(object.position) } if (this.defines && this.defines['USE_RTE']) { From a4dbf462240eeb1195530093ae3a2988051b5bad Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Fri, 10 Jan 2025 15:30:40 +0200 Subject: [PATCH 13/28] feat(viewer-lib): Updates to WEB-2313, orbiting around mouse cursor Orbiting around mouse cursor now works correctly with an orthographic projection as well as when toggling between orthographic and perspective. Disabled WASD navigation for now. SmoothOrbitControls now has protected members instead of private allowing extension Documented the important parts of the pivotal navigation code --- packages/viewer-sandbox/src/main.ts | 5 +- .../controls/SmoothOrbitControls.ts | 223 ++++++++++-------- 2 files changed, 126 insertions(+), 102 deletions(-) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 1022615ba3..62ec7ffff2 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -6,7 +6,8 @@ import { Viewer, HybridCameraController, ViewModes, - SelectionExtension + SelectionExtension, + CameraController } from '@speckle/viewer' import './style.css' @@ -45,7 +46,7 @@ const createViewer = async (containerName: string, _stream: string) => { const viewer: Viewer = new Viewer(container, params) await viewer.init() - const cameraController = viewer.createExtension(HybridCameraController) + const cameraController = viewer.createExtension(CameraController) const selection = viewer.createExtension(SelectionExtension) const sections = viewer.createExtension(SectionTool) viewer.createExtension(SectionOutlines) diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index 14c5862306..a7dea36384 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -131,54 +131,55 @@ export enum PointerChangeEvent { * ensure that the camera's matrixWorld is in sync before using SmoothControls. */ export class SmoothOrbitControls extends SpeckleControls { - private _enabled: boolean = false - private _options: Required - private isUserPointing = false + protected _enabled: boolean = false + protected _options: Required + protected isUserPointing = false // Pan state public enablePan = true public enableTap = true - private panProjection = new Matrix3() - private panPerPixel = 0 + protected panProjection = new Matrix3() + protected panPerPixel = 0 // Internal orbital position state public spherical = new Spherical() - private goalSpherical = new Spherical() - private origin = new Vector3() - private pivotalOrigin: Vector3 = new Vector3() - private goalOrigin = new Vector3() - private targetDamperX = new Damper() - private targetDamperY = new Damper() - private targetDamperZ = new Damper() - private thetaDamper = new Damper() - private phiDamper = new Damper() - private radiusDamper = new Damper() - private logFov = Math.log(55) - private goalLogFov = this.logFov - private fovDamper = new Damper() + protected goalSpherical = new Spherical() + protected origin = new Vector3() + protected pivotalOrigin: Vector3 = new Vector3() + protected goalOrigin = new Vector3() + protected targetDamperX = new Damper() + protected targetDamperY = new Damper() + protected targetDamperZ = new Damper() + protected thetaDamper = new Damper() + protected phiDamper = new Damper() + protected radiusDamper = new Damper() + protected logFov = Math.log(55) + protected goalLogFov = this.logFov + protected fovDamper = new Damper() // Pointer state - private touchMode: TouchMode = null - private pointers: Pointer[] = [] - private startPointerPosition = { clientX: 0, clientY: 0 } - private lastSeparation = 0 - private touchDecided = false - private zoomControlCoord: Vector2 = new Vector2() - - private _targetCamera: PerspectiveCamera | OrthographicCamera - private _container: HTMLElement - private _lastTick: number = 0 - private _basisTransform: Matrix4 = new Matrix4() - private _basisTransformInv: Matrix4 = new Matrix4() - private _radiusDelta: number = 0 - - private scene: Scene - private world: World - private intersections: Intersections - - private orbitSphere: Mesh - private pivotPoint: Vector3 = new Vector3() - private lastPivotPoint: Vector3 = new Vector3() + protected touchMode: TouchMode = null + protected pointers: Pointer[] = [] + protected startPointerPosition = { clientX: 0, clientY: 0 } + protected lastSeparation = 0 + protected touchDecided = false + protected zoomControlCoord: Vector2 = new Vector2() + + protected _targetCamera: PerspectiveCamera | OrthographicCamera + protected _container: HTMLElement + protected _lastTick: number = 0 + protected _basisTransform: Matrix4 = new Matrix4() + protected _basisTransformInv: Matrix4 = new Matrix4() + protected _radiusDelta: number = 0 + + protected scene: Scene + protected world: World + protected intersections: Intersections + + protected orbitSphere: Mesh + protected pivotPoint: Vector3 = new Vector3() + protected lastPivotPoint: Vector3 = new Vector3() + protected usePivotal = false public get enabled(): boolean { return this._enabled @@ -249,6 +250,14 @@ export class SmoothOrbitControls extends SpeckleControls { set targetCamera(value: PerspectiveCamera | OrthographicCamera) { this._targetCamera = value + this.usePivotal = this._options.orbitAroundCursor + + /** We move the lat pivot point somwhere outside of world bounds, in order to force a pivotal origin recompute */ + this.lastPivotPoint.set( + this.world.worldOrigin.x + this.world.worldSize.x, + this.world.worldOrigin.y + this.world.worldSize.y, + this.world.worldOrigin.z + this.world.worldSize.z + ) this.moveCamera() } @@ -627,44 +636,43 @@ export class SmoothOrbitControls extends SpeckleControls { return true } - protected polarFromPivotal() { + /** Function expects the position argument to be in a CS where Y is up */ + protected polarFromPivotal(position: Vector3) { const quaternion = this.quaternionFromSpherical(this.spherical) + /** Forward direction */ const dir = new Vector3().setFromMatrixColumn( new Matrix4().makeRotationFromQuaternion(quaternion), 2 ) - const camPos = new Vector3() - .copy(this._targetCamera.position) - .applyMatrix4(this._basisTransformInv) + const camPos = new Vector3().copy(position) + /** Pivot needs to be transformed in a Y up CS */ const pivotPoint = new Vector3() .copy(this.pivotPoint) .applyMatrix4(this._basisTransformInv) - // let cameraPivotDist - // if (this._targetCamera instanceof OrthographicCamera) { - // const offset = dir.multiplyScalar( - // this._options.maximumRadius - this.options.minimumRadius - this.spherical.radius - // ) - // const plm = new Vector3().copy(camPos).sub(offset) - // cameraPivotDist = camPos.distanceTo(pivotPoint) - // } else cameraPivotDist = camPos.distanceTo(pivotPoint) const cameraPivotDist = camPos.distanceTo(pivotPoint) const cameraPivotDir = new Vector3().copy(camPos).sub(pivotPoint) cameraPivotDir.normalize() const dot = Math.min(Math.max(dir.dot(cameraPivotDir), -1), 1) const angle = Math.acos(dot) + /** We compute a new distanced based on the pivot point */ const polarRadius = cameraPivotDist * Math.cos(angle) + /** We compute a new origin based on the pivot point, but keeping it along the camera's current forward direction */ const polarOrigin = camPos.sub(new Vector3().copy(dir).multiplyScalar(polarRadius)) this.goalOrigin.copy(polarOrigin) this.origin.copy(polarOrigin) - this.goalSpherical.radius = polarRadius - this.spherical.radius = polarRadius + + /** For orthographica camera's we don't need to update the radius because it will break their orthographic size */ + if (this._targetCamera instanceof PerspectiveCamera) { + this.goalSpherical.radius = polarRadius + this.spherical.radius = polarRadius + } } - /** Function expects the origin in a CS where Y is up */ + /** Function expects the origin argument to be in a CS where Y is up */ protected positionFromPivotal(origin: Vector3, quaternion: Quaternion) { const pivotPoint = new Vector3() .copy(this.pivotPoint) @@ -680,11 +688,13 @@ export class SmoothOrbitControls extends SpeckleControls { return position } - /** Function expects the pivotPoint in a CS where Y is up */ - protected getPivotalOrigin(pivotPoint: Vector3, quaternion: Quaternion) { - const pivotalOrigin = new Vector3() - .copy(this._targetCamera.position) - .applyMatrix4(this._basisTransformInv) + /** Function expects the pivotPoint and position arguments to be in a CS where Y is up */ + protected getPivotalOrigin( + pivotPoint: Vector3, + position: Vector3, + quaternion: Quaternion + ) { + const pivotalOrigin = new Vector3().copy(position) pivotalOrigin.sub(pivotPoint) pivotalOrigin.applyQuaternion(new Quaternion().copy(quaternion).invert()) @@ -693,61 +703,76 @@ export class SmoothOrbitControls extends SpeckleControls { return pivotalOrigin } - protected usePivotal = false - /** This flag decides if full pivotal movement is going to be used or a 'softer' inbetween pivotal and polar one, - * where the polar origin is moved at the same depth as the pivot point. We don't expose this (yet) - */ - protected fullPivotal = true - protected moveCamera() { this.spherical.makeSafe() - const pivotPoint = new Vector3() - .copy(this.pivotPoint) - .applyMatrix4(this._basisTransformInv) - const prevPivotPoint = new Vector3() - .copy(this.lastPivotPoint) - .applyMatrix4(this._basisTransformInv) - - const deltaPivot = prevPivotPoint.sub(pivotPoint) + /** We get the current position and rotation based off the latest polar params + * The ground truth is going to always be the polar CS! + */ const quaternion = this.quaternionFromSpherical(this.spherical) + let position = this.positionFromSpherical(this.spherical, this.origin) + + if (this.usePivotal) { + /** We transform both current and previous pivots in a CS where Y us up */ + const pivotPoint = new Vector3() + .copy(this.pivotPoint) + .applyMatrix4(this._basisTransformInv) + const prevPivotPoint = new Vector3() + .copy(this.lastPivotPoint) + .applyMatrix4(this._basisTransformInv) + + const deltaPivot = prevPivotPoint.sub(pivotPoint) + + /** We recompute the pivotal origin/pivotal offset, but only when required! */ + if (deltaPivot.length() > 0) { + this.pivotalOrigin.copy(this.getPivotalOrigin(pivotPoint, position, quaternion)) + } - if (deltaPivot.length() > 0) { - this.pivotalOrigin.copy(this.getPivotalOrigin(pivotPoint, quaternion)) - } - - let position - if (this.usePivotal && this.fullPivotal) { + /** We get a new position in the pivotal CS */ position = this.positionFromPivotal(this.pivotalOrigin, quaternion) - this.polarFromPivotal() - } else position = this.positionFromSpherical(this.spherical, this.origin) + /** We update the polar CS based off the new pivotal camera position, + * essentially creating a virtual pair polar CS which can reproduce the pivotal position */ + this.polarFromPivotal(position) + /** Update the last pivot */ + this.lastPivotPoint.copy(this.pivotPoint) + } + /** We transform both position and quaternion in the required basis */ position.applyQuaternion( new Quaternion().setFromRotationMatrix(this._basisTransform) ) quaternion.premultiply(new Quaternion().setFromRotationMatrix(this._basisTransform)) - // if (this._targetCamera instanceof OrthographicCamera) { - // const cameraDirection = new Vector3() - // .setFromSpherical(this.spherical) - // .applyQuaternion(new Quaternion().setFromRotationMatrix(this._basisTransform)) - // .normalize() - // position.add( - // cameraDirection.multiplyScalar( - // this._options.maximumRadius - - // this.options.minimumRadius - - // this.spherical.radius - // ) - // ) - // } + /** This is a trick we do for ortographic projection which stops the near plane from clipping into geometry + * In orthographic projection the camera's 'depth' along it's forward does not matter. Zoooming is achieved by + * varying the orthographic size, not by moving the camera. + */ + if (this._targetCamera instanceof OrthographicCamera) { + const cameraDirection = new Vector3() + .setFromSpherical(this.spherical) + .applyQuaternion(new Quaternion().setFromRotationMatrix(this._basisTransform)) + .normalize() + position.add( + cameraDirection.multiplyScalar( + this._options.maximumRadius - + this.options.minimumRadius - + this.spherical.radius + ) + ) + } + /** Apply values and update transform */ this._targetCamera.position.copy(position) this._targetCamera.quaternion.copy(quaternion) this._targetCamera.updateMatrixWorld(true) + + /** Fov update */ if (this._targetCamera instanceof PerspectiveCamera) if (this._targetCamera.fov !== Math.exp(this.logFov)) { this._targetCamera.fov = Math.exp(this.logFov) this._targetCamera.updateProjectionMatrix() } + + /** Compute the correct orthographic size based on the polar radius */ if (this._targetCamera instanceof OrthographicCamera) { const orthographicSize = computeOrthographicSize( this.spherical.radius, @@ -762,8 +787,7 @@ export class SmoothOrbitControls extends SpeckleControls { this._targetCamera.updateProjectionMatrix() } - this.lastPivotPoint.copy(this.pivotPoint) - + /** Update the debug origin sphere */ this.orbitSphere.position.copy( this._options.orbitAroundCursor ? this.pivotPoint @@ -771,13 +795,14 @@ export class SmoothOrbitControls extends SpeckleControls { ) } - // Ortho height to distance functions + /* + // Ortho height to distance function. Keeping for reference private orthographicHeightToDistance(height: number) { if (!(this._targetCamera instanceof OrthographicCamera)) return this.spherical.radius return height / (Math.tan(MathUtils.DEG2RAD * Math.exp(this.logFov) * 0.5) * 2) - } + }*/ /** Three.js Spherical assumes (0, 1, 0) as up... */ protected positionFromSpherical(spherical: Spherical, origin?: Vector3) { @@ -985,8 +1010,6 @@ export class SmoothOrbitControls extends SpeckleControls { ) if (res) { this.pivotPoint.copy(res[0].point) - this.polarFromPivotal() - this.usePivotal = true } } From fc1cf444dca3fbab8cbccb911fdb7dbe2aeba361 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Fri, 10 Jan 2025 17:03:33 +0200 Subject: [PATCH 14/28] feat(viewer-lib): Mouse orbiting now takes clipping planes into consideration --- packages/viewer/src/modules/LegacyViewer.ts | 3 +-- .../modules/extensions/CameraController.ts | 3 +-- .../controls/SmoothOrbitControls.ts | 23 ++++++++----------- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/viewer/src/modules/LegacyViewer.ts b/packages/viewer/src/modules/LegacyViewer.ts index 4af3f9a499..c9c6b2f177 100644 --- a/packages/viewer/src/modules/LegacyViewer.ts +++ b/packages/viewer/src/modules/LegacyViewer.ts @@ -44,7 +44,6 @@ import { BatchObject } from './batching/BatchObject.js' import { SpeckleLoader } from './loaders/Speckle/SpeckleLoader.js' import Logger from './utils/Logger.js' import { ViewModes } from './extensions/ViewModes.js' -import { HybridCameraController } from './extensions/HybridCameraController.js' class LegacySelectionExtension extends SelectionExtension { /** FE2 'manually' selects objects pon it's own, so we're disabling the extension's event handler @@ -121,7 +120,7 @@ export class LegacyViewer extends Viewer { params: ViewerParams = DefaultViewerParams ) { super(container, params) - this.cameraController = this.createExtension(HybridCameraController) + this.cameraController = this.createExtension(CameraController) this.selection = this.createExtension(LegacySelectionExtension) this.sections = this.createExtension(SectionTool) this.createExtension(SectionOutlines) diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index 92b066edc2..579b080fda 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -196,8 +196,7 @@ export class CameraController extends Extension implements SpeckleCamera { this.perspectiveCamera, this.viewer.getContainer(), this.viewer.World, - this.viewer.getRenderer().scene, - this.viewer.getRenderer().intersections, + this.viewer.getRenderer(), this._options ) orbitControls.enabled = true diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index a7dea36384..9ce4be6545 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -24,7 +24,6 @@ import { OrthographicCamera, Quaternion, Euler, - Scene, Mesh, SphereGeometry } from 'three' @@ -33,11 +32,11 @@ import { Damper, SETTLING_TIME } from '../../utils/Damper.js' import { World } from '../../World.js' import { SpeckleControls } from './SpeckleControls.js' -import { Intersections } from '../../Intersections.js' import { lerp } from 'three/src/math/MathUtils.js' import { computeOrthographicSize } from '../CameraController.js' import { ObjectLayers } from '../../../IViewer.js' import SpeckleBasicMaterial from '../../materials/SpeckleBasicMaterial.js' +import SpeckleRenderer from '../../SpeckleRenderer.js' /** * @param {Number} value @@ -172,9 +171,8 @@ export class SmoothOrbitControls extends SpeckleControls { protected _basisTransformInv: Matrix4 = new Matrix4() protected _radiusDelta: number = 0 - protected scene: Scene protected world: World - protected intersections: Intersections + protected renderer: SpeckleRenderer protected orbitSphere: Mesh protected pivotPoint: Vector3 = new Vector3() @@ -208,16 +206,14 @@ export class SmoothOrbitControls extends SpeckleControls { camera: PerspectiveCamera | OrthographicCamera, container: HTMLElement, world: World, - scene: Scene, - intersections: Intersections, + renderer: SpeckleRenderer, options: Required ) { super() this._targetCamera = camera this._container = container this.world = world - this.intersections = intersections - this.scene = scene + this.renderer = renderer this._options = Object.assign({}, options) this.setDamperDecayTime(this._options.damperDecay) @@ -234,7 +230,7 @@ export class SmoothOrbitControls extends SpeckleControls { this.orbitSphere = new Mesh(new SphereGeometry(0.5, 32, 16), billboardMaterial) this.orbitSphere.layers.set(ObjectLayers.OVERLAY) this.orbitSphere.visible = false - this.scene.add(this.orbitSphere) + this.renderer.scene.add(this.orbitSphere) } /** @@ -339,7 +335,6 @@ export class SmoothOrbitControls extends SpeckleControls { // polar, azimuth and radius: this.setOrbit() this.setFieldOfView(Math.exp(this.goalLogFov)) - this.orbitSphere.visible = this._options.showOrbitPoint } /** Computes min/max radius values based on the current world size */ @@ -1000,15 +995,15 @@ export class SmoothOrbitControls extends SpeckleControls { ((event.clientY - this._container.offsetTop) / this._container.offsetHeight) * -2 + 1 - const res = this.intersections.intersect( - this.scene, + const res = this.renderer.intersections.intersect( + this.renderer.scene, this._targetCamera as PerspectiveCamera, new Vector2(x, y), ObjectLayers.STREAM_CONTENT_MESH, true, - this.world.worldBox // TO DO: This does not account for transformed objects + this.renderer.clippingVolume ) - if (res) { + if (res && res.length) { this.pivotPoint.copy(res[0].point) this.usePivotal = true } From 87208740e5305d18520493f2fa27693b272fa780 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Fri, 10 Jan 2025 17:30:35 +0200 Subject: [PATCH 15/28] chore(viewer-lib): Fixed sandbox build error --- packages/viewer-sandbox/src/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 62ec7ffff2..1a4cbf3749 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -4,7 +4,6 @@ import { SelectionEvent, ViewerEvent, Viewer, - HybridCameraController, ViewModes, SelectionExtension, CameraController From b40e99e779e6881be0532522f525bb84a0a85e04 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Mon, 13 Jan 2025 15:15:00 +0200 Subject: [PATCH 16/28] fix(viewer-lib): Handled WEB-2449 and WEB-2450 Additionally fixed an issue where changing the orbit pivot would trigger a hard render, adding the unneeded noise of AO re-convergence --- .../extensions/controls/SmoothOrbitControls.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index 9ce4be6545..0728e8171d 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -48,6 +48,7 @@ const clamp = (value: number, lowerLimit: number, upperLimit: number): number => Math.max(lowerLimit, Math.min(upperLimit, value)) const PAN_SENSITIVITY = 0.018 +const MOVEMENT_EPSILON = 1e-5 const vector3 = new Vector3() export type TouchMode = null | ((dx: number, dy: number) => void) @@ -626,9 +627,8 @@ export class SmoothOrbitControls extends SpeckleControls { normalization ) this.origin.set(x, y, z) - this.moveCamera() - return true + return this.moveCamera() } /** Function expects the position argument to be in a CS where Y is up */ @@ -698,7 +698,10 @@ export class SmoothOrbitControls extends SpeckleControls { return pivotalOrigin } - protected moveCamera() { + protected moveCamera(): boolean { + const lastCameraPos = new Vector3().copy(this._targetCamera.position) + const lastCameraQuat = new Quaternion().copy(this._targetCamera.quaternion) + this.spherical.makeSafe() /** We get the current position and rotation based off the latest polar params @@ -784,10 +787,15 @@ export class SmoothOrbitControls extends SpeckleControls { /** Update the debug origin sphere */ this.orbitSphere.position.copy( - this._options.orbitAroundCursor + this._options.orbitAroundCursor && this.usePivotal ? this.pivotPoint : new Vector3().copy(this.origin).applyMatrix4(this._basisTransform) ) + + return ( + lastCameraPos.sub(this._targetCamera.position).length() > MOVEMENT_EPSILON || + lastCameraQuat.angleTo(this._targetCamera.quaternion) > MOVEMENT_EPSILON + ) } /* From 5de1bc5883b2bf26fabbc9e7df0cbce7a9aee444 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Mon, 13 Jan 2025 19:33:18 +0200 Subject: [PATCH 17/28] fix(viewer-lib): Fixed the issue with focusing and other camera animations caused by the introduction on the pivotal CS. Pivot sphere now shows only on orbit --- .../src/modules/extensions/controls/SmoothOrbitControls.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index 0728e8171d..841351578c 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -277,6 +277,7 @@ export class SmoothOrbitControls extends SpeckleControls { /** Three.js Spherical assumes (0, 1, 0) as up... */ v1.applyMatrix4(this._basisTransformInv) this.setTarget(v1.x, v1.y, v1.z) + this.usePivotal = false } /** @@ -295,6 +296,7 @@ export class SmoothOrbitControls extends SpeckleControls { this.setTarget(nativeOrigin.x, nativeOrigin.y, nativeOrigin.z) this.setRadius(sphere.radius) + this.usePivotal = false } /** @@ -990,6 +992,7 @@ export class SmoothOrbitControls extends SpeckleControls { target.add(dxy.applyMatrix3(this.panProjection)) this.setTarget(target.x, target.y, target.z) this.usePivotal = false + this.orbitSphere.visible = false } protected onPointerDown = (event: PointerEvent) => { @@ -1149,6 +1152,7 @@ export class SmoothOrbitControls extends SpeckleControls { (event.button === 2 || event.ctrlKey || event.metaKey || event.shiftKey) ) { this.initializePan() + this.orbitSphere.visible = false } // this.element.style.cursor = 'grabbing' } @@ -1175,6 +1179,7 @@ export class SmoothOrbitControls extends SpeckleControls { this.userAdjustOrbit(0, 0, deltaZoom) event.preventDefault() this.usePivotal = false + this.orbitSphere.visible = false // TO DO // this.dispatchEvent({ type: 'user-interaction' }) } From 572ae38cccb07461a6065469bffb32163774386c Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Tue, 14 Jan 2025 12:17:23 +0200 Subject: [PATCH 18/28] feat(viewer-lib): Updates on mouse orbiting: - When clicking outside of the model, oribitig will switch to polar and use the last computed origin (which is still going to be based on the last pivot point) Made the pivot sphere speckle blue The pivot sphere now only shows when clicking on objects, when clicking outside of the model it will not show --- .../src/modules/extensions/controls/SmoothOrbitControls.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index 841351578c..b1931bdb11 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -218,7 +218,7 @@ export class SmoothOrbitControls extends SpeckleControls { this._options = Object.assign({}, options) this.setDamperDecayTime(this._options.damperDecay) - const billboardMaterial = new SpeckleBasicMaterial({ color: 0x43af11 }, [ + const billboardMaterial = new SpeckleBasicMaterial({ color: 0x047efb }, [ 'BILLBOARD_FIXED' ]) billboardMaterial.opacity = 0.75 @@ -1017,9 +1017,12 @@ export class SmoothOrbitControls extends SpeckleControls { if (res && res.length) { this.pivotPoint.copy(res[0].point) this.usePivotal = true + this.orbitSphere.visible = this._options.showOrbitPoint + } else { + this.usePivotal = false + this.orbitSphere.visible = false } } - this.orbitSphere.visible = this._options.showOrbitPoint if (this.pointers.length > 2) { return From 9be15850bc8083a3e4a8e1fb530af965d8e9f18c Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Tue, 14 Jan 2025 15:39:38 +0200 Subject: [PATCH 19/28] feat(viewer-lib): Update for WASD aka fly mode: - Smoother combined navigation by using the immediate controler position and orientation as opposed the the goal ones - Pivot sphere properly hides when in fly mode - The bug where the camera would incorrectly jump when toggling between fly and oribit is now gone (or I cannot reproduce it anymore) --- packages/viewer-sandbox/src/main.ts | 5 +++-- .../modules/extensions/CameraController.ts | 4 ++-- .../extensions/controls/FlyControls.ts | 21 +++++++++++++++++++ .../controls/SmoothOrbitControls.ts | 21 ++++++++++++++++++- .../extensions/controls/SpeckleControls.ts | 2 ++ 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 1a4cbf3749..310399eccf 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -6,7 +6,8 @@ import { Viewer, ViewModes, SelectionExtension, - CameraController + CameraController, + HybridCameraController } from '@speckle/viewer' import './style.css' @@ -45,7 +46,7 @@ const createViewer = async (containerName: string, _stream: string) => { const viewer: Viewer = new Viewer(container, params) await viewer.init() - const cameraController = viewer.createExtension(CameraController) + const cameraController = viewer.createExtension(HybridCameraController) const selection = viewer.createExtension(SelectionExtension) const sections = viewer.createExtension(SectionTool) viewer.createExtension(SectionOutlines) diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index 579b080fda..f4b24ab33a 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -249,8 +249,8 @@ export class CameraController extends Extension implements SpeckleCamera { oldControls.enabled = false newControls.enabled = true newControls.fromPositionAndTarget( - oldControls.getPosition(), - oldControls.getTarget() + oldControls.getCurrentPosition(), + oldControls.getCurrentTarget() ) newControls.jumpToGoal() this._activeControls = newControls diff --git a/packages/viewer/src/modules/extensions/controls/FlyControls.ts b/packages/viewer/src/modules/extensions/controls/FlyControls.ts index ebe8ef0623..1c8d2838f4 100644 --- a/packages/viewer/src/modules/extensions/controls/FlyControls.ts +++ b/packages/viewer/src/modules/extensions/controls/FlyControls.ts @@ -231,6 +231,27 @@ class FlyControls extends SpeckleControls { return new Vector3().copy(this.goalPosition) } + /** + * Gets the current goal position + */ + public getCurrentPosition(): Vector3 { + return this.position + } + + /** + * Gets the point in model coordinates the model should orbit/pivot around. + */ + public getCurrentTarget(): Vector3 { + const target = new Vector3().copy(this.position) + const matrix = new Matrix4().makeRotationFromEuler(this.euler) + const forward = new Vector3() + .setFromMatrixColumn(matrix, 2) + .applyMatrix4(this._basisTransform) + .normalize() + target.addScaledVector(forward, -this.world.getRelativeOffset(0.2)) + return target + } + /** * Sets the smoothing decay time. */ diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index b1931bdb11..5f4fc452d6 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -186,7 +186,10 @@ export class SmoothOrbitControls extends SpeckleControls { public set enabled(value: boolean) { if (value) { this.enableInteraction() - } else this.disableInteraction() + } else { + this.disableInteraction() + this.orbitSphere.visible = false + } this._enabled = value } @@ -315,6 +318,22 @@ export class SmoothOrbitControls extends SpeckleControls { return this.goalOrigin.clone().applyMatrix4(this._basisTransform) } + /** + * Gets the current goal position + */ + public getCurrentPosition(): Vector3 { + return this.positionFromSpherical(this.spherical, this.origin).applyMatrix4( + this._basisTransform + ) + } + + /** + * Gets the point in model coordinates the model should orbit/pivot around. + */ + public getCurrentTarget(): Vector3 { + return this.origin.clone().applyMatrix4(this._basisTransform) + } + public isStationary(): boolean { return ( this.goalSpherical.theta === this.spherical.theta && diff --git a/packages/viewer/src/modules/extensions/controls/SpeckleControls.ts b/packages/viewer/src/modules/extensions/controls/SpeckleControls.ts index 37eea47e42..e7fbf29f17 100644 --- a/packages/viewer/src/modules/extensions/controls/SpeckleControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SpeckleControls.ts @@ -26,4 +26,6 @@ export abstract class SpeckleControls extends EventEmitter { abstract fromPositionAndTarget(position: Vector3, target: Vector3): void abstract getTarget(): Vector3 abstract getPosition(): Vector3 + abstract getCurrentTarget(): Vector3 + abstract getCurrentPosition(): Vector3 } From c6d71a8e623d29bc8d60b8498e6d40c2fce402ce Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Tue, 14 Jan 2025 16:14:17 +0200 Subject: [PATCH 20/28] fix(viewer-lib): Fixed sandbox compile error --- packages/viewer-sandbox/src/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 310399eccf..0ed06e5d55 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -6,7 +6,6 @@ import { Viewer, ViewModes, SelectionExtension, - CameraController, HybridCameraController } from '@speckle/viewer' From 74b6ddca8ecdb782ca157deac49bafcf18802af7 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Tue, 14 Jan 2025 18:05:35 +0200 Subject: [PATCH 21/28] feat(viewer-lib): Added the hybrid fly orbit controller to the legacy viewer --- packages/viewer/src/modules/LegacyViewer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/viewer/src/modules/LegacyViewer.ts b/packages/viewer/src/modules/LegacyViewer.ts index c9c6b2f177..4af3f9a499 100644 --- a/packages/viewer/src/modules/LegacyViewer.ts +++ b/packages/viewer/src/modules/LegacyViewer.ts @@ -44,6 +44,7 @@ import { BatchObject } from './batching/BatchObject.js' import { SpeckleLoader } from './loaders/Speckle/SpeckleLoader.js' import Logger from './utils/Logger.js' import { ViewModes } from './extensions/ViewModes.js' +import { HybridCameraController } from './extensions/HybridCameraController.js' class LegacySelectionExtension extends SelectionExtension { /** FE2 'manually' selects objects pon it's own, so we're disabling the extension's event handler @@ -120,7 +121,7 @@ export class LegacyViewer extends Viewer { params: ViewerParams = DefaultViewerParams ) { super(container, params) - this.cameraController = this.createExtension(CameraController) + this.cameraController = this.createExtension(HybridCameraController) this.selection = this.createExtension(LegacySelectionExtension) this.sections = this.createExtension(SectionTool) this.createExtension(SectionOutlines) From dd4556a207777bdd6878dd6a41e9961f9b52963a Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Mon, 20 Jan 2025 17:44:02 +0200 Subject: [PATCH 22/28] feat(viewer-lib): Added a slower movement speed to WASD navigation when camera is close to geometry --- .../src/modules/extensions/CameraController.ts | 1 + .../modules/extensions/controls/FlyControls.ts | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index f4b24ab33a..dc8f8dbc77 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -425,6 +425,7 @@ export class CameraController extends Extension implements SpeckleCamera { fallback?: number ): number | undefined { const minDist = this.getClosestGeometryDistance(fallback) + ;(this._controlsList[1] as FlyControls).minDist = minDist if (minDist === Number.POSITIVE_INFINITY) { return this.computeNearCameraPlaneEmpiric(targetVolume, offsetScale) } diff --git a/packages/viewer/src/modules/extensions/controls/FlyControls.ts b/packages/viewer/src/modules/extensions/controls/FlyControls.ts index 1c8d2838f4..f978d0b370 100644 --- a/packages/viewer/src/modules/extensions/controls/FlyControls.ts +++ b/packages/viewer/src/modules/extensions/controls/FlyControls.ts @@ -18,6 +18,8 @@ const _changeEvent = { type: 'change' } const _PI_2 = Math.PI / 2 type MoveType = 'forward' | 'back' | 'left' | 'right' | 'up' | 'down' const walkingSpeed = 1.42 // m/s +const closeRelativeFactor = 0.03 +const farRelativeFactor = 0.2 export interface FlyControlsOptions { [name: string]: unknown @@ -56,6 +58,8 @@ class FlyControls extends SpeckleControls { private _basisTransform: Matrix4 = new Matrix4() private _basisTransformInv: Matrix4 = new Matrix4() + protected _minDist: number + private world: World public get enabled(): boolean { @@ -95,6 +99,10 @@ class FlyControls extends SpeckleControls { this._basisTransformInv.invert() } + public set minDist(value: number) { + this._minDist = value + } + constructor( camera: PerspectiveCamera | OrthographicCamera, container: HTMLElement, @@ -126,8 +134,14 @@ class FlyControls extends SpeckleControls { if (!this._enabled) return false + let relativeFactor = this.world.getRelativeOffset(farRelativeFactor) + if (this._minDist) { + if (this._minDist < relativeFactor * 0.5) + relativeFactor = this.world.getRelativeOffset(closeRelativeFactor) + } + const deltaSeconds = delta / 1000 - const scaledWalkingSpeed = this.world.getRelativeOffset(0.2) * walkingSpeed + const scaledWalkingSpeed = relativeFactor * walkingSpeed if (this.keyMap.forward) this.velocity.z = -scaledWalkingSpeed * this._options.moveSpeed * deltaSeconds From ebf2322b2701fd4abd8e2b01dd9b2b901fd4fe2b Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Tue, 21 Jan 2025 15:22:27 +0200 Subject: [PATCH 23/28] fix(viewer-lib): Fixed the issue where opening the context menu while holding down a WASD key would make the camera move indefinetely --- .../extensions/HybridCameraController.ts | 33 +++++++++++++++++++ .../extensions/controls/FlyControls.ts | 24 ++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/packages/viewer/src/modules/extensions/HybridCameraController.ts b/packages/viewer/src/modules/extensions/HybridCameraController.ts index 914d039fa5..208b9125ab 100644 --- a/packages/viewer/src/modules/extensions/HybridCameraController.ts +++ b/packages/viewer/src/modules/extensions/HybridCameraController.ts @@ -12,10 +12,25 @@ export class HybridCameraController extends CameraController { up: false, down: false } + + protected contextMenuTriggered = false + public constructor(viewer: IViewer) { super(viewer) document.addEventListener('keydown', this.onKeyDown.bind(this)) document.addEventListener('keyup', this.onKeyUp.bind(this)) + document.addEventListener('contextmenu', this.onContextMenu.bind(this)) + } + + public onEarlyUpdate(_delta?: number): void { + super.onEarlyUpdate(_delta) + /** We do this because sometimes while holding a kewy down you get an extra + * key down event **after** the context menu event, locking it in place + */ + if (this.contextMenuTriggered) { + this.cancelMove() + this.contextMenuTriggered = false + } } protected onKeyDown(event: KeyboardEvent) { @@ -106,4 +121,22 @@ export class HybridCameraController extends CameraController { ) this.toggleControls() } + + protected onContextMenu() { + this.contextMenuTriggered = true + } + + protected cancelMove() { + this.keyMap.back = false + this.keyMap.forward = false + this.keyMap.down = false + this.keyMap.up = false + this.keyMap.left = false + this.keyMap.right = false + if ( + this._controlsList[1].enabled && + Object.values(this.keyMap).every((v) => v === false) + ) + this.toggleControls() + } } diff --git a/packages/viewer/src/modules/extensions/controls/FlyControls.ts b/packages/viewer/src/modules/extensions/controls/FlyControls.ts index f978d0b370..2b12e176bf 100644 --- a/packages/viewer/src/modules/extensions/controls/FlyControls.ts +++ b/packages/viewer/src/modules/extensions/controls/FlyControls.ts @@ -46,6 +46,7 @@ class FlyControls extends SpeckleControls { up: false, down: false } + protected contextMenuTriggered = false protected eulerXDamper: Damper = new Damper() protected eulerYDamper: Damper = new Damper() @@ -128,6 +129,14 @@ class FlyControls extends SpeckleControls { } public update(delta?: number): boolean { + /** We do this because sometimes while holding a kewy down you get an extra + * key down event **after** the context menu event, locking it in place + */ + if (this.contextMenuTriggered) { + this.cancelMove() + this.contextMenuTriggered = false + } + const now = performance.now() delta = delta !== undefined ? delta : now - this._lastTick this._lastTick = now @@ -306,6 +315,7 @@ class FlyControls extends SpeckleControls { this.container.addEventListener('pointermove', this.onMouseMove) document.addEventListener('keydown', this.onKeyDown) document.addEventListener('keyup', this.onKeyUp) + document.addEventListener('contextmenu', this.onContextMenu) } protected disconnect() { @@ -314,6 +324,7 @@ class FlyControls extends SpeckleControls { this.container.removeEventListener('pointermove', this.onMouseMove) document.removeEventListener('keydown', this.onKeyDown) document.removeEventListener('keyup', this.onKeyUp) + document.removeEventListener('contextmenu', this.onContextMenu) for (const k in this.keyMap) this.keyMap[k as MoveType] = false } @@ -412,5 +423,18 @@ class FlyControls extends SpeckleControls { break } } + + protected onContextMenu = () => { + this.contextMenuTriggered = true + } + + protected cancelMove() { + this.keyMap.forward = false + this.keyMap.left = false + this.keyMap.back = false + this.keyMap.right = false + this.keyMap.up = false + this.keyMap.down = false + } } export { FlyControls } From 59b5e15b2e3694e9164c87cd974fa0bb55e99f2e Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Wed, 22 Jan 2025 18:49:00 +0200 Subject: [PATCH 24/28] Feat(viewer-lib): Update to WASD controls: - Disabled cursor orbiting - Added an option to allow for world space up/down with e/q keys. By default it's enabled - Fixed the pan speed to work similar to WASD speed in two steps depending how close the camera is to geometry --- .../modules/extensions/CameraController.ts | 5 +++- .../extensions/controls/FlyControls.ts | 17 +++++++++-- .../controls/SmoothOrbitControls.ts | 30 ++++++++++++++----- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index dc8f8dbc77..3750cf79c2 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -94,12 +94,13 @@ export const DefaultOrbitControlsOptions: Required = { touchAction: 'none', infiniteZoom: true, zoomToCursor: true, - orbitAroundCursor: true, + orbitAroundCursor: false, showOrbitPoint: true, lookSpeed: 1, moveSpeed: 1, damperDecay: 30, enableLook: true, + relativeUpDown: false, nearPlaneCalculation: NearPlaneCalculation.ACCURATE } @@ -248,6 +249,7 @@ export class CameraController extends Extension implements SpeckleCamera { oldControls.enabled = false newControls.enabled = true + newControls.fromPositionAndTarget( oldControls.getCurrentPosition(), oldControls.getCurrentTarget() @@ -425,6 +427,7 @@ export class CameraController extends Extension implements SpeckleCamera { fallback?: number ): number | undefined { const minDist = this.getClosestGeometryDistance(fallback) + ;(this._controlsList[0] as SmoothOrbitControls).minDist = minDist ;(this._controlsList[1] as FlyControls).minDist = minDist if (minDist === Number.POSITIVE_INFINITY) { return this.computeNearCameraPlaneEmpiric(targetVolume, offsetScale) diff --git a/packages/viewer/src/modules/extensions/controls/FlyControls.ts b/packages/viewer/src/modules/extensions/controls/FlyControls.ts index 2b12e176bf..c4e78d6922 100644 --- a/packages/viewer/src/modules/extensions/controls/FlyControls.ts +++ b/packages/viewer/src/modules/extensions/controls/FlyControls.ts @@ -20,6 +20,7 @@ type MoveType = 'forward' | 'back' | 'left' | 'right' | 'up' | 'down' const walkingSpeed = 1.42 // m/s const closeRelativeFactor = 0.03 const farRelativeFactor = 0.2 +const relativeTargetDistance = 0.01 export interface FlyControlsOptions { [name: string]: unknown @@ -27,6 +28,7 @@ export interface FlyControlsOptions { lookSpeed?: number moveSpeed?: number damperDecay?: number + relativeUpDown?: boolean } class FlyControls extends SpeckleControls { @@ -171,6 +173,7 @@ class FlyControls extends SpeckleControls { if (!this.keyMap.down && !this.keyMap.up) this.velocity.y = 0 if (this.isStationary()) return false + this.moveBy(this.velocity) this.updatePositionRotation(delta) @@ -245,7 +248,10 @@ class FlyControls extends SpeckleControls { .setFromMatrixColumn(matrix, 2) .applyMatrix4(this._basisTransform) .normalize() - target.addScaledVector(forward, -this.world.getRelativeOffset(0.2)) + target.addScaledVector( + forward, + -this.world.getRelativeOffset(relativeTargetDistance) + ) return target } @@ -271,7 +277,10 @@ class FlyControls extends SpeckleControls { .setFromMatrixColumn(matrix, 2) .applyMatrix4(this._basisTransform) .normalize() - target.addScaledVector(forward, -this.world.getRelativeOffset(0.2)) + target.addScaledVector( + forward, + -this.world.getRelativeOffset(relativeTargetDistance) + ) return target } @@ -291,7 +300,9 @@ class FlyControls extends SpeckleControls { const camera = this._targetCamera _vectorBuff0.setFromMatrixColumn(camera.matrix, 2) this.goalPosition.addScaledVector(_vectorBuff0, amount.z) - _vectorBuff0.setFromMatrixColumn(camera.matrix, 1) + this._options.relativeUpDown + ? _vectorBuff0.setFromMatrixColumn(camera.matrix, 1) + : _vectorBuff0.copy(this.up) this.goalPosition.addScaledVector(_vectorBuff0, amount.y) _vectorBuff0.setFromMatrixColumn(camera.matrix, 0) this.goalPosition.addScaledVector(_vectorBuff0, amount.x) diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index 5f4fc452d6..589d01c574 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -112,6 +112,9 @@ export enum PointerChangeEvent { PointerChangeEnd = 'pointer-change-end' } +const closeRelativeFactorPan = 0.06 +const farRelativeFactorPan = 0.4 + /** * SmoothControls is a Three.js helper for adding delightful pointer and * keyboard-based input to a staged Three.js scene. Its API is very similar to @@ -180,6 +183,8 @@ export class SmoothOrbitControls extends SpeckleControls { protected lastPivotPoint: Vector3 = new Vector3() protected usePivotal = false + protected _minDist: number + public get enabled(): boolean { return this._enabled } @@ -261,6 +266,10 @@ export class SmoothOrbitControls extends SpeckleControls { this.moveCamera() } + public set minDist(value: number) { + this._minDist = value + } + /** The input position and target will be in a basis with (0,1,0) as up */ public fromPositionAndTarget(position: Vector3, target: Vector3): void { /** This check is targeted exclusevely towards the frontend which calls this method pointlessly each frame @@ -812,6 +821,7 @@ export class SmoothOrbitControls extends SpeckleControls { ? this.pivotPoint : new Vector3().copy(this.origin).applyMatrix4(this._basisTransform) ) + this.orbitSphere.visible = true return ( lastCameraPos.sub(this._targetCamera.position).length() > MOVEMENT_EPSILON || @@ -996,14 +1006,20 @@ export class SmoothOrbitControls extends SpeckleControls { protected movePan(dx: number, dy: number) { const dxy = vector3.set(dx, dy, 0).multiplyScalar(this._options.inputSensitivity) + let relativeFactor = this.world.getRelativeOffset(farRelativeFactorPan) + if (this._minDist) { + if (this._minDist < relativeFactor * 0.5) { + relativeFactor = this.world.getRelativeOffset(closeRelativeFactorPan) + } + } + + const radiusFactor = clamp( + this.spherical.radius, + this.world.getRelativeOffset(0.025), + Number.MAX_VALUE + ) const metersPerPixel = - clamp( - this.spherical.radius, - this.world.getRelativeOffset(0.025), - Number.MAX_VALUE - ) * - Math.exp(this.logFov) * - this.panPerPixel + Math.max(relativeFactor, radiusFactor) * Math.exp(this.logFov) * this.panPerPixel dxy.multiplyScalar(metersPerPixel) /** This panProjection assumes (0, 1, 0) as up... */ From e74227ef178ffac835ffc440a984120fc30a5eb0 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Wed, 22 Jan 2025 19:05:25 +0200 Subject: [PATCH 25/28] chore(viewer-lib): Tidied up a bit --- .../modules/extensions/CameraController.ts | 37 ++++++++----------- .../extensions/HybridCameraController.ts | 18 ++------- .../extensions/controls/FlyControls.ts | 6 +-- .../controls/SmoothOrbitControls.ts | 7 ++-- .../extensions/controls/SpeckleControls.ts | 10 +++++ 5 files changed, 36 insertions(+), 42 deletions(-) diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index 3750cf79c2..169c1917c5 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -23,9 +23,6 @@ import { SmoothOrbitControls } from './controls/SmoothOrbitControls.js' -// const UP: Vector3 = new Vector3(0, 1, 0) -// const quatBuff = new Quaternion() - export enum NearPlaneCalculation { EMPIRIC, ACCURATE @@ -111,7 +108,8 @@ export class CameraController extends Extension implements SpeckleCamera { protected _lastCameraChanged: boolean = false protected _options: Required = DefaultOrbitControlsOptions protected _activeControls: SpeckleControls - protected _controlsList: SpeckleControls[] = [] + protected _orbitControls: SmoothOrbitControls + protected _flyControls: FlyControls get renderingCamera(): PerspectiveCamera | OrthographicCamera { return this._renderingCamera @@ -152,9 +150,8 @@ export class CameraController extends Extension implements SpeckleCamera { public set options(value: CameraControllerOptions) { Object.assign(this._options, value) - this._controlsList.forEach((controls: SpeckleControls) => { - controls.options = value - }) + this._orbitControls.options = value + this._flyControls.options = value } public constructor(viewer: IViewer) { @@ -183,31 +180,28 @@ export class CameraController extends Extension implements SpeckleCamera { /** Perspective camera as default on startup */ this.renderingCamera = this.perspectiveCamera - const flyControls = new FlyControls( + this._flyControls = new FlyControls( this._renderingCamera, this.viewer.getContainer(), this.viewer.World, this._options ) - flyControls.enabled = false - flyControls.setDamperDecayTime(30) - flyControls.up = new Vector3(0, 0, 1) + this._flyControls.enabled = false + this._flyControls.setDamperDecayTime(30) + this._flyControls.up = new Vector3(0, 0, 1) - const orbitControls = new SmoothOrbitControls( + this._orbitControls = new SmoothOrbitControls( this.perspectiveCamera, this.viewer.getContainer(), this.viewer.World, this.viewer.getRenderer(), this._options ) - orbitControls.enabled = true + this._orbitControls.enabled = true this.viewer.getRenderer().speckleCamera = this - this._controlsList.push(orbitControls) - this._controlsList.push(flyControls) - - this._activeControls = orbitControls + this._activeControls = this._orbitControls this.default() } @@ -240,9 +234,9 @@ export class CameraController extends Extension implements SpeckleCamera { let newControls: SpeckleControls | undefined = undefined if (this._activeControls instanceof SmoothOrbitControls) { - newControls = this._controlsList[1] + newControls = this._flyControls } else if (this._activeControls instanceof FlyControls) { - newControls = this._controlsList[0] + newControls = this._orbitControls } if (!newControls) throw new Error('Not controls found!') @@ -427,8 +421,9 @@ export class CameraController extends Extension implements SpeckleCamera { fallback?: number ): number | undefined { const minDist = this.getClosestGeometryDistance(fallback) - ;(this._controlsList[0] as SmoothOrbitControls).minDist = minDist - ;(this._controlsList[1] as FlyControls).minDist = minDist + this._flyControls.minDist = minDist + this._orbitControls.minDist = minDist + if (minDist === Number.POSITIVE_INFINITY) { return this.computeNearCameraPlaneEmpiric(targetVolume, offsetScale) } diff --git a/packages/viewer/src/modules/extensions/HybridCameraController.ts b/packages/viewer/src/modules/extensions/HybridCameraController.ts index 208b9125ab..ffe632d436 100644 --- a/packages/viewer/src/modules/extensions/HybridCameraController.ts +++ b/packages/viewer/src/modules/extensions/HybridCameraController.ts @@ -1,4 +1,3 @@ -import { clamp } from 'three/src/math/MathUtils.js' import { IViewer } from '../../IViewer.js' import { CameraController } from './CameraController.js' type MoveType = 'forward' | 'back' | 'left' | 'right' | 'up' | 'down' @@ -34,7 +33,6 @@ export class HybridCameraController extends CameraController { } protected onKeyDown(event: KeyboardEvent) { - let moveSpeed = this.options.moveSpeed ? this.options.moveSpeed : 1 switch (event.code) { case 'ArrowUp': case 'KeyW': @@ -65,19 +63,9 @@ export class HybridCameraController extends CameraController { case 'KeyE': this.keyMap.down = true break - case 'KeyF': - moveSpeed += 0.25 - moveSpeed = clamp(moveSpeed, 0.1, 5) - this.options = { moveSpeed } - break - case 'KeyC': - moveSpeed -= 0.25 - moveSpeed = clamp(moveSpeed, 0.1, 5) - this.options = { moveSpeed } - break } if ( - !this._controlsList[1].enabled && + !this._flyControls.enabled && Object.values(this.keyMap).some((v) => v === true) ) this.toggleControls() @@ -116,7 +104,7 @@ export class HybridCameraController extends CameraController { break } if ( - this._controlsList[1].enabled && + this._flyControls.enabled && Object.values(this.keyMap).every((v) => v === false) ) this.toggleControls() @@ -134,7 +122,7 @@ export class HybridCameraController extends CameraController { this.keyMap.left = false this.keyMap.right = false if ( - this._controlsList[1].enabled && + this._flyControls.enabled && Object.values(this.keyMap).every((v) => v === false) ) this.toggleControls() diff --git a/packages/viewer/src/modules/extensions/controls/FlyControls.ts b/packages/viewer/src/modules/extensions/controls/FlyControls.ts index c4e78d6922..6ac95e06ff 100644 --- a/packages/viewer/src/modules/extensions/controls/FlyControls.ts +++ b/packages/viewer/src/modules/extensions/controls/FlyControls.ts @@ -20,7 +20,7 @@ type MoveType = 'forward' | 'back' | 'left' | 'right' | 'up' | 'down' const walkingSpeed = 1.42 // m/s const closeRelativeFactor = 0.03 const farRelativeFactor = 0.2 -const relativeTargetDistance = 0.01 +const relativeMinTargetDistance = 0.01 export interface FlyControlsOptions { [name: string]: unknown @@ -250,7 +250,7 @@ class FlyControls extends SpeckleControls { .normalize() target.addScaledVector( forward, - -this.world.getRelativeOffset(relativeTargetDistance) + -this.world.getRelativeOffset(relativeMinTargetDistance) ) return target } @@ -279,7 +279,7 @@ class FlyControls extends SpeckleControls { .normalize() target.addScaledVector( forward, - -this.world.getRelativeOffset(relativeTargetDistance) + -this.world.getRelativeOffset(relativeMinTargetDistance) ) return target } diff --git a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts index 589d01c574..3fde991853 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -114,6 +114,8 @@ export enum PointerChangeEvent { const closeRelativeFactorPan = 0.06 const farRelativeFactorPan = 0.4 +const relativeMinTargetDistance = 0.01 +const relativeMaxTargetDistance = 0.2 /** * SmoothControls is a Three.js helper for adding delightful pointer and @@ -513,8 +515,8 @@ export class SmoothOrbitControls extends SpeckleControls { ) worldSizeOffset = clamp( worldSizeOffset, - this.world.getRelativeOffset(0.01), - this.world.getRelativeOffset(0.2) + this.world.getRelativeOffset(relativeMinTargetDistance), + this.world.getRelativeOffset(relativeMaxTargetDistance) ) const zoomAmount = worldSizeOffset * Math.sign(deltaZoom) //deltaZoom * this.spherical.radius * Math.tan(fov * 0.5) @@ -821,7 +823,6 @@ export class SmoothOrbitControls extends SpeckleControls { ? this.pivotPoint : new Vector3().copy(this.origin).applyMatrix4(this._basisTransform) ) - this.orbitSphere.visible = true return ( lastCameraPos.sub(this._targetCamera.position).length() > MOVEMENT_EPSILON || diff --git a/packages/viewer/src/modules/extensions/controls/SpeckleControls.ts b/packages/viewer/src/modules/extensions/controls/SpeckleControls.ts index e7fbf29f17..4fff1f8585 100644 --- a/packages/viewer/src/modules/extensions/controls/SpeckleControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SpeckleControls.ts @@ -3,6 +3,8 @@ import EventEmitter from '../../EventEmitter.js' export abstract class SpeckleControls extends EventEmitter { protected _up: Vector3 = new Vector3(0, 1, 0) + protected _minDist: number = 0 + public get up() { return this._up } @@ -10,6 +12,14 @@ export abstract class SpeckleControls extends EventEmitter { this._up.copy(value) } + public get minDist() { + return this._minDist + } + + public set minDist(value: number) { + this._minDist = value + } + abstract get options(): Partial> abstract set options(value: Partial>) From 593768c77e3a7d666ac7de272cdff53776237c4f Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Thu, 23 Jan 2025 01:23:09 +0200 Subject: [PATCH 26/28] fix(viewer-lib): Fixed an ugly bug where the camera distance calculataion plane would flip, especially when WASD-ing, and mess up the min distance calculation which led the camera near plane to be way off --- packages/viewer/src/modules/extensions/CameraController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index 169c1917c5..dafb2a0b76 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -472,8 +472,8 @@ export class CameraController extends Extension implements SpeckleCamera { } protected getClosestGeometryDistance(fallback?: number): number { - const cameraPosition = this._renderingCamera.position - const cameraTarget = this.getTarget() + const cameraPosition = this._activeControls.getCurrentPosition() + const cameraTarget = this._activeControls.getCurrentTarget() const cameraDir = new Vector3().subVectors(cameraTarget, cameraPosition).normalize() const batches = this.viewer From ead9c1fa07693808f98a6eb538986d0d2790be59 Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Thu, 23 Jan 2025 11:56:52 +0200 Subject: [PATCH 27/28] chore(viewer-lib): Swapped E to up and Q to down --- .../src/modules/extensions/HybridCameraController.ts | 8 ++++---- .../viewer/src/modules/extensions/controls/FlyControls.ts | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/viewer/src/modules/extensions/HybridCameraController.ts b/packages/viewer/src/modules/extensions/HybridCameraController.ts index ffe632d436..1202714f76 100644 --- a/packages/viewer/src/modules/extensions/HybridCameraController.ts +++ b/packages/viewer/src/modules/extensions/HybridCameraController.ts @@ -55,12 +55,12 @@ export class HybridCameraController extends CameraController { break case 'PageUp': - case 'KeyQ': + case 'KeyE': this.keyMap.up = true break case 'PageDown': - case 'KeyE': + case 'KeyQ': this.keyMap.down = true break } @@ -94,12 +94,12 @@ export class HybridCameraController extends CameraController { break case 'PageUp': - case 'KeyQ': + case 'KeyE': this.keyMap.up = false break case 'PageDown': - case 'KeyE': + case 'KeyQ': this.keyMap.down = false break } diff --git a/packages/viewer/src/modules/extensions/controls/FlyControls.ts b/packages/viewer/src/modules/extensions/controls/FlyControls.ts index 6ac95e06ff..7e42c11dbb 100644 --- a/packages/viewer/src/modules/extensions/controls/FlyControls.ts +++ b/packages/viewer/src/modules/extensions/controls/FlyControls.ts @@ -390,12 +390,12 @@ class FlyControls extends SpeckleControls { break case 'PageUp': - case 'KeyQ': + case 'KeyE': this.keyMap.up = true break case 'PageDown': - case 'KeyE': + case 'KeyQ': this.keyMap.down = true break } @@ -424,12 +424,12 @@ class FlyControls extends SpeckleControls { break case 'PageUp': - case 'KeyQ': + case 'KeyE': this.keyMap.up = false break case 'PageDown': - case 'KeyE': + case 'KeyQ': this.keyMap.down = false break } From a2c04d8da652d48b1a8db78fb008349789d2816f Mon Sep 17 00:00:00 2001 From: AlexandruPopovici Date: Wed, 29 Jan 2025 10:11:46 +0200 Subject: [PATCH 28/28] Re-nabled cursor orbiting --- packages/viewer/src/modules/extensions/CameraController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index dafb2a0b76..150808a821 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -91,7 +91,7 @@ export const DefaultOrbitControlsOptions: Required = { touchAction: 'none', infiniteZoom: true, zoomToCursor: true, - orbitAroundCursor: false, + orbitAroundCursor: true, showOrbitPoint: true, lookSpeed: 1, moveSpeed: 1,