diff --git a/viewer/src/components/context/context.ts b/viewer/src/components/context/context.ts index fa2ee35d..e8ed57af 100644 --- a/viewer/src/components/context/context.ts +++ b/viewer/src/components/context/context.ts @@ -1,4 +1,5 @@ -import { Clock, Mesh, Object3D, Plane, Vector2, Vector3 } from 'three'; +import { Clock, Matrix4, Mesh, Object3D, Plane, Vector2, Vector3 } from 'three'; +import { VRButton } from 'three/examples/jsm/webxr/VRButton'; import { IfcCamera } from './camera/camera'; import { IfcRaycaster } from './raycaster'; import { IfcRenderer } from './renderer/renderer'; @@ -178,6 +179,10 @@ export class IfcContext { return this.ifcCaster.castRayIfc(); } + castVrRay(from: Matrix4, to: Matrix4) { + return this.ifcCaster.castVrRay(from, to); + } + fitToFrame() { this.ifcCamera.navMode[NavigationModes.Orbit].fitModelToFrame(); } @@ -196,6 +201,8 @@ export class IfcContext { if (this.stats) this.stats.begin(); const isWebXR = this.options.webXR || false; if (isWebXR) { + document.body.appendChild(VRButton.createButton(this.getRenderer())); + this.getRenderer().xr.enabled = true; this.renderForWebXR(); } else { requestAnimationFrame(this.render); @@ -203,14 +210,17 @@ export class IfcContext { this.updateAllComponents(); if (this.stats) this.stats.end(); }; - + private renderForWebXR = () => { const newAnimationLoop = () => { + this.webXrMoveTracking(); this.getRenderer().render(this.getScene(), this.getCamera()); }; this.getRenderer().setAnimationLoop(newAnimationLoop); }; + webXrMoveTracking = () => {}; // empty function called on webXR render loop; which is replaced in VRControllers to handle VR movement + private updateAllComponents() { const delta = this.clock.getDelta(); this.items.components.forEach((component) => component.update(delta)); diff --git a/viewer/src/components/context/raycaster.ts b/viewer/src/components/context/raycaster.ts index baa50981..038546ae 100644 --- a/viewer/src/components/context/raycaster.ts +++ b/viewer/src/components/context/raycaster.ts @@ -1,4 +1,4 @@ -import { Intersection, Object3D, Raycaster } from 'three'; +import { Intersection, Matrix4, Object3D, Raycaster } from 'three'; import { IfcComponent } from '../../base-types'; import { IfcContext } from './context'; @@ -29,6 +29,12 @@ export class IfcRaycaster extends IfcComponent { return filtered.length > 0 ? filtered[0] : null; } + castVrRay(from: Matrix4, to: Matrix4) { + this.raycaster.ray.origin.setFromMatrixPosition(from); + this.raycaster.ray.direction.set(0, 0, -1).applyMatrix4(to); + return this.raycaster.intersectObjects(this.context.items.pickableIfcModels)[0]; + } + private filterClippingPlanes(objs: Intersection[]) { const planes = this.context.getClippingPlanes(); if (objs.length <= 0 || !planes || planes?.length <= 0) return objs; diff --git a/viewer/src/components/context/vrControllers.ts b/viewer/src/components/context/vrControllers.ts new file mode 100644 index 00000000..09591e9a --- /dev/null +++ b/viewer/src/components/context/vrControllers.ts @@ -0,0 +1,90 @@ +import { Vector3, Line, BufferGeometry, Object3D, Group, Matrix4, Quaternion } from 'three'; +import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory'; +import { IfcContext } from './context'; +import { IfcManager } from '../ifc'; + +export class IfcVrControllers { + context: IfcContext; + ifcManager: IfcManager; + controller1: Group; + controller2: Group; + controllerGrip1: Group; + controllerGrip2: Group; + cameraDolly = new Object3D(); + dummyCam = new Object3D(); + tempMatrix = new Matrix4(); + letUserMove: Boolean = false; + + constructor(context: IfcContext, ifcManager: IfcManager) { + this.context = context; + this.context.webXrMoveTracking = this.handleUserMovement; + this.ifcManager = ifcManager; + this.controller1 = this.context.renderer.renderer.xr.getController(0); + this.controller1.addEventListener('squeezestart', this.allowMovement.bind(this)); + this.controller1.addEventListener('squeezeend', this.stopMovement.bind(this)); + this.controller2 = this.context.renderer.renderer.xr.getController(1); + this.controller2.addEventListener('selectstart', this.highlight.bind(this)); + this.controller2.addEventListener('squeezestart', this.clearHighlight.bind(this)); + const controllerModelFactory = new XRControllerModelFactory(); + this.controllerGrip1 = this.context.renderer.renderer.xr.getControllerGrip(0); + this.controllerGrip1.add(controllerModelFactory.createControllerModel(this.controllerGrip1)); + this.controllerGrip2 = this.context.renderer.renderer.xr.getControllerGrip(1); + this.controllerGrip2.add(controllerModelFactory.createControllerModel(this.controllerGrip2)); + this.context.getScene().add(this.controller1); + this.context.getScene().add(this.controller2); + this.context.getScene().add(this.controllerGrip1); + this.context.getScene().add(this.controllerGrip2); + const geometry = new BufferGeometry().setFromPoints([ + new Vector3(0, 0, 0), + new Vector3(0, 0, -1) + ]); + const line = new Line(geometry); + line.name = 'line'; + line.scale.z = 5; + this.controller1.add(line.clone()); + this.controller2.add(line.clone()); + this.context.getCamera().position.set(0, 0, 0); + this.cameraDolly.add(this.context.getCamera()); + this.context.getCamera().add(this.dummyCam); + // Needed to add controllers to dolly?? Not sure without device to test on + // this.cameraDolly.add(this.controller1); + // this.cameraDolly.add(this.controller2); + // this.cameraDolly.add(this.controllerGrip1); + // this.cameraDolly.add(this.controllerGrip2); + } + + highlight(event: any) { + const controller = event.target as Group; + const found = this.context.castVrRay(controller.matrixWorld, this.tempMatrix); + if (found) { + this.ifcManager.selector.selection.pick(found); + } else { + this.ifcManager.selector.selection.unpick(); + } + } + + clearHighlight() { + this.ifcManager.selector.selection.unpick(); + } + + allowMovement() { + this.letUserMove = true; + } + + stopMovement() { + this.letUserMove = false; + } + + handleUserMovement = () => { + if (this.letUserMove) { + const speed = 2; + const moveZ = -0.05 * speed; + const saveQuat = this.cameraDolly.quaternion.clone(); + const holder = new Quaternion(); + this.dummyCam.getWorldQuaternion(holder); + this.cameraDolly.quaternion.copy(holder); + this.cameraDolly.translateZ(moveZ); + this.cameraDolly.quaternion.copy(saveQuat); + } + }; +} diff --git a/viewer/src/ifc-viewer-api.ts b/viewer/src/ifc-viewer-api.ts index 78f97447..904de64a 100644 --- a/viewer/src/ifc-viewer-api.ts +++ b/viewer/src/ifc-viewer-api.ts @@ -19,6 +19,7 @@ import { PDFWriter } from './components/import-export/pdf'; import { EdgeProjector } from './components/import-export/edges-vectorizer/edge-projection'; import { ClippingEdges } from './components/display/clipping-planes/clipping-edges'; import { SelectionWindow } from './components/selection/selection-window'; +import { IfcVrControllers } from './components/context/vrControllers'; export class IfcViewerAPI { context: IfcContext; @@ -37,6 +38,7 @@ export class IfcViewerAPI { axes: IfcAxes; dropbox: DropboxAPI; selectionWindow: SelectionWindow; + vrControllers: IfcVrControllers; constructor(options: ViewerOptions) { if (!options.container) throw new Error('Could not get container element!'); @@ -56,6 +58,7 @@ export class IfcViewerAPI { this.GLTF = new GLTFManager(this.context, this.IFC); this.dropbox = new DropboxAPI(this.context, this.IFC); this.selectionWindow = new SelectionWindow(this.context); + this.vrControllers = new IfcVrControllers(this.context, this.IFC); ClippingEdges.ifc = this.IFC; ClippingEdges.context = this.context; }