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

src/representation/representation.js

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

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

import { Debug, Log, ColormakerRegistry, ExtensionFragDepth } from '../globals.js'
import { defaults } from '../utils.js'
import Queue from '../utils/queue.js'
import Counter from '../utils/counter.js'

/**
 * Representation parameter object.
 * @typedef {Object} RepresentationParameters - representation parameters
 * @property {Boolean} [lazy] - only build & update the representation when visible
 *                            otherwise defer changes until set visible again
 * @property {Integer} [clipNear] - position of camera near/front clipping plane
 *                                in percent of scene bounding box
 * @property {Integer} [clipRadius] - radius of clipping sphere
 * @property {Vector3} [clipCenter] - position of for spherical clipping
 * @property {Boolean} [flatShaded] - render flat shaded
 * @property {Float} [opacity] - translucency: 1 is fully opaque, 0 is fully transparent
 * @property {Boolean} [depthWrite] - depth write
 * @property {String} [side] - which triangle sides to render, "front" front-side,
 *                            "back" back-side, "double" front- and back-side
 * @property {Boolean} [wireframe] - render as wireframe
 * @property {String} [colorScheme] - color scheme
 * @property {String} [colorScale] - color scale, either a string for a
 *                                 predefined scale or an array of
 *                                 colors to be used as the scale
 * @property {Boolean} [colorReverse] - reverse color scale
 * @property {Color} [colorValue] - color value
 * @property {Integer[]} [colorDomain] - scale value range
 * @property {Integer} colorDomain.0 - min value
 * @property {Integer} colorDomain.1 - max value
 * @property {String} [colorMode] - color mode, one of rgb, hsv, hsl, hsi, lab, hcl
 * @property {Float} [roughness] - how rough the material is, between 0 and 1
 * @property {Float} [metalness] - how metallic the material is, between 0 and 1
 * @property {Color} [diffuse] - diffuse color for lighting
 * @property {Boolean} [disablePicking] - disable picking
 */

/**
 * Representation object
 * @interface
 * @param {Object} object - the object to be represented
 * @param {Viewer} viewer - a viewer object
 * @param {RepresentationParameters} [params] - representation parameters
 */
class Representation {
  constructor (object, viewer, params) {
    // eslint-disable-next-line no-unused-vars
    const p = params || {}

    this.type = ''

    this.parameters = {

      lazy: {
        type: 'boolean'
      },

      clipNear: {
        type: 'range', step: 1, max: 100, min: 0, buffer: true
      },
      clipRadius: {
        type: 'number', precision: 1, max: 1000, min: 0, buffer: true
      },
      clipCenter: {
        type: 'vector3', precision: 1, buffer: true
      },
      flatShaded: {
        type: 'boolean', buffer: true
      },
      opacity: {
        type: 'range', step: 0.01, max: 1, min: 0, buffer: true
      },
      depthWrite: {
        type: 'boolean', buffer: true
      },
      side: {
        type: 'select',
        buffer: true,
        options: { front: 'front', back: 'back', double: 'double' }
      },
      wireframe: {
        type: 'boolean', buffer: true
      },

      colorScheme: {
        type: 'select',
        update: 'color',
        options: {}
      },
      colorScale: {
        type: 'select',
        update: 'color',
        options: ColormakerRegistry.getScales()
      },
      colorReverse: {
        type: 'boolean', update: 'color'
      },
      colorValue: {
        type: 'color', update: 'color'
      },
      colorDomain: {
        type: 'hidden', update: 'color'
      },
      colorMode: {
        type: 'select',
        update: 'color',
        options: ColormakerRegistry.getModes()
      },

      roughness: {
        type: 'range', step: 0.01, max: 1, min: 0, buffer: true
      },
      metalness: {
        type: 'range', step: 0.01, max: 1, min: 0, buffer: true
      },
      diffuse: {
        type: 'color', buffer: true
      },

      matrix: {
        type: 'hidden', buffer: true
      },

      disablePicking: {
        type: 'boolean', rebuild: true
      }

    }

    /**
     * @type {Viewer}
     */
    this.viewer = viewer

    /**
     * Counter that keeps track of tasks related to the creation of
     * the representation, including surface calculations.
     * @type {Counter}
     */
    this.tasks = new Counter()

    /**
     * @type {Queue}
     * @private
     */
    this.queue = new Queue(this.make.bind(this))

    /**
     * @type {Array}
     * @private
     */
    this.bufferList = []

    if (this.parameters.colorScheme) {
      this.parameters.colorScheme.options = ColormakerRegistry.getSchemes()
    }
  }

