diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index a936e51d98..1a4cbf3749 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -4,9 +4,9 @@ import { SelectionEvent, ViewerEvent, Viewer, - CameraController, ViewModes, - SelectionExtension + SelectionExtension, + CameraController } from '@speckle/viewer' import './style.css' @@ -20,6 +20,7 @@ import { import { SectionTool } from '@speckle/viewer' import { SectionOutlines } from '@speckle/viewer' import { ViewModesKeys } from './Extensions/ViewModesKeys' +// import { JSONSpeckleStream } from './JSONSpeckleStream' import { BoxSelection } from './Extensions/BoxSelection' const createViewer = async (containerName: string, _stream: string) => { @@ -111,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' @@ -456,10 +457,12 @@ const getStream = () => { // Perfectly flat // 'https://app.speckle.systems/projects/344f803f81/models/5582ab673e' + // big baker + // 'https://latest.speckle.systems/projects/126cd4b7bb/models/032d09f716' // '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/Viewer.ts b/packages/viewer/src/modules/Viewer.ts index d0f77a118f..e8d742a3c4 100644 --- a/packages/viewer/src/modules/Viewer.ts +++ b/packages/viewer/src/modules/Viewer.ts @@ -202,7 +202,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 da4783164a..579b080fda 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -94,6 +94,8 @@ export const DefaultOrbitControlsOptions: Required = { touchAction: 'none', infiniteZoom: true, zoomToCursor: true, + orbitAroundCursor: true, + showOrbitPoint: true, lookSpeed: 1, moveSpeed: 1, damperDecay: 30, @@ -194,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 @@ -295,8 +296,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/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..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 { @@ -192,14 +198,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 +214,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 +223,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) } /** @@ -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 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 ed254e6bbe..b1931bdb11 100644 --- a/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts +++ b/packages/viewer/src/modules/extensions/controls/SmoothOrbitControls.ts @@ -24,16 +24,19 @@ import { OrthographicCamera, Quaternion, Euler, - Scene + Mesh, + SphereGeometry } from 'three' 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 @@ -45,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) @@ -93,6 +97,10 @@ export interface SmoothOrbitControlsOptions { infiniteZoom?: boolean // Zoom to cursor zoomToCursor?: boolean + // Orbit around cursor + orbitAroundCursor?: boolean + // Show orbit point + showOrbitPoint?: boolean // Dampening damperDecay?: number } @@ -123,49 +131,54 @@ 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 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 + 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 world: World + protected renderer: SpeckleRenderer + + protected orbitSphere: Mesh + protected pivotPoint: Vector3 = new Vector3() + protected lastPivotPoint: Vector3 = new Vector3() + protected usePivotal = false public get enabled(): boolean { return this._enabled @@ -194,20 +207,31 @@ 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) - this.scene - this.intersections + + const billboardMaterial = new SpeckleBasicMaterial({ color: 0x047efb }, [ + '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.renderer.scene.add(this.orbitSphere) } /** @@ -223,6 +247,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() } @@ -245,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 } /** @@ -263,13 +296,16 @@ export class SmoothOrbitControls extends SpeckleControls { this.setTarget(nativeOrigin.x, nativeOrigin.y, nativeOrigin.z) this.setRadius(sphere.radius) + this.usePivotal = false } /** * 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 + ) } /** @@ -285,7 +321,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.pivotPoint.equals(this.lastPivotPoint) ) } @@ -593,17 +630,123 @@ export class SmoothOrbitControls extends SpeckleControls { ) this.origin.set(x, y, z) - this.moveCamera() + return this.moveCamera() + } - return true + /** 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(position) + + /** Pivot needs to be transformed in a Y up CS */ + const pivotPoint = new Vector3() + .copy(this.pivotPoint) + .applyMatrix4(this._basisTransformInv) + + 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) + + /** 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 argument to be in a CS where Y is up */ + protected positionFromPivotal(origin: Vector3, quaternion: Quaternion) { + const pivotPoint = new Vector3() + .copy(this.pivotPoint) + .applyMatrix4(this._basisTransformInv) + + const position = new Vector3() + position.copy(origin) + + position.sub(pivotPoint) + position.applyQuaternion(quaternion) + position.add(pivotPoint) + + return position } - protected moveCamera() { - // Derive the new camera position from the updated spherical: + /** 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()) + pivotalOrigin.add(pivotPoint) + + return pivotalOrigin + } + + protected moveCamera(): boolean { + const lastCameraPos = new Vector3().copy(this._targetCamera.position) + const lastCameraQuat = new Quaternion().copy(this._targetCamera.quaternion) + this.spherical.makeSafe() - const position = this.positionFromSpherical(this.spherical, this.origin) + + /** 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)) + } + + /** We get a new position in the pivotal CS */ + position = this.positionFromPivotal(this.pivotalOrigin, quaternion) + /** 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)) + /** 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) @@ -617,14 +760,19 @@ export class SmoothOrbitControls extends SpeckleControls { ) ) } + /** 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, @@ -638,9 +786,22 @@ export class SmoothOrbitControls extends SpeckleControls { this._targetCamera.bottom = orthographicSize.y / -2 this._targetCamera.updateProjectionMatrix() } + + /** Update the debug origin sphere */ + this.orbitSphere.position.copy( + 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 + ) } - /* 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 @@ -654,9 +815,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 +824,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 } @@ -833,9 +991,39 @@ 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 + this.orbitSphere.visible = false } protected onPointerDown = (event: PointerEvent) => { + 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.renderer.intersections.intersect( + this.renderer.scene, + this._targetCamera as PerspectiveCamera, + new Vector2(x, y), + ObjectLayers.STREAM_CONTENT_MESH, + true, + this.renderer.clippingVolume + ) + 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 + } + } + if (this.pointers.length > 2) { return } @@ -849,11 +1037,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, @@ -935,6 +1118,7 @@ export class SmoothOrbitControls extends SpeckleControls { if (this.isUserPointing) { this.emit(PointerChangeEvent.PointerChangeEnd) } + this.orbitSphere.visible = false } protected onTouchChange(event: PointerEvent) { @@ -971,7 +1155,7 @@ 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.orbitSphere.visible = false } // this.element.style.cursor = 'grabbing' } @@ -997,6 +1181,8 @@ export class SmoothOrbitControls extends SpeckleControls { 60 this.userAdjustOrbit(0, 0, deltaZoom) event.preventDefault() + this.usePivotal = false + this.orbitSphere.visible = 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']) {