NGL@1.0.0-beta.7 Home Manual Reference Source Gallery

src/representation/surface-representation.js

/**
 * @file Surface Representation
 * @author Alexander Rose <alexander.rose@weirdbyte.de>
 * @private
 */

import { Vector3, Box3 } from '../../lib/three.es6.js'

import { defaults } from '../utils.js'
import Representation from './representation.js'
import Volume from '../surface/volume.js'
import SurfaceBuffer from '../buffer/surface-buffer.js'
import DoubleSidedBuffer from '../buffer/doublesided-buffer'
import ContourBuffer from '../buffer/contour-buffer.js'

/**
 * Surface representation parameter object. Extends {@link RepresentationParameters}
 *
 * @typedef {Object} SurfaceRepresentationParameters - surface representation parameters
 *
 * @property {String} isolevelType - Meaning of the isolevel value. Either *value* for the literal value or *sigma* as a factor of the sigma of the data. For volume data only.
 * @property {Float} isolevel - The value at which to create the isosurface. For volume data only.
 * @property {Integer} smooth - How many iterations of laplacian smoothing after surface triangulation. For volume data only.
 * @property {Boolean} background - Render the surface in the background, unlit.
 * @property {Boolean} opaqueBack - Render the back-faces (where normals point away from the camera) of the surface opaque, ignoring the transparency parameter.
 * @property {Integer} boxSize - Size of the box to triangulate volume data in. Set to zero to triangulate the whole volume. For volume data only.
 * @property {Boolean} useWorker - Weather or not to triangulate the volume asynchronously in a Web Worker. For volume data only.
 * @property {Boolean} wrap - Wrap volume data around the edges; use in conjuction with boxSize but not larger than the volume dimension. For volume data only.
 */

/**
 * Surface representation
 */
class SurfaceRepresentation extends Representation {
  /**
   * Create Surface representation object
   * @param {Surface|Volume} surface - the surface or volume to be represented
   * @param {Viewer} viewer - a viewer object
   * @param {SurfaceRepresentationParameters} params - surface representation parameters
   */
  constructor (surface, viewer, params) {
    super(surface, viewer, params)

    this.type = 'surface'

    this.parameters = Object.assign({

      isolevelType: {
        type: 'select',
        options: {
          'value': 'value', 'sigma': 'sigma'
        }
      },
      isolevel: {
        type: 'number', precision: 2, max: 1000, min: -1000
      },
      negateIsolevel: {
        type: 'boolean'
      },
      smooth: {
        type: 'integer', precision: 1, max: 10, min: 0
      },
      background: {
        type: 'boolean', rebuild: true  // FIXME
      },
      opaqueBack: {
        type: 'boolean', buffer: true
      },
      boxSize: {
        type: 'integer', precision: 1, max: 100, min: 0
      },
      colorVolume: {
        type: 'hidden'
      },
      contour: {
        type: 'boolean', rebuild: true
      },
      useWorker: {
        type: 'boolean', rebuild: true
      },
      wrap: {
        type: 'boolean', rebuild: true
      }

    }, this.parameters)

    if (surface instanceof Volume) {
      this.surface = undefined
      this.volume = surface
    } else {
      this.surface = surface
      this.volume = undefined
    }

    this.boxCenter = new Vector3()
    this.__boxCenter = new Vector3()
    this.box = new Box3()
    this.__box = new Box3()

    this._position = new Vector3()
    this.setBox = function setBox () {
      this._position.copy(viewer.translationGroup.position).negate()
      if (!this._position.equals(this.boxCenter)) {
        this.setParameters({ 'boxCenter': this._position })
      }
    }

    this.viewer.signals.ticked.add(this.setBox, this)

    this.init(params)
  }

  init (params) {
    const p = params || {}
    p.colorScheme = defaults(p.colorScheme, 'uniform')
    p.colorValue = defaults(p.colorValue, 0xDDDDDD)

    this.isolevelType = defaults(p.isolevelType, 'sigma')
    this.isolevel = defaults(p.isolevel, 2.0)
    this.negateIsolevel = defaults(p.negateIsolevel, false)
    this.smooth = defaults(p.smooth, 0)
    this.background = defaults(p.background, false)
    this.opaqueBack = defaults(p.opaqueBack, true)
    this.boxSize = defaults(p.boxSize, 0)
    this.colorVolume = defaults(p.colorVolume, undefined)
    this.contour = defaults(p.contour, false)
    this.useWorker = defaults(p.useWorker, true)
    this.wrap = defaults(p.wrap, false)

    super.init(p)

    this.build()
  }

  attach (callback) {
    this.bufferList.forEach(buffer => {
      this.viewer.add(buffer)
    })

    this.setVisibility(this.visible)

    callback()
  }