  init (params) {
    const p = params || {}

    this.clipNear = defaults(p.clipNear, 0)
    this.clipRadius = defaults(p.clipRadius, 0)
    this.clipCenter = defaults(p.clipCenter, new Vector3())
    this.flatShaded = defaults(p.flatShaded, false)
    this.side = defaults(p.side, 'double')
    this.opacity = defaults(p.opacity, 1.0)
    this.depthWrite = defaults(p.depthWrite, true)
    this.wireframe = defaults(p.wireframe, false)

    this.setColor(p.color, p)

    this.colorScheme = defaults(p.colorScheme, 'uniform')
    this.colorScale = defaults(p.colorScale, '')
    this.colorReverse = defaults(p.colorReverse, false)
    this.colorValue = defaults(p.colorValue, 0x909090)
    this.colorDomain = defaults(p.colorDomain, undefined)
    this.colorMode = defaults(p.colorMode, 'hcl')

    this.visible = defaults(p.visible, true)
    this.quality = defaults(p.quality, undefined)

    this.roughness = defaults(p.roughness, 0.4)
    this.metalness = defaults(p.metalness, 0.0)
    this.diffuse = defaults(p.diffuse, 0xffffff)

    this.lazy = defaults(p.lazy, false)
    this.lazyProps = {
      build: false,
      bufferParams: {},
      what: {}
    }

    this.matrix = defaults(p.matrix, new Matrix4())

    this.disablePicking = defaults(p.disablePicking, false)

    // handle common parameters when applicable

    const tp = this.parameters

    if (tp.sphereDetail === true) {
      tp.sphereDetail = {
        type: 'integer', max: 3, min: 0, rebuild: 'impostor'
      }
    }
    if (tp.radialSegments === true) {
      tp.radialSegments = {
        type: 'integer', max: 25, min: 5, rebuild: 'impostor'
      }
    }
    if (tp.openEnded === true) {
      tp.openEnded = {
        type: 'boolean', rebuild: 'impostor', buffer: true
      }
    }
    if (tp.disableImpostor === true) {
      tp.disableImpostor = {
        type: 'boolean', rebuild: true
      }
    }

    if (p.quality === 'low') {
      if (tp.sphereDetail) this.sphereDetail = 0
      if (tp.radialSegments) this.radialSegments = 5
    } else if (p.quality === 'medium') {
      if (tp.sphereDetail) this.sphereDetail = 1
      if (tp.radialSegments) this.radialSegments = 10
    } else if (p.quality === 'high') {
      if (tp.sphereDetail) this.sphereDetail = 2
      if (tp.radialSegments) this.radialSegments = 20
    } else {
      if (tp.sphereDetail) {
        this.sphereDetail = defaults(p.sphereDetail, 1)
      }
      if (tp.radialSegments) {
        this.radialSegments = defaults(p.radialSegments, 10)
      }
    }

    if (tp.openEnded) {
      this.openEnded = defaults(p.openEnded, true)
    }

    if (tp.disableImpostor) {
      this.disableImpostor = defaults(p.disableImpostor, false)
    }
  }

  getColorParams (p) {
    return Object.assign({

      scheme: this.colorScheme,
      scale: this.colorScale,
      reverse: this.colorReverse,
      value: this.colorValue,
      domain: this.colorDomain,
      mode: this.colorMode

    }, p)
  }

  getBufferParams (p) {
    return Object.assign({

      clipNear: this.clipNear,
      clipRadius: this.clipRadius,
      clipCenter: this.clipCenter,
      flatShaded: this.flatShaded,
      opacity: this.opacity,
      depthWrite: this.depthWrite,
      side: this.side,
      wireframe: this.wireframe,

      roughness: this.roughness,
      metalness: this.metalness,
      diffuse: this.diffuse,

      matrix: this.matrix,

      disablePicking: this.disablePicking

    }, p)
  }

  setColor (value, p) {
    const types = Object.keys(ColormakerRegistry.getSchemes())

    if (typeof value === 'string' && types.includes(value.toLowerCase())) {
      if (p) {
        p.colorScheme = value
      } else {
        this.setParameters({ colorScheme: value })
      }
    } else if (value !== undefined) {
      value = new Color(value).getHex()
      if (p) {
        p.colorScheme = 'uniform'
        p.colorValue = value
      } else {
        this.setParameters({
          colorScheme: 'uniform', colorValue: value
        })
      }
    }

    return this
  }

  // TODO
  // get prepare(){ return false; }

  create () {

    // this.bufferList.length = 0;

  }

  update () {
    this.build()
  }

  build (updateWhat) {
    if (this.lazy && !this.visible) {
      this.lazyProps.build = true
      return
    }

    if (!this.prepare) {
      this.tasks.increment()
      this.make()
      return
    }

    // don't let tasks accumulate
    if (this.queue.length() > 0) {
      this.tasks.change(1 - this.queue.length())
      this.queue.kill()
    } else {
      this.tasks.increment()
    }

    this.queue.push(updateWhat || false)
  }

