packages/miew/src/gfx/vr/WebVRPoC.js

Summary

Maintainability
A
1 hr
Test Coverage
import createWebVRButton from './createWebVRButton'
import gfxutils from '../gfxutils'
import logger from '../../utils/logger'
import settings from '../../settings'
import UberMaterial from '../shaders/UberMaterial'
import {
  CylinderGeometry,
  Mesh,
  Object3D,
  PerspectiveCamera,
  Color
} from 'three'

export default class WebVRPoC {
  constructor(onToggle) {
    this._mainCamera = new PerspectiveCamera()
    this._button = null
    this._onToggle = onToggle

    this._molContainer = new gfxutils.RCGroup()
    this._user = new gfxutils.RCGroup()
    this._scalingPivot = new Object3D()
    this._user.add(this._scalingPivot)

    this._controller1 = null
    this._controller2 = null
    this._pressedGripsCounter = 0
    this._distance = 0

    this._gfx = null
  }

  startScalingByControllers() {
    // reset scale
    this._distance = this._controller1.position.distanceTo(
      this._controller2.position
    )
    gfxutils.getMiddlePoint(
      this._controller1.position,
      this._controller2.position,
      this._scalingPivot.position
    )
    this._scalingPivot.scale.set(1, 1, 1)
    this._scalingPivot.updateMatrix()
    this._scalingPivot.updateMatrixWorld()
    // link molecule to pivot
    this._scalingPivot.addSavingWorldTransform(this._molContainer)
  }

  stopScalingByControllers() {
    this._gfx.scene.addSavingWorldTransform(this._molContainer)
  }

  handleGripsDown(event) {
    this._pressedGripsCounter++
    if (this._pressedGripsCounter === 2) {
      this.startScalingByControllers()
    } else if (this._pressedGripsCounter === 1) {
      event.target.addSavingWorldTransform(this._molContainer)
    }
  }

  handleGripsUp(event) {
    this._pressedGripsCounter--
    if (this._pressedGripsCounter === 1) {
      this.stopScalingByControllers()
      // reattach molecule to other controller
      const anotherController =
        event.target === this._controller1
          ? this._controller2
          : this._controller1
      anotherController.addSavingWorldTransform(this._molContainer)
    } else if (this._pressedGripsCounter === 0) {
      this._gfx.scene.addSavingWorldTransform(this._molContainer)
    }
  }

  enable(gfx) {
    if (!gfx) {
      logger.warn("WebVR couldn't be enabled, because gfx is not defined")
      return
    }
    this._gfx = gfx
    const { renderer, camera } = gfx
    if (!renderer) {
      throw new Error('No renderer is available to toggle WebVR')
    }
    if (!camera) {
      throw new Error('No camera is available to toggle WebVR')
    }

    // enable xr in renderer
    renderer.xr.enabled = true
    // add button for turning vr mode
    if (!this._button) {
      this._button = createWebVRButton(this)
      document.body.appendChild(this._button)
    } else {
      this._button.style.display = 'block'
    }
    // store fog setting
    this._mainFog = settings.now.fog
    settings.set('fog', false)

    this._plugVRNodesIntoScene(gfx, renderer)
    this._setControllersListeners()

    // make some Miew job
    if (this._onToggle) {
      this._onToggle(true)
    }
  }

  _plugVRNodesIntoScene(gfx, renderer) {
    // store common scene camera
    this._mainCamera.copy(gfx.camera)
    // add hierarchical structure for webVR into scene
    gfx.scene.add(this._user)
    // turn on webvr transformation
    gfx.scene.add(this._molContainer)
    this._molContainer.add(gfx.root)

    this._controller1 = renderer.xr.getController(0)
    this._controller2 = renderer.xr.getController(1)
    const mesh = this._createControllerMesh()
    this._controller1.add(mesh)
    this._controller2.add(mesh.clone())
    this._user.add(this._controller1)
    this._user.add(this._controller2)
  }