  prepare (callback) {
    if (this.volume) {
      let isolevel

      if (this.isolevelType === 'sigma') {
        isolevel = this.volume.getValueForSigma(this.isolevel)
      } else {
        isolevel = this.isolevel
      }
      if (this.negateIsolevel) isolevel *= -1

      if (!this.surface ||
        this.__isolevel !== isolevel ||
        this.__smooth !== this.smooth ||
        this.__contour !== this.contour ||
        this.__wrap !== this.wrap ||
        this.__boxSize !== this.boxSize ||
        (this.boxSize > 0 &&
            !this.__boxCenter.equals(this.boxCenter))
      ) {
        this.__isolevel = isolevel
        this.__smooth = this.smooth
        this.__contour = this.contour
        this.__wrap = this.wrap
        this.__boxSize = this.boxSize
        this.__boxCenter.copy(this.boxCenter)
        this.__box.copy(this.box)

        const onSurfaceFinish = surface => {
          this.surface = surface
          callback()
        }

        if (this.useWorker) {
          this.volume.getSurfaceWorker(
            isolevel, this.smooth, this.boxCenter, this.boxSize,
            this.contour, this.wrap, onSurfaceFinish
          )
        } else {
          onSurfaceFinish(
            this.volume.getSurface(
              isolevel, this.smooth, this.boxCenter, this.boxSize,
              this.contour, this.wrap
            )
          )
        }
      } else {
        callback()
      }
    } else {
      callback()
    }
  }

  create () {
    const sd = {
      position: this.surface.getPosition(),
      color: this.surface.getColor(this.getColorParams()),
      index: this.surface.getIndex()
    }

    let buffer

    if (this.contour) {
      buffer = new ContourBuffer(
        sd,
        this.getBufferParams({ wireframe: false })
      )
    } else {
      sd.normal = this.surface.getNormal()
      sd.picking = this.surface.getPicking()

      const surfaceBuffer = new SurfaceBuffer(
        sd,
        this.getBufferParams({
          background: this.background,
          opaqueBack: this.opaqueBack,
          dullInterior: false
        })
      )

      buffer = new DoubleSidedBuffer(surfaceBuffer)
    }

    this.bufferList.push(buffer)
  }

  update (what) {
    if (this.bufferList.length === 0) return

    what = what || {}

    const surfaceData = {}

    if (what.position) {
      surfaceData.position = this.surface.getPosition()
    }

    if (what.color) {
      surfaceData.color = this.surface.getColor(
        this.getColorParams()
      )
    }

    if (what.index) {
      surfaceData.index = this.surface.getIndex()
    }

    if (what.normal) {
      surfaceData.normal = this.surface.getNormal()
    }

    this.bufferList.forEach(function (buffer) {
      buffer.setAttributes(surfaceData)
    })
  }

  /**
   * Set representation parameters
   * @alias SurfaceRepresentation#setParameters
   * @param {SurfaceRepresentationParameters} params - surface parameter object
   * @param {Object} [what] - buffer data attributes to be updated,
   *                        note that this needs to be implemented in the
   *                        derived classes. Generally it allows more
   *                        fine-grained control over updating than
   *                        forcing a rebuild.
   * @param {Boolean} what.position - update position data
   * @param {Boolean} what.color - update color data
   * @param {Boolean} [rebuild] - whether or not to rebuild the representation
   * @return {SurfaceRepresentation} this object
   */
  setParameters (params, what, rebuild) {
    if (params && params.isolevelType !== undefined &&
      this.volume
    ) {
      if (this.isolevelType === 'value' &&
        params.isolevelType === 'sigma'
      ) {
        this.isolevel = this.volume.getSigmaForValue(this.isolevel)
      } else if (this.isolevelType === 'sigma' &&
        params.isolevelType === 'value'
      ) {
        this.isolevel = this.volume.getValueForSigma(this.isolevel)
      }

      this.isolevelType = params.isolevelType
    }

    if (params && params.boxCenter) {
      this.boxCenter.copy(params.boxCenter)
      delete params.boxCenter
    }

    // Forbid wireframe && contour as in molsurface
    if (params && params.wireframe && (
      params.contour || (params.contour === undefined && this.contour)
    )) {
      params.wireframe = false
    }

    super.setParameters(params, what, rebuild)

    if (this.volume) {
      this.volume.getBox(this.boxCenter, this.boxSize, this.box)
    }

    if (params && params.colorVolume !== undefined) {
      what.color = true
    }

    if (this.surface && (
      params.isolevel !== undefined ||
      params.negateIsolevel !== undefined ||
      params.smooth !== undefined ||
      params.wrap !== undefined ||
      params.boxSize !== undefined ||
      (this.boxSize > 0 &&
        !this.__box.equals(this.box))
    )) {
      this.build({
        'position': true,
        'color': true,
        'index': true,
        'normal': !this.contour
      })
    }

    return this
  }

  getColorParams () {
    const p = super.getColorParams()

    p.volume = this.colorVolume

    return p
  }

  dispose () {
    this.viewer.signals.ticked.remove(this.setBox, this)

    super.dispose()
  }
}

export default SurfaceRepresentation