  make (updateWhat, callback) {
    if (Debug) Log.time('Representation.make ' + this.type)

    const _make = function () {
      if (updateWhat) {
        this.update(updateWhat)
        this.viewer.requestRender()
        this.tasks.decrement()
        if (callback) callback()
      } else {
        this.clear()
        this.create()
        if (!this.manualAttach && !this.disposed) {
          if (Debug) Log.time('Representation.attach ' + this.type)
          this.attach(function () {
            if (Debug) Log.timeEnd('Representation.attach ' + this.type)
            this.tasks.decrement()
            if (callback) callback()
          }.bind(this))
        }
      }

      if (Debug) Log.timeEnd('Representation.make ' + this.type)
    }.bind(this)

    if (this.prepare) {
      this.prepare(_make)
    } else {
      _make()
    }
  }

  attach (callback) {
    this.setVisibility(this.visible)

    callback()
  }

  /**
   * Set the visibility of the representation
   * @param {Boolean} value - visibility flag
   * @param {Boolean} [noRenderRequest] - whether or not to request a re-render from the viewer
   * @return {Representation} this object
   */
  setVisibility (value, noRenderRequest) {
    this.visible = value

    if (this.visible) {
      const lazyProps = this.lazyProps
      const bufferParams = lazyProps.bufferParams
      const what = lazyProps.what

      if (lazyProps.build) {
        lazyProps.build = false
        this.build()
        return
      } else if (Object.keys(bufferParams).length || Object.keys(what).length) {
        lazyProps.bufferParams = {}
        lazyProps.what = {}
        this.updateParameters(bufferParams, what)
      }
    }

    this.bufferList.forEach(function (buffer) {
      buffer.setVisibility(value)
    })

    if (!noRenderRequest) this.viewer.requestRender()

    return this
  }

  /**
   * Set the visibility of the representation
   * @param {RepresentationParameters} params - parameters 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 {Representation} this object
   */
  setParameters (params, what, rebuild) {
    const p = params || {}
    const tp = this.parameters

    this.setColor(p.color, p)

    what = what || {}
    rebuild = rebuild || false

    const bufferParams = {}

    for (let name in p) {
      if (p[ name ] === undefined) continue
      if (tp[ name ] === undefined) continue

      if (tp[ name ].int) p[ name ] = parseInt(p[ name ])
      if (tp[ name ].float) p[ name ] = parseFloat(p[ name ])

      // no value change
      if (p[ name ] === this[ name ] && (
          !p[ name ].equals || p[ name ].equals(this[ name ])
        )
      ) continue

      if (this[ name ] && this[ name ].set) {
        this[ name ].set(p[ name ])
      } else {
        this[ name ] = p[ name ]
      }

      // buffer param
      if (tp[ name ].buffer) {
        if (tp[ name ].buffer === true) {
          bufferParams[ name ] = p[ name ]
        } else {
          bufferParams[ tp[ name ].buffer ] = p[ name ]
        }
      }

      // mark for update
      if (tp[ name ].update) {
        what[ tp[ name ].update ] = true
      }

      // mark for rebuild
      if (tp[ name ].rebuild &&
        !(tp[ name ].rebuild === 'impostor' &&
          ExtensionFragDepth && !this.disableImpostor)
      ) {
        rebuild = true
      }
    }

    //

    if (rebuild) {
      this.build()
    } else {
      this.updateParameters(bufferParams, what)
    }

    return this
  }

  updateParameters (bufferParams, what) {
    if (this.lazy && !this.visible) {
      Object.assign(this.lazyProps.bufferParams, bufferParams)
      Object.assign(this.lazyProps.what, what)
      return
    }

    this.bufferList.forEach(function (buffer) {
      buffer.setParameters(bufferParams)
    })

    if (Object.keys(what).length) {
      this.update(what)  // update buffer attribute
    }

    this.viewer.requestRender()
  }

  getParameters () {
    const params = {
      lazy: this.lazy,
      visible: this.visible,
      quality: this.quality
    }

    Object.keys(this.parameters).forEach(name => {
      if (this.parameters[ name ] !== null) {
        params[ name ] = this[ name ]
      }
    })

    return params
  }

  clear () {
    this.bufferList.forEach(buffer => {
      this.viewer.remove(buffer)
      buffer.dispose()
    })
    this.bufferList.length = 0

    this.viewer.requestRender()
  }

  dispose () {
    this.disposed = true
    this.queue.kill()
    this.tasks.dispose()
    this.clear()
  }
}

export default Representation