  _setControllersListeners() {
    this._controller1.addEventListener('selectstart', (event) => {
      this.handleGripsDown(event)
    })
    this._controller1.addEventListener('selectend', (event) => {
      this.handleGripsUp(event)
    })
    this._controller2.addEventListener('selectstart', (event) => {
      this.handleGripsDown(event)
    })
    this._controller2.addEventListener('selectend', (event) => {
      this.handleGripsUp(event)
    })

    this._controller1.addEventListener('squeezestart', (event) => {
      this.handleGripsDown(event)
    })
    this._controller1.addEventListener('squeezeend', (event) => {
      this.handleGripsUp(event)
    })
    this._controller2.addEventListener('squeezestart', (event) => {
      this.handleGripsDown(event)
    })
    this._controller2.addEventListener('squeezeend', (event) => {
      this.handleGripsUp(event)
    })
  }

  disable() {
    if (!this._gfx) {
      return
    }
    const { renderer, camera } = this._gfx
    if (!renderer) {
      throw new Error('No renderer is available to toggle WebVR')
    }

    // nullify webxr callback for animation frame
    renderer.setAnimationLoop(null)
    const session = renderer.xr.getSession()
    if (session) {
      session.end()
    }
    renderer.xr.enabled = false
    // remove button of VR entering
    if (this._button) {
      this._button.style.display = 'none'
    }
    // restore fog param
    settings.set('fog', this._mainFog)

    this._unplugVRNodesFromScene(camera)

    // make some Miew job
    if (this._onToggle) {
      this._onToggle(false)
    }
  }

  _unplugVRNodesFromScene(camera) {
    // restore common camera
    if (this._mainCamera && camera) {
      camera.copy(this._mainCamera)
    }
    // turn off webvr transformation
    const root = this._molContainer.children[0]
    if (root) {
      this._gfx.scene.add(root)
    }
    this._molContainer.parent.remove(this._molContainer)
    if (this._user) {
      this._gfx.scene.remove(this._user)
    }
    // free scene nodes
    this._molContainer = null
    this._user = null
    this._scalingPivot = null
    this._user = null
    this._controller1 = null
    this._controller2 = null
  }

  _createControllerMesh() {
    // visualize controllers with cylinders
    const geometry = new CylinderGeometry(0.04, 0.04, 0.3)
    const material = new UberMaterial()
    material.setValues({ lights: false, overrideColor: true })
    material.setUberOptions({ fixedColor: new Color(0x4444ff) })
    material.updateUniforms()
    const cylinder = new Mesh(geometry, material)
    cylinder.rotateX(-Math.PI / 2)
    return cylinder
  }

  updateMoleculeScale() {
    if (!this._controller1 || !this._controller2) {
      return
    }

    const self = this
    // update molecule scaling by controllers
    if (self._pressedGripsCounter === 2) {
      // recalc scaling pivot
      gfxutils.getMiddlePoint(
        self._controller1.position,
        self._controller2.position,
        self._scalingPivot.position
      )
      // recalc scaler
      const dist = self._controller1.position.distanceTo(
        self._controller2.position
      )
      const scaler = dist / self._distance
      self._scalingPivot.scale.multiplyScalar(scaler)
      // save cur distance for next frame
      self._distance = dist
    }
  }

  /**
   * Reposition molecule right before the camera.
   * @note The proper way is to initiate headset in the place of common Miew's camera.
   * But threejs limitations on setting new XRReferenceSpace enforce the molecule repositioning
   * Hope, something will change.
   */
  moveSceneBehindHeadset() {
    const gfx = this._gfx
    const { camera } = gfx

    // set container position in camera space
    const container = this._molContainer
    container.matrix.identity()
    container.position.set(0, 0, -4.0)
    container.updateMatrix()

    // update container world matrix
    container.matrixWorld.multiplyMatrices(camera.matrixWorld, container.matrix)
    // readd to scene
    gfx.scene.addSavingWorldTransform(container)
    if (this._onToggle) {
      this._onToggle(true)
    }
  }

  getCanvas() {
    const gfx = this._gfx
    return gfx && gfx.renderer ? gfx.renderer.domElement : null
  }
}