modeling/src/maths/plane/fromNoisyPoints.js

const vec3 = require('../vec3')
const fromNormalAndPoint = require('./fromNormalAndPoint')

/**
 * Create a best-fit plane from the given noisy vertices.
 *
 * NOTE: There are two possible orientations for every plane.
 *       This function always produces positive orientations.
 *
 * See http://www.ilikebigbits.com for the original discussion
 *
 * @param {Plane} out - receiving plane
 * @param {Array} vertices - list of vertices in any order or position
 * @returns {Plane} out
 * @alias module:modeling/maths/plane.fromNoisyPoints
 */
const fromNoisyPoints = (out, ...vertices) => {
  out[0] = 0.0
  out[1] = 0.0
  out[2] = 0.0
  out[3] = 0.0

  // calculate the centroid of the vertices
  // NOTE: out is the centriod
  const n = vertices.length
  vertices.forEach((v) => {
    vec3.add(out, out, v)
  })
  vec3.scale(out, out, 1.0 / n)

  // Calculate full 3x3 covariance matrix, excluding symmetries
  let xx = 0.0
  let xy = 0.0
  let xz = 0.0
  let yy = 0.0
  let yz = 0.0
  let zz = 0.0

  const vn = vec3.create()
  vertices.forEach((v) => {
    // NOTE: out is the centriod
    vec3.subtract(vn, v, out)
    xx += vn[0] * vn[0]
    xy += vn[0] * vn[1]
    xz += vn[0] * vn[2]
    yy += vn[1] * vn[1]
    yz += vn[1] * vn[2]
    zz += vn[2] * vn[2]
  })

  xx /= n
  xy /= n
  xz /= n
  yy /= n
  yz /= n
  zz /= n

  // Calculate the smallest Eigenvector of the covariance matrix
  // which becomes the plane normal

  vn[0] = 0.0
  vn[1] = 0.0
  vn[2] = 0.0

  // weighted directional vector
  const wdv = vec3.create()

  // X axis
  let det = yy * zz - yz * yz
  wdv[0] = det
  wdv[1] = xz * yz - xy * zz
  wdv[2] = xy * yz - xz * yy

  let weight = det * det
  vec3.add(vn, vn, vec3.scale(wdv, wdv, weight))

  // Y axis
  det = xx * zz - xz * xz
  wdv[0] = xz * yz - xy * zz
  wdv[1] = det
  wdv[2] = xy * xz - yz * xx

  weight = det * det
  if (vec3.dot(vn, wdv) < 0.0) {
    weight = -weight
  }
  vec3.add(vn, vn, vec3.scale(wdv, wdv, weight))

  // Z axis
  det = xx * yy - xy * xy
  wdv[0] = xy * yz - xz * yy
  wdv[1] = xy * xz - yz * xx
  wdv[2] = det

  weight = det * det
  if (vec3.dot(vn, wdv) < 0.0) {
    weight = -weight
  }
  vec3.add(vn, vn, vec3.scale(wdv, wdv, weight))

  // create the plane from normal and centriod
  // NOTE: out is the centriod
  return fromNormalAndPoint(out, vn, out)
}

module.exports = fromNoisyPoints