lib/scenic/math/matrix.ex

#
#  Created by Boyd Multerer
#  Copyright © 2017 Kry10 Limited. All rights reserved.
#

# NIF version of the matrix math library. Accepts only binary form of matrices

# always row major

defmodule Scenic.Math.Matrix do
  @moduledoc """
  A collection of functions to work with matrices.

  All the matrix fucntions in this module work exclusively with the binary form
  of a matrix, which is a compact binary of 16 4-byte floats.

  If you would like to convert back and forth from the more human friendly list
  version, then please use the functions in [Scenic.Math.Matrix.Utils](Scenic.Math.Matrix.Utils)
  """

  alias Scenic.Math
  alias Scenic.Math.Matrix
  import :erlang, only: [{:nif_error, 1}]

  #  import IEx

  @app Mix.Project.config()[:app]

  # load the NIF
  @compile {:autoload, false}
  @on_load :load_nifs
  @doc false
  def load_nifs do
    :ok =
      @app
      |> :code.priv_dir()
      |> :filename.join('matrix')
      |> :erlang.load_nif(0)
  end

  @matrix_size 4 * 16

  @matrix_zero <<
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native
  >>

  @matrix_identity <<
    1.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    1.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    1.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    0.0::float-size(32)-native,
    1.0::float-size(32)-native
  >>

  # ============================================================================
  # common constants
  @doc "A matrix where all the values are 0"
  @spec zero() :: Math.matrix()
  def zero(), do: @matrix_zero

  @doc "The identity matrix"
  @spec identity() :: Math.matrix()
  def identity(), do: @matrix_identity

  # ============================================================================
  # build matrices - all output a binary matrix

  # --------------------------------------------------------
  # explicit builders.

  # --------------------------------------------------------
  # from a tupled matrix
  # def build({{v0x, v0y}, {v1x, v1y}}) do
  #   build({v0x, v0y}, {v1x, v1y})
  # end

  # def build({{v0x, v0y, v0z}, {v1x, v1y, v1z}, {v2x, v2y, v2z}}) do
  #   build({v0x, v0y, v0z}, {v1x, v1y, v1z}, {v2x, v2y, v2z})
  # end

  # def build(
  #       {{v0x, v0y, v0z, v0w}, {v1x, v1y, v1z, v1w}, {v2x, v2y, v2z, v2w}, {v3x, v3y, v3z, v3w}}
  #     ) do
  #   build({v0x, v0y, v0z, v0w}, {v1x, v1y, v1z, v1w}, {v2x, v2y, v2z, v2w}, {v3x, v3y, v3z, v3w})
  # end

  # # --------------------------------------------------------
  # # from a 2x2 tuple matrix
  # def build({v0x, v0y}, {v1x, v1y}) do
  #   <<
  #     v0x::float-size(32)-native,
  #     v0y::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     v1x::float-size(32)-native,
  #     v1y::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     1.0::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     1.0::float-size(32)-native
  #   >>
  # end

  # # --------------------------------------------------------
  # # from a 3x3 matrix
  # def build({v0x, v0y, v0z}, {v1x, v1y, v1z}, {v2x, v2y, v2z}) do
  #   <<
  #     v0x::float-size(32)-native,
  #     v0y::float-size(32)-native,
  #     v0z::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     v1x::float-size(32)-native,
  #     v1y::float-size(32)-native,
  #     v1z::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     v2x::float-size(32)-native,
  #     v2y::float-size(32)-native,
  #     v2z::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     0.0::float-size(32)-native,
  #     1.0::float-size(32)-native
  #   >>
  # end

  # # --------------------------------------------------------
  # # build a 4x4 matrix
  # def build(
  #       {v0x, v0y, v0z, v0w},
  #       {v1x, v1y, v1z, v1w},
  #       {v2x, v2y, v2z, v2w},
  #       {v3x, v3y, v3z, v3w}
  #     ) do
  #   <<
  #     v0x::float-size(32)-native,
  #     v0y::float-size(32)-native,
  #     v0z::float-size(32)-native,
  #     v0w::float-size(32)-native,
  #     v1x::float-size(32)-native,
  #     v1y::float-size(32)-native,
  #     v1z::float-size(32)-native,
  #     v1w::float-size(32)-native,
  #     v2x::float-size(32)-native,
  #     v2y::float-size(32)-native,
  #     v2z::float-size(32)-native,
  #     v2w::float-size(32)-native,
  #     v3x::float-size(32)-native,
  #     v3y::float-size(32)-native,
  #     v3z::float-size(32)-native,
  #     v3w::float-size(32)-native
  #   >>
  # end

  # ============================================================================
  # specific builders. each does a certain job

  # --------------------------------------------------------
  # translation matrix
  @doc """
  Build a matrix that represents a simple translation.

  Parameters:
  * vector_2: the vector defining how much to translate

  Returns:
  A binary matrix
  """
  @spec build_translation(vector_2 :: Math.vector_2()) :: Math.matrix()
  def build_translation(vector_2)
  def build_translation({x, y}), do: do_build_translation({x, y, 0.0})
  # def build_translation({x, y, z}), do: build_translation(x, y, z)
  # def build_translation(x, y), do: build_translation(x, y, 0.0)
  defp do_build_translation({x, y, z}) do
    <<
      1.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      x * 1.0::float-size(32)-native,
      0.0::float-size(32)-native,
      1.0::float-size(32)-native,
      0.0::float-size(32)-native,
      y * 1.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      1.0::float-size(32)-native,
      z * 1.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      1.0::float-size(32)-native
    >>
  end

  # --------------------------------------------------------
  @doc """
  Build a matrix that represents a scaling operation.

  Parameters:
  * scale: the amount to scale by. Can be either a number or a vector_2

  Returns:
  A binary matrix
  """
  @spec build_scale(scale :: number | Math.vector_2()) :: Math.matrix()
  def build_scale(scale)
  def build_scale(s) when is_number(s), do: do_build_scale({s, s, s})
  def build_scale({x, y}), do: do_build_scale({x, y, 1.0})
  # def build_scale({x, y, z}), do: build_scale(x, y, z)
  # def build_scale(x, y), do: build_scale(x, y, 1.0)
  defp do_build_scale({x, y, z}) do
    <<
      x * 1.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      y * 1.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      z * 1.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      1.0::float-size(32)-native
    >>
  end

  # --------------------------------------------------------
  # rotation matrix

  #  def build_rotation( {radians, axis} )
  #      when is_number(radians) and is_atom(axis) do
  #    build_rotation( radians, axis )
  #  end
  # def build_rotation( {axis, radians} )
  #     when is_number(radians) and is_atom(axis) do
  #   build_rotation( radians, axis )
  # end

  @doc """
  Build a matrix that represents a 2D rotation around the origin.

  Parameters:
  * angle: the amount to rotate, in radians

  Returns:
  A binary matrix
  """
  @spec build_rotation(angle :: number) :: Math.matrix()
  def build_rotation(angle)

  # def build_rotation( radians, :x ) do
  #   cos = :math.cos( radians )
  #   sin = :math.sin( radians )
  #   <<
  #     1.0 :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,

  #     0.0 :: float-size(32)-native,
  #     cos :: float-size(32)-native,
  #     sin :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,

  #     0.0 :: float-size(32)-native,
  #     -sin :: float-size(32)-native,
  #     cos :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,

  #     0.0 :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,
  #     1.0 :: float-size(32)-native
  #   >>
  # end

  # def build_rotation( radians, :y ) do
  #   cos = :math.cos( radians )
  #   sin = :math.sin( radians )
  #   <<
  #     cos :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,
  #     sin :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,

  #     0.0 :: float-size(32)-native,
  #     1.0 :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,

  #     -sin :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,
  #     cos :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,

  #     0.0 :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,
  #     0.0 :: float-size(32)-native,
  #     1.0 :: float-size(32)-native
  #   >>
  # end

  def build_rotation(radians) do
    cos = :math.cos(radians)
    sin = :math.sin(radians)

    <<
      cos::float-size(32)-native,
      -sin::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      sin::float-size(32)-native,
      cos::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      1.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      0.0::float-size(32)-native,
      1.0::float-size(32)-native
    >>
  end

  # --------------------------------------------------------
  @doc """
  Build a matrix that represents a 2D rotation around a point.

  Parameters:
  * angle: the amount to rotate, in radians
  * pin: position to pin the rotation around

  Returns:
  A binary matrix
  """
  @spec build_rotate_around(angle :: number, pin :: Math.point()) :: Math.matrix()
  def build_rotate_around(angle, pin)

  def build_rotate_around(radians, {x, y}) do
    build_translation({-x, -y})
    |> Matrix.rotate(radians)
    |> Matrix.translate({x, y})
  end

  # ============================================================================
  # act on a matrix

  # --------------------------------------------------------
  @doc """
  Multiply a matrix by a rotation.

  Parameters:
  * matrix: The incoming source matrix
  * angle: the amount to rotate, in radians or nil (which does nothing

  Returns:
  A binary matrix
  """
  @spec rotate(matrix :: Math.matrix(), angle :: number | nil) :: Math.matrix()
  def rotate(matrix, angle)
  def rotate(matrix, nil), do: matrix

  def rotate(matrix, angle) do
    Matrix.mul(
      matrix,
      build_rotation(angle)
    )
  end

  # def rotate( matrix, radians, axis ) when is_atom(axis) do
  #   build_rotation( radians, axis )
  #   |> ( &Matrix.mul(matrix, &1) ).()
  # end

  # --------------------------------------------------------
  @doc """
  Multiply a matrix by a translation.

  Parameters:
  * matrix: The incoming source matrix
  * vector_2: the vector to translate by or nil (which does nothing)

  Returns:
  A binary matrix
  """
  @spec translate(matrix :: Math.matrix(), vector_2 :: Math.vector_2() | nil) :: Math.matrix()
  def translate(matrix, vector_2)
  def translate(matrix, nil), do: matrix

  def translate(matrix, {x, y}) do
    Matrix.mul(
      matrix,
      build_translation({x, y})
    )
  end

  # def translate(matrix, {x, y, z}), do: translate(matrix, x, y, z)
  # def translate(matrix, nil), do: matrix
  # def translate(matrix, x, y), do: build_translation(x, y) |> (&Matrix.mul(matrix, &1)).()
  # def translate(matrix, x, y, z), do: build_translation(x, y, z) |> (&Matrix.mul(matrix, &1)).()

  # --------------------------------------------------------
  @doc """
  Multiply a matrix by a scale factor.

  Parameters:
  * matrix: The incoming source matrix
  * scale: the amount to scale by. Can be either a number, a vector, or nil (which does nothing)

  Returns:
  A binary matrix
  """
  @spec scale(matrix :: Math.matrix(), scale :: number | Math.vector_2() | nil) :: Math.matrix()
  def scale(matrix, scale)
  def scale(matrix, nil), do: matrix
  # def scale(matrix, {x, y}), do: scale(matrix, {x, y})
  # def scale(matrix, {x, y, z}), do: scale(matrix,{ x, y, z})
  def scale(matrix, s) do
    Matrix.mul(
      matrix,
      build_scale(s)
    )
  end

  # def scale(matrix, x, y), do: build_scale(x, y) |> (&Matrix.mul(matrix, &1)).()
  # def scale(matrix, x, y, z), do: build_scale(x, y, z) |> (&Matrix.mul(matrix, &1)).()

  # ============================================================================
  # get / set functions

  # --------------------------------------------------------
  @doc """
  Get a single value out of a binary matrix.

  Parameters:
  * matrix: The source matrix
  * x: the column to pull the data from
  * y: the row to pull the data from

  Returns:
  A number
  """
  @spec get(matrix :: Math.matrix(), x :: number, y :: number) :: number
  def get(matrix, x, y)

  def get(matrix, x, y)
      when is_integer(x) and is_integer(y) and x >= 0 and y >= 0 and x < 4 and y < 4 do
    skip_size = y * 4 * 4 + x * 4

    <<
      _::binary-size(skip_size),
      v::float-size(32)-native,
      _::binary
    >> = matrix

    v
  end

  # --------------------------------------------------------
  @doc """
  Put a single value into a binary matrix.

  Parameters:
  * matrix: The source matrix
  * x: the column to pull the data from
  * y: the row to pull the data from
  * v: the value to put into the matrix. Must be a number.

  Returns:
  A number
  """
  @spec put(matrix :: Math.matrix(), x :: number, y :: number, v :: number) :: Math.matrix()
  def put(matrix, x, y, v)
  def put(matrix, x, y, v) when is_integer(v), do: put(matrix, x, y, v * 1.0)

  def put(matrix, x, y, v)
      when is_integer(x) and is_integer(y) and is_float(v) and x >= 0 and y >= 0 and x < 4 and
             y < 4 do
    skip_size = y * 4 * 4 + x * 4

    <<
      pre::binary-size(skip_size),
      _::binary-size(4),
      post::binary
    >> = matrix

    <<
      pre::binary,
      v::float-size(32)-native,
      post::binary
    >>
  end

  # --------------------------------------------------------
  @doc """
  Extract the 2D vector represented by the matrix.

  Parameters:
  * matrix: The source matrix

  Returns:
  A vector_2
  """
  @spec get_xy(matrix :: Math.matrix()) :: Math.vector_2()
  def get_xy(matrix)

  def get_xy(<<
        _::binary-size(12),
        x::float-size(32)-native,
        _::binary-size(12),
        y::float-size(32)-native,
        _::binary
      >>) do
    {x, y}
  end

  # --------------------------------------------------------
  # def get_xyz(matrix)
  # def get_xyz(<<
  #       _::binary-size(12),
  #       x::float-size(32)-native,
  #       _::binary-size(12),
  #       y::float-size(32)-native,
  #       _::binary-size(12),
  #       z::float-size(32)-native,
  #       _::binary
  #     >>) do
  #   {x, y, z}
  # end

  # ============================================================================
  # main functions

  # --------------------------------------------------------
  # test if two matrices are close. Is sometimes better than
  # testing equality as floating point errors can be a factor
  @doc """
  Test if two matrices are close in value to each other.

  Parameters:
  * matrix_a: The first matrix
  * matrix_b: The second matrix
  * tolerance: Defines what close means. Defaults to: 0.000001

  Returns:
  A boolean
  """
  @spec close?(matrix_a :: Math.matrix(), matrix_a :: Math.matrix(), tolerance :: number) ::
          boolean
  def close?(matrix_a, matrix_b, tolerance \\ 0.000001)

  def close?(<<_::binary-size(@matrix_size)>> = a, <<_::binary-size(@matrix_size)>> = b, t)
      when is_float(t) do
    # in NIF
    nif_close(a, b, t)
  end

  defp nif_close(_, _, _), do: nif_error("Did not find nif_close")

  # --------------------------------------------------------
  @doc """
  Add two matrices together.

  This operation is implemented as a NIF for performance.

  Parameters:
  * matrix_a: The first matrix
  * matrix_b: The second matrix

  Returns:
  The resulting matrix
  """
  @spec add(matrix_a :: Math.matrix(), matrix_b :: Math.matrix()) :: Math.matrix()
  def add(matrix_a, matrix_b) do
    # in NIF
    nif_add(matrix_a, matrix_b)
  end

  defp nif_add(_, _), do: nif_error("Did not find nif_add")

  # --------------------------------------------------------
  @doc """
  Subtract one matrix from another.

  This operation is implemented as a NIF for performance.

  Parameters:
  * matrix_a: The first matrix
  * matrix_b: The second matrix, which is subtracted from the first

  Returns:
  The resulting matrix
  """
  @spec sub(matrix_a :: Math.matrix(), matrix_b :: Math.matrix()) :: Math.matrix()
  def sub(matrix_a, matrix_b) do
    # in NIF
    nif_subtract(matrix_a, matrix_b)
  end

  defp nif_subtract(_, _), do: nif_error("Did not find nif_subtract")

  # --------------------------------------------------------
  @doc """
  Multiply a list of matrices together.

  This operation is implemented as a NIF for performance.

  Parameters:
  * matrix_list: A list of matrices

  Returns:
  The resulting matrix
  """
  @spec mul(matrix_list :: list(Math.matrix())) :: Math.matrix()
  def mul(matrix_list)

  def mul(matrix_list) when is_list(matrix_list) do
    # in NIF
    nif_multiply_list(matrix_list)
  end

  # --------------------------------------------------------
  @doc """
  Multiply a matrix by another matrix or a scalar.

  This operation is implemented as a NIF for performance.

  Parameters:
  * matrix: A matrix
  * multiplier: A number (scalar) or a matrix to multiply by

  Returns:
  The resulting matrix
  """
  @spec mul(matrix :: Math.matrix(), multiplier :: number | Math.matrix()) :: Math.matrix()

  def mul(matrix, multiplier)
  def mul(a, s) when is_integer(s), do: mul(a, s * 1.0)

  def mul(a, s) when is_float(s) do
    # in NIF
    nif_multiply_scalar(a, s)
  end

  # multiply two matrixes
  def mul(matrix_a, matrix_b) do
    # in NIF
    nif_multiply(matrix_a, matrix_b)
  end

  defp nif_multiply(_, _), do: nif_error("Did not find nif_multiply")
  defp nif_multiply_scalar(_, _), do: nif_error("Did not find nif_multiply_scalar")
  defp nif_multiply_list(_), do: nif_error("Did not find nif_multiply_list")

  # --------------------------------------------------------
  @doc """
  Divide a matrix by a scalar.

  This operation is implemented as a NIF for performance.

  Parameters:
  * matrix: A matrix
  * divisor: A number (scalar) to divide by

  Returns:
  The resulting matrix
  """
  @spec div(matrix :: Math.matrix(), divisor :: number) :: Math.matrix()
  def div(matrix, scalar)
  def div(a, s) when is_integer(s), do: Matrix.div(a, s * 1.0)

  def div(a, s) when is_float(s) do
    # in NIF
    nif_divide_scalar(a, s)
  end

  defp nif_divide_scalar(_, _), do: nif_error("Did not find nif_divide_scalar")

  # --------------------------------------------------------
  @doc """
  Transpose a matrix

  This operation is implemented as a NIF for performance.

  Parameters:
  * matrix: A matrix

  Returns:
  The resulting matrix
  """
  @spec transpose(matrix :: Math.matrix()) :: Math.matrix()
  def transpose(matrix) do
    # in NIF
    nif_transpose(matrix)
  end

  defp nif_transpose(_), do: nif_error("Did not find nif_transpose")

  # --------------------------------------------------------
  @doc """
  Calculate the determinant of a matrix

  This operation is implemented as a NIF for performance.

  Parameters:
  * matrix: A matrix

  Returns:
  The resulting matrix
  """
  @spec determinant(matrix :: Math.matrix()) :: Math.matrix()
  def determinant(matrix) do
    # in NIF
    nif_determinant(matrix)
  end

  defp nif_determinant(_), do: nif_error("Did not find nif_determinant")

  # --------------------------------------------------------
  @doc """
  Calculate the adjugate of a matrix

  This operation is implemented as a NIF for performance.

  Parameters:
  * matrix: A matrix

  Returns:
  The resulting matrix
  """
  @spec adjugate(matrix :: Math.matrix()) :: Math.matrix()
  def adjugate(matrix) do
    # in NIF
    nif_adjugate(matrix)
  end

  defp nif_adjugate(_), do: nif_error("Did not find nif_adjugate")

  # --------------------------------------------------------
  @doc """
  Inverte a matrix.

  This operation is implemented as a NIF for performance.

  Parameters:
  * matrix: A matrix

  Returns:
  The resulting matrix
  """
  @spec invert(matrix :: Math.matrix()) :: Math.matrix()
  def invert(matrix) do
    case nif_determinant(matrix) do
      0.0 ->
        :err_zero_determinant

      det ->
        matrix
        |> nif_adjugate()
        |> nif_multiply_scalar(1.0 / det)
    end
  end

  # --------------------------------------------------------
  @doc """
  Project a vector by a matrix.

  This operation is implemented as a NIF for performance.

  Parameters:
  * matrix: A matrix
  * vector: The vector to project

  Returns:
  The projected vector
  """
  @spec project_vector(matrix :: Math.matrix(), vector_2 :: Math.vector_2()) :: Math.vector_2()
  def project_vector(matrix, {x, y}) do
    # in NIF
    nif_project_vector2(matrix, x, y)
  end

  # --------------------------------------------------------
  # def project_vector(a, {x, y, z}) do
  #   # in NIF
  #   nif_project_vector3(a, x, y, z)
  # end
  defp nif_project_vector2(_, _, _), do: nif_error("Did not find nif_project_vector2")
  # defp nif_project_vector3(_, _, _, _), do: nif_error("Did not find nif_project_vector3")

  # --------------------------------------------------------
  @doc """
  Project a list of vectors by a matrix.

  This operation is implemented as a NIF for performance.

  Parameters:
  * matrix: A matrix
  * vectors: The list of vectors to project

  Returns:
  A list of projected vectors
  """
  @spec project_vector(matrix :: Math.matrix(), vector_list :: list(Math.vector_2())) ::
          list(Math.vector_2())
  def project_vectors(a, vector_bin) do
    # in NIF
    nif_project_vector2s(a, vector_bin)
  end

  defp nif_project_vector2s(_, _), do: nif_error("Did not find nif_project_vector2s")

  # --------------------------------------------------------
  # def project_vector3s(a, vector_bin) do
  #   # in NIF
  #   nif_project_vector3s(a, vector_bin)
  # end
  # defp nif_project_vector3s(_, _), do: nif_error("Did not find nif_project_vector3s")
end