Triangle3D.java

/*
 * Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.irurueta.geometry;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * This class defines a triangle in the 3D space.
 */
@SuppressWarnings("DuplicatedCode")
public class Triangle3D implements Serializable {
    /**
     * Default threshold value. Thresholds are used to determine whether a point
     * lies inside the triangle or not, or if its locus or not, etc.
     */
    public static final double DEFAULT_THRESHOLD = 1e-9;

    /**
     * Minimum allowed threshold value
     */
    public static final double MIN_THRESHOLD = 0.0;

    /**
     * Constant defining number of coordinates
     */
    public static final int INHOM_COORDS = 3;

    /**
     * Constant defining number of vertices on a triangle
     */
    public static final int NUM_VERTICES = 3;

    /**
     * 1st vertex of this triangle.
     */
    private Point3D vertex1;

    /**
     * 2nd vertex of this triangle.
     */
    private Point3D vertex2;

    /**
     * 3rd vertex of this triangle.
     */
    private Point3D vertex3;

    /**
     * Constructor.
     *
     * @param vertex1 1st vertex.
     * @param vertex2 2nd vertex.
     * @param vertex3 3rd vertex.
     * @throws NullPointerException Raised if any of the vertices is null.
     */
    public Triangle3D(final Point3D vertex1, final Point3D vertex2, final Point3D vertex3) {
        setVertices(vertex1, vertex2, vertex3);
    }

    /**
     * Returns 1st vertex of this triangle.
     *
     * @return 1st vertex.
     */
    public Point3D getVertex1() {
        return vertex1;
    }

    /**
     * Sets 1st vertex of this triangle.
     *
     * @param vertex1 1st vertex.
     * @throws NullPointerException Raised if provided vertex is null.
     */
    public void setVertex1(final Point3D vertex1) {
        if (vertex1 == null) {
            throw new NullPointerException();
        }
        this.vertex1 = vertex1;
    }

    /**
     * Returns 2nd vertex of this triangle.
     *
     * @return 2nd vertex.
     */
    public Point3D getVertex2() {
        return vertex2;
    }

    /**
     * Sets 2nd vertex of this triangle.
     *
     * @param vertex2 2nd vertex.
     * @throws NullPointerException Raised if provided vertex is null.
     */
    public void setVertex2(final Point3D vertex2) {
        if (vertex2 == null) {
            throw new NullPointerException();
        }
        this.vertex2 = vertex2;
    }

    /**
     * Returns 3rd vertex of this triangle.
     *
     * @return 3rd vertex.
     */
    public Point3D getVertex3() {
        return vertex3;
    }

    /**
     * Sets 3rd vertex of this triangle.
     *
     * @param vertex3 3rd vertex.
     * @throws NullPointerException Raised if provided vertex is null.
     */
    public void setVertex3(final Point3D vertex3) {
        if (vertex3 == null) {
            throw new NullPointerException();
        }
        this.vertex3 = vertex3;
    }

    /**
     * Returns vertices of this triangle as a list of points.
     *
     * @return Vertices of this triangle.
     */
    public List<Point3D> getVertices() {
        final var vertices = new ArrayList<Point3D>(NUM_VERTICES);
        vertices(vertices);
        return vertices;
    }

    /**
     * Stores vertices of this triangle in provided list. Note that content of
     * list will be cleared before storing this triangle's vertices.
     *
     * @param result list where vertices will be stored.
     */
    public void vertices(final List<Point3D> result) {
        result.clear();
        result.add(vertex1);
        result.add(vertex2);
        result.add(vertex3);
    }

    /**
     * Sets all vertices of this triangle.
     *
     * @param vertex1 1st vertex.
     * @param vertex2 2nd vertex.
     * @param vertex3 3rd vertex.
     * @throws NullPointerException Raised if any of the vertices is null.
     */
    public final void setVertices(final Point3D vertex1, final Point3D vertex2, final Point3D vertex3) {
        if (vertex1 == null || vertex2 == null || vertex3 == null) {
            throw new NullPointerException();
        }

        this.vertex1 = vertex1;
        this.vertex2 = vertex2;
        this.vertex3 = vertex3;
    }

    /**
     * Returns area of provided triangle.
     *
     * @param triangle Triangle to be evaluated.
     * @return Area of triangle.
     */
    public static double area(final Triangle3D triangle) {
        return area(triangle.getVertex1(), triangle.getVertex2(), triangle.getVertex3());
    }

    /**
     * Returns area of the triangle formed by provided vertices.
     *
     * @param vertex1 1st vertex of a triangle.
     * @param vertex2 2nd vertex of a triangle.
     * @param vertex3 3rd vertex of a triangle.
     * @return Area of a triangle.
     */
    public static double area(final Point3D vertex1, final Point3D vertex2, final Point3D vertex3) {
        // The signed area of a triangle is half the determinant of its vectors,
        // or half the modulus of the cross product of its vectors

        // Hence, having the vectors of the triangle defined as:
        // v1 = vertex2 - vertex1, and v2 = vertex3 - vertex1, then:
        final var inhomX1 = vertex1.getInhomX();
        final var inhomY1 = vertex1.getInhomY();
        final var inhomZ1 = vertex1.getInhomZ();

        // given triangle ABC made by vectors ab and ac
        final var abX = vertex2.getInhomX() - inhomX1;
        final var abY = vertex2.getInhomY() - inhomY1;
        final var abZ = vertex2.getInhomZ() - inhomZ1;

        final var acX = vertex3.getInhomX() - inhomX1;
        final var acY = vertex3.getInhomY() - inhomY1;
        final var acZ = vertex3.getInhomZ() - inhomZ1;

        // the area of ABC is half the modulus of the cross product of
        // vectors ab and ac
        final var crossX = abY * acZ - abZ * acY;
        final var crossY = abZ * acX - abX * acZ;
        final var crossZ = abX * acY - abY * acX;

        return 0.5 * Math.sqrt(crossX * crossX + crossY * crossY + crossZ * crossZ);
    }

    /**
     * Returns area of this triangle.
     *
     * @return Area of this triangle.
     */
    public double getArea() {
        return area(vertex1, vertex2, vertex3);
    }

    /**
     * Determines whether vertices of this triangle are considered to be
     * co-linear. Points are considered to be colinear when area of triangle is
     * very small.
     *
     * @return True if vertices are colinear, false otherwise.
     */
    public boolean areVerticesColinear() {
        return areVerticesColinear(DEFAULT_THRESHOLD);
    }

    /**
     * Determines whether vertices of this triangle are considered to be
     * co-linear up to certain threshold. Points are considered to be colinear
     * when are of triangle is very small.
     *
     * @param threshold Threshold to determine whether vertices are colinear.
     *                  Vertices will be colinear when area of triangle is smaller than provided
     *                  threshold.
     * @return True if vertices are colinear, false otherwise.
     * @throws IllegalArgumentException Raised if provided threshold is negative.
     */
    public boolean areVerticesColinear(final double threshold) {
        if (threshold < MIN_THRESHOLD) {
            throw new IllegalArgumentException();
        }
        return getArea() <= threshold;
    }

    /**
     * Returns perimeter of provided triangle.
     *
     * @param triangle Perimeter of provided triangle.
     * @return Perimeter of provided triangle.
     */
    public static double perimeter(final Triangle3D triangle) {
        return perimeter(triangle.getVertex1(), triangle.getVertex2(), triangle.getVertex3());
    }

    /**
     * Returns perimeter of triangle formed by provided vertices.
     *
     * @param vertex1 1st vertex of a triangle.
     * @param vertex2 2nd vertex of a triangle.
     * @param vertex3 3rd vertex of a triangle.
     * @return Perimeter of a triangle.
     */
    public static double perimeter(final Point3D vertex1, final Point3D vertex2, final Point3D vertex3) {
        return vertex1.distanceTo(vertex2) + vertex2.distanceTo(vertex3) + vertex3.distanceTo(vertex1);
    }

    /**
     * Returns perimeter of this triangle.
     *
     * @return Perimeter of this triangle.
     */
    public double getPerimeter() {
        return perimeter(this);
    }

    /**
     * Indicates whether provided point lies inside this triangle or not.
     * To lie inside point must be on the same plane formed by this triangle and
     * within triangle boundaries.
     *
     * @param point Point to be checked.
     * @return True if point lies inside this triangle, false otherwise.
     */
    public boolean isInside(final Point3D point) {
        return isInside(point, DEFAULT_THRESHOLD);
    }

    /**
     * Indicates whether provided point lies inside this triangle or not up to
     * a certain threshold.
     * To lie inside point must be on the same plane formed by this triangle and
     * within triangle boundaries up to a certain threshold.
     *
     * @param point     Point to be checked.
     * @param threshold Threshold to determine whether point is inside this
     *                  triangle, or not. This should usually be a small value.
     * @return True if point lies inside this triangle, false otherwise.
     * @throws IllegalArgumentException Raised if provided threshold is negative.
     */
    public boolean isInside(final Point3D point, final double threshold) {
        return isInside(vertex1, vertex2, vertex3, point, threshold);
    }

    /**
     * Indicates whether provided point lies inside provided triangle or not
     * To lie inside point must be on the same plane formed by provided triangle
     * and within triangle boundaries.
     *
     * @param triangle A triangle.
     * @param point    Point to be checked.
     * @return True if point lies inside provided triangle, false otherwise.
     */
    public static boolean isInside(final Triangle3D triangle, final Point3D point) {
        return isInside(triangle, point, DEFAULT_THRESHOLD);
    }

    /**
     * Indicates whether provided point lies inside provided triangle or not up
     * to a certain threshold.
     * To lie inside point must be on the same plane formed by provided triangle
     * and within triangle boundaries up to a certain threshold.
     *
     * @param triangle  A triangle.
     * @param point     Point to be checked.
     * @param threshold Threshold to determine whether point is inside this
     *                  triangle, or not. This should usually be a small value.
     * @return True if point lies inside this triangle, false otherwise.
     * @throws IllegalArgumentException Raised if provided threshold is negative.
     */
    public static boolean isInside(final Triangle3D triangle, final Point3D point, final double threshold) {
        return isInside(triangle.getVertex1(), triangle.getVertex2(), triangle.getVertex3(), point, threshold);
    }

    /**
     * Indicates whether provided point lies inside a triangle formed by
     * provided vertices or not.
     * To lie inside point must be on the same plane formed by that triangle and
     * within its boundaries.
     *
     * @param vertex1 1st vertex of a triangle.
     * @param vertex2 2nd vertex of a triangle.
     * @param vertex3 3rd vertex of a triangle.
     * @param point   Point to be checked.
     * @return True if point lies inside triangle formed by provided vertices,
     * false otherwise.
     */
    public static boolean isInside(
            final Point3D vertex1, final Point3D vertex2, final Point3D vertex3, final Point3D point) {
        return isInside(vertex1, vertex2, vertex3, point, DEFAULT_THRESHOLD);
    }

    /**
     * Indicates whether provided point lies inside a triangle formed by
     * provided vertices or not up to a certain threshold.
     * To lie inside point must be on the same plane formed by that triangle and
     * within its boundaries up to a certain threshold.
     *
     * @param vertex1   1st vertex of a triangle.
     * @param vertex2   2nd vertex of a triangle.
     * @param vertex3   3rd vertex of a triangle.
     * @param point     Point to be checked.
     * @param threshold Threshold to determine whether point is inside the
     *                  triangle formed by provided vertices or not. This should usually be a
     *                  small value.
     * @return True if point lies inside triangle formed by provided vertices,
     * false otherwise.
     * @throws IllegalArgumentException Raised if provided threshold is negative.
     */
    public static boolean isInside(
            final Point3D vertex1, final Point3D vertex2,
            final Point3D vertex3, final Point3D point, final double threshold) {
        if (threshold < MIN_THRESHOLD) {
            throw new IllegalArgumentException();
        }
        // given triangle ABC made by vectors:
        // ab = p2 - p1, and ac = p3 - p1

        // If point (x, y) lies within triangle ABC, then we have 3 sub-triangles
        // ApB, BpC and ApC made of points:
        // ApB: mVertex1, point, mVertex2
        // BpC: mVertex2, point, mVertex3
        // ApC: mVertex3, point, mVertex1

        // The point will lie inside triangle ABC if the sum of the areas of the
        // 3 sub-triangles ApB, BpC and ApC equals the area of triangle ABC (up to
        // certain accuracy to account for numerical precision)

        // Then the areas of triangles are:
        final var areaABC = area(vertex1, vertex2, vertex3);

        final var areaApB = area(vertex1, point, vertex2);
        final var areaBpC = area(vertex2, point, vertex3);
        final var areaApC = area(vertex3, point, vertex1);

        return Math.abs(areaApB + areaBpC + areaApC - areaABC) <= threshold;
    }

    /**
     * Returns the plane formed by the vertices of this triangle.
     * The difference between a plane and a 3D triangle is that a triangle has
     * its boundaries defined, whereas a plane extends up to the infinity.
     *
     * @return A plane.
     * @throws ColinearPointsException Raised if vertices of this triangle are
     *                                 co-linear (triangle has area equal or very close to 0.0).
     */
    public Plane toPlane() throws ColinearPointsException {
        return new Plane(vertex1, vertex2, vertex3);
    }

    /**
     * Computes the plane formed by the vertices of this triangle and stores the
     * result into provided Plane instance.
     * The difference between a plane and a 3D triangle is that a triangle has
     * its boundaries defined, whereas a plane extends up to the infinity.
     *
     * @param result Instance where resulting plane will be stored.
     * @throws ColinearPointsException Raised if vertices of this triangle are
     *                                 co-linear (triangle has area equal or very close to 0.0).
     */
    public void toPlane(final Plane result) throws ColinearPointsException {
        result.setParametersFromThreePoints(vertex1, vertex2, vertex3);
    }

    /**
     * Returns center of this triangle, which is the result of averaging its
     * vertices.
     *
     * @return Center of this triangle.
     */
    public Point3D getCenter() {
        final var result = Point3D.create();
        center(result);
        return result;
    }

    /**
     * Computes the center of this triangle and stores the result in provided
     * point. The center of this triangle is computed as the average of its
     * vertices.
     *
     * @param result Point instance where center will be stored.
     */
    public void center(final Point3D result) {
        center(vertex1, vertex2, vertex3, result);
    }

    /**
     * Computes the center of a triangle formed by provided vertices.
     * The center is computed as the average of the three vertices.
     *
     * @param vertex1 1st vertex of a triangle.
     * @param vertex2 2nd vertex of a triangle.
     * @param vertex3 3rd vertex of a triangle.
     * @return Center of a triangle formed by provided vertices.
     */
    public static Point3D center(final Point3D vertex1, final Point3D vertex2, final Point3D vertex3) {
        final var result = Point3D.create();
        center(vertex1, vertex2, vertex3, result);
        return result;
    }

    /**
     * Computes the center of provided triangle.
     * The center is computed as the average of the vertices of provided
     * triangle.
     *
     * @param t A triangle.
     * @return Center of provided triangle.
     */
    public static Point3D center(final Triangle3D t) {
        return center(t.getVertex1(), t.getVertex2(), t.getVertex3());
    }

    /**
     * Computes the center of a triangle formed by provided vertices and stores
     * the result in provided result point.
     * The center is computed as the average of provided vertices.
     *
     * @param vertex1 1st vertex of a triangle.
     * @param vertex2 2nd vertex of a triangle.
     * @param vertex3 3rd vertex of a triangle.
     * @param result  Point instance where center will be stored.
     */
    public static void center(
            final Point3D vertex1, final Point3D vertex2, final Point3D vertex3, final Point3D result) {

        final var x = (vertex1.getInhomX() + vertex2.getInhomX() + vertex3.getInhomX()) / 3.0;
        final var y = (vertex1.getInhomY() + vertex2.getInhomY() + vertex3.getInhomY()) / 3.0;
        final var z = (vertex1.getInhomZ() + vertex2.getInhomZ() + vertex3.getInhomZ()) / 3.0;

        result.setInhomogeneousCoordinates(x, y, z);
    }

    /**
     * Computes the center of provided triangle and stores the result in
     * provided result point.
     * The center is computed as the average of the vertices of provided
     * triangle.
     *
     * @param t      A triangle.
     * @param result Point instance where center will be stored.
     */
    public static void center(final Triangle3D t, final Point3D result) {
        center(t.getVertex1(), t.getVertex2(), t.getVertex3(), result);
    }

    /**
     * Computes the shortest distance from a given point to the boundaries of
     * this triangle, considering its boundaries as lines with a finite length
     * Distance is computed up to triangle boundary, no matter if point lies
     * inside the triangle or not.
     *
     * @param point Point to be checked.
     * @return Shortest distance to this triangle.
     */
    public double getShortestDistance(final Point3D point) {
        return shortestDistance(this, point);
    }

    /**
     * Computes the shortest distance from a given point to the boundaries of
     * provided triangle, considering its boundaries as lines with a finite
     * length.
     * Distance is computed up to triangle boundary, no matter if point lies
     * inside the triangle or not.
     *
     * @param triangle A triangle.
     * @param point    Point to be checked.
     * @return Shortest distance to this triangle.
     */
    public static double shortestDistance(final Triangle3D triangle, final Point3D point) {
        return shortestDistance(triangle.getVertex1(), triangle.getVertex2(), triangle.getVertex3(), point);
    }

    /**
     * Computes the shortest distance from a given point to the boundaries of
     * a triangle formed by provided vertices, where those boundaries are
     * considered to be lines with a finite length.
     * Distance is computed up to triangle boundary, no matter if point lies
     * inside the triangle or not.
     *
     * @param vertex1 1st vertex of a triangle.
     * @param vertex2 2nd vertex of a triangle.
     * @param vertex3 3rd vertex of a triangle.
     * @param point   Point to be checked.
     * @return Shortest distance to the triangle formed by provided vertices.
     */
    public static double shortestDistance(
            final Point3D vertex1, final Point3D vertex2, final Point3D vertex3, final Point3D point) {

        // normalize points to increase accuracy
        vertex1.normalize();
        vertex2.normalize();
        vertex3.normalize();
        point.normalize();

        var bestDist = Double.MAX_VALUE;
        var dist = Double.MAX_VALUE;

        Line3D line = null;
        try {
            line = new Line3D(vertex1, vertex2);
            //to increase accuracy
            line.normalize();
            if (line.isLocus(point)) {
                if (point.isBetween(vertex1, vertex2)) {
                    return 0.0;
                } else {
                    // point is outside the triangle and
                    // point belongs to the line forming this side of the
                    // triangle, hence the closest vertex of this line will be
                    // the shortest distance
                    bestDist = vertex1.distanceTo(point);
                    dist = vertex2.distanceTo(point);
                    if (dist < bestDist) {
                        bestDist = dist;
                    }

                    return bestDist;
                }
            }

            // point does not belong to the first line
            bestDist = Math.abs(line.getDistance(point));
        } catch (final CoincidentPointsException e) {
            if (point.equals(vertex1)) {
                return vertex1.distanceTo(point);
            }
            if (point.equals(vertex2)) {
                return vertex2.distanceTo(point);
            }
        }

        if (line == null) {
            return bestDist;
        }

        // try on second side of the triangle
        try {
            line.setPlanesFromPoints(vertex1, vertex3);
            // to increase accuracy
            line.normalize();
            if (line.isLocus(point)) {
                if (point.isBetween(vertex1, vertex3)) {
                    return 0.0;
                } else {
                    // point belongs to the line forming this side of the
                    // triangle, hence the closest vertex of this line will be
                    // the shortest distance
                    bestDist = vertex1.distanceTo(point);
                    dist = vertex3.distanceTo(point);
                    if (dist < bestDist) {
                        bestDist = dist;
                    }

                    return bestDist;
                }
            }

            // point does not belong to the first or second line
            dist = Math.abs(line.getDistance(point));
        } catch (final CoincidentPointsException e) {
            if (point.equals(vertex1)) {
                return vertex1.distanceTo(point);
            }
            if (point.equals(vertex3)) {
                return vertex3.distanceTo(point);
            }
        }

        // check if second line is closest to first line
        if (dist < bestDist) {
            bestDist = dist;
        }

        // try on third side of the triangle
        try {
            line.setPlanesFromPoints(vertex2, vertex3);
            // to increase accuracy
            line.normalize();
            if (line.isLocus(point)) {
                if (point.isBetween(vertex2, vertex3)) {
                    return 0.0;
                } else {
                    // point belongs to the line forming this side of the
                    // triangle, hence the closest vertex of this line will be
                    // the shortest distance
                    bestDist = vertex2.distanceTo(point);
                    dist = vertex3.distanceTo(point);
                    if (dist < bestDist) {
                        bestDist = dist;
                    }

                    return bestDist;
                }
            }

            // point does not belong to any line forming a side of the triangle
            dist = Math.abs(line.getDistance(point));
        } catch (final CoincidentPointsException e) {
            if (point.equals(vertex2)) {
                return vertex2.distanceTo(point);
            }
            if (point.equals(vertex3)) {
                return vertex3.distanceTo(point);
            }
        }

        // check if distance to third line is the shortest
        if (dist < bestDist) {
            bestDist = dist;
        }

        return bestDist;
    }

    /**
     * Returns the point which is locus of this triangle closest to provided
     * point.
     *
     * @param point Point to be checked.
     * @return Closest point laying in this triangle boundaries.
     */
    public Point3D getClosestPoint(final Point3D point) {
        return getClosestPoint(point, DEFAULT_THRESHOLD);
    }

    /**
     * Returns the point which is locus of this triangle (up to a certain
     * threshold) closest to provided point.
     *
     * @param point     Point to be checked.
     * @param threshold Threshold to determine when a point is locus of this
     *                  triangle or not.
     * @return Closest point laying in this triangle boundaries.
     * @throws IllegalArgumentException Raised if provided threshold is negative.
     */
    public Point3D getClosestPoint(final Point3D point, final double threshold) {
        final var result = Point3D.create();
        closestPoint(point, result, threshold);
        return result;
    }

    /**
     * Computes the point which is locus of this triangle closest to provided
     * point and stores the result in provided result point.
     *
     * @param point  Point to be checked.
     * @param result Point where result will be stored.
     */
    public void closestPoint(final Point3D point, final Point3D result) {
        closestPoint(point, result, DEFAULT_THRESHOLD);
    }

    /**
     * Computes the point which is locus of this triangle (up to a certain
     * threshold) closest to provided point and stores the result in provided
     * result point.
     *
     * @param point     Point to be checked.
     * @param result    Point where result will be stored.
     * @param threshold Threshold to determine when a point is locus of this
     *                  triangle or not.
     * @throws IllegalArgumentException Raised if provided threshold is negative.
     */
    public void closestPoint(final Point3D point, final Point3D result, final double threshold) {
        if (threshold < MIN_THRESHOLD) {
            throw new IllegalArgumentException();
        }

        // normalize vertices and point to increase accuracy
        vertex1.normalize();
        vertex2.normalize();
        vertex3.normalize();
        point.normalize();

        Line3D line1;
        Line3D line2;
        Line3D line3;
        try {
            line1 = new Line3D(vertex1, vertex2);
            // to increase accuracy
            line1.normalize();
            if (line1.isLocus(point)) {
                if (point.isBetween(vertex1, vertex2)) {
                    // point is on this side of the triangle, so point must
                    // be the result
                    result.setCoordinates(point);
                } else {
                    // point belongs to the line forming this side of the
                    // triangle, hence the closest vertex of this line will be
                    // the closest point to the triangle
                    final var dist1 = vertex1.distanceTo(point);
                    final var dist2 = vertex2.distanceTo(point);
                    if (dist1 < dist2) {
                        result.setCoordinates(vertex1);
                    } else {
                        result.setCoordinates(vertex2);
                    }
                }
                return;
            }
        } catch (final CoincidentPointsException e) {
            if (point.equals(vertex1) || point.equals(vertex2)) {
                result.setCoordinates(point);
            }
            return;
        }


        // try on second side of the triangle
        try {
            line2 = new Line3D(vertex1, vertex3);
            // to increase accuracy
            line2.normalize();
            if (line2.isLocus(point)) {
                if (point.isBetween(vertex1, vertex3)) {
                    // point is on this side of the triangle, so point must be
                    // the result
                    result.setCoordinates(point);
                } else {
                    // point belongs to the line forming this side of the
                    // triangle, hence the closest vertex of this line will be
                    // the closest point to the triangle
                    final var dist1 = vertex1.distanceTo(point);
                    final var dist3 = vertex3.distanceTo(point);
                    if (dist1 < dist3) {
                        result.setCoordinates(vertex1);
                    } else {
                        result.setCoordinates(vertex3);
                    }
                }
                return;
            }
        } catch (final CoincidentPointsException e) {
            if (point.equals(vertex1) || point.equals(vertex3)) {
                result.setCoordinates(point);
            }
            return;
        }


        // try on third side of the triangle
        try {
            line3 = new Line3D(vertex2, vertex3);
            // to increase accuracy
            line3.normalize();
            if (line3.isLocus(point)) {
                if (point.isBetween(vertex2, vertex3)) {
                    // point is on this side of the triangle, so point must be
                    // the result
                    result.setCoordinates(point);
                } else {
                    // point belongs to the line forming this side of the
                    // triangle, hence the closest vertex of this line will be
                    // the closest point to the triangle
                    final var dist2 = vertex2.distanceTo(point);
                    final var dist3 = vertex3.distanceTo(point);
                    if (dist2 < dist3) {
                        result.setCoordinates(vertex2);
                    } else {
                        result.setCoordinates(vertex3);
                    }
                }
                return;
            }
        } catch (final CoincidentPointsException e) {
            if (point.equals(vertex2) || point.equals(vertex3)) {
                result.setCoordinates(point);
            }
            return;
        }


        // point does not belong to any line forming a side of the triangle,
        // so we find the closest point for each side
        Point3D closest1;
        Point3D closest2;
        Point3D closest3;

        closest1 = line1.getClosestPoint(point, threshold);
        // to increase accuracy
        closest1.normalize();

        closest2 = line2.getClosestPoint(point, threshold);
        // to increase accuracy
        closest2.normalize();

        closest3 = line3.getClosestPoint(point, threshold);
        // to increase accuracy
        closest3.normalize();

        // check if points lie within sides of triangle
        final var between1 = closest1.isBetween(vertex1, vertex2);
        final var between2 = closest2.isBetween(vertex1, vertex3);
        final var between3 = closest3.isBetween(vertex2, vertex3);

        final var distClosest1 = closest1.distanceTo(point);
        final var distClosest2 = closest2.distanceTo(point);
        final var distClosest3 = closest3.distanceTo(point);

        final var distVertex1 = vertex1.distanceTo(point);
        final var distVertex2 = vertex2.distanceTo(point);
        final var distVertex3 = vertex3.distanceTo(point);

        if (between1 && !between2 && !between3) {
            // choose closest1 or opposite vertex (vertex3)
            if (distClosest1 < distVertex3) {
                result.setCoordinates(closest1);
            } else {
                result.setCoordinates(vertex3);
            }
        } else if (!between1 && between2 && !between3) {
            // choose closest2 or opposite vertex (vertex2)
            if (distClosest2 < distVertex2) {
                result.setCoordinates(closest2);
            } else {
                result.setCoordinates(vertex2);
            }
        } else if (!between1 && !between2 && between3) {
            // choose closest3 or opposite vertex (vertex1)
            if (distClosest3 < distVertex1) {
                result.setCoordinates(closest3);
            } else {
                result.setCoordinates(vertex1);
            }
        } else if (between1 && between2 && !between3) {
            // determine if closest1 or closest2
            if (distClosest1 < distClosest2) {
                result.setCoordinates(closest1);
            } else {
                result.setCoordinates(closest2);
            }
        } else if (!between1 && between2) {
            // and between3

            // determine if closest2 or closest3
            if (distClosest2 < distClosest3) {
                result.setCoordinates(closest2);
            } else {
                result.setCoordinates(closest3);
            }
        } else if (between1 && !between2) {
            // and between3

            // determine if closest1 or closest3
            if (distClosest1 < distClosest3) {
                result.setCoordinates(closest1);
            } else {
                result.setCoordinates(closest3);
            }
        } else if (between1) {
            // and between2 and between3

            // determine if closest1, closest2 or closest3
            if (distClosest1 < distClosest2 && distClosest1 < distClosest3) {
                // pick closest1
                result.setCoordinates(closest1);
            } else if (distClosest2 < distClosest1 && distClosest2 < distClosest3) {
                // pick closest2
                result.setCoordinates(closest2);
            } else {
                // pick closest3
                result.setCoordinates(closest3);
            }
        } else {
            // all closest points are outside vertex limits, so we pick the
            // closest vertex
            if (distVertex1 < distVertex2 && distVertex1 < distVertex3) {
                // pick vertex1
                result.setCoordinates(vertex1);
            } else if (distVertex2 < distVertex1 && distVertex2 < distVertex3) {
                // pick vertex2
                result.setCoordinates(vertex2);
            } else {
                // pick vertex3
                result.setCoordinates(vertex3);
            }
        }
    }

    /**
     * Returns boolean indicating if provided point is locus of this triangle
     * (i.e. lies within this triangle boundaries) up to a certain threshold.
     *
     * @param point     Point to be checked.
     * @param threshold Threshold to determine if point is locus or not. This
     *                  should usually be a small value.
     * @return True if provided point is locus, false otherwise.
     * @throws IllegalArgumentException Raised if provided threshold is negative.
     */
    public boolean isLocus(final Point3D point, final double threshold) {
        if (threshold < MIN_THRESHOLD) {
            throw new IllegalArgumentException();
        }

        return point.isBetween(vertex1, vertex2, threshold) || point.isBetween(vertex1, vertex3, threshold)
                || point.isBetween(vertex2, vertex3, threshold);
    }

    /**
     * Returns boolean indicating if provided point is locus of this triangle
     * (i.e. lies within this triangle boundaries).
     *
     * @param point Point to be checked.
     * @return True if provided point is locus, false otherwise.
     */
    public boolean isLocus(final Point3D point) {
        return isLocus(point, DEFAULT_THRESHOLD);
    }


    /**
     * Returns array containing orientation of this 3D triangle.
     *
     * @return Array containing orientation of this 3D triangle.
     * @throws CoincidentPointsException Raised if vertices of this triangle
     *                                   are too close to each other.
     */
    public double[] getOrientation() throws CoincidentPointsException {
        return orientation(this);
    }

    /**
     * Returns array containing orientation of this 3D triangle.
     *
     * @param threshold Threshold to determine whether vertices of this triangle
     *                  are coincident or not.
     * @return Array containing orientation of this 3D triangle.
     * @throws IllegalArgumentException  Raised if provided threshold is negative.
     * @throws CoincidentPointsException Raised if vertices of this triangle
     *                                   are too close to each other.
     */
    public double[] getOrientation(final double threshold) throws CoincidentPointsException {
        return orientation(this, threshold);
    }

    /**
     * Computes orientation of this 3D triangle and stores the result in
     * provided array.
     *
     * @param result Array where orientation is stored.
     * @throws IllegalArgumentException  Raised if provided array does not have
     *                                   length 3.
     * @throws CoincidentPointsException Raised if vertices of this triangle
     *                                   are too close to each other.
     */
    public void orientation(final double[] result) throws CoincidentPointsException {
        orientation(this, result);
    }

    /**
     * Computes orientation of this 3D triangle and stores the result in
     * provided array.
     *
     * @param result    Array where orientation is stored.
     * @param threshold Threshold to determine whether vertices of this triangle
     *                  are coincident or not.
     * @throws IllegalArgumentException  Raised if provided threshold is negative
     *                                   or if provided array does not have length 3.
     * @throws CoincidentPointsException Raised if vertices of this triangle
     *                                   are too close to each other.
     */
    public void orientation(final double[] result, final double threshold) throws CoincidentPointsException {
        orientation(this, result, threshold);
    }

    /**
     * Returns orientation of provided 3D triangle.
     *
     * @param triangle A triangle.
     * @return Array containing orientation of provided 3D triangle.
     * @throws CoincidentPointsException Raised if vertices of provided triangle
     *                                   are too close to each other.
     */
    public static double[] orientation(final Triangle3D triangle) throws CoincidentPointsException {
        return orientation(triangle.getVertex1(), triangle.getVertex2(), triangle.getVertex3());
    }

    /**
     * Returns orientation of provided 3D triangle.
     *
     * @param triangle  A triangle.
     * @param threshold Threshold to determine whether vertices of provided
     *                  triangle are coincident or not.
     * @return Array containing orientation of provided 3D triangle.
     * @throws IllegalArgumentException  Raised if provided threshold is negative.
     * @throws CoincidentPointsException Raised if vertices of this triangle are
     *                                   too close to each other.
     */
    public static double[] orientation(final Triangle3D triangle, final double threshold)
            throws CoincidentPointsException {
        return orientation(triangle.getVertex1(), triangle.getVertex2(),
                triangle.getVertex3(), threshold);
    }

    /**
     * Computes orientation of provided 3D triangle and stores the result in
     * provided array.
     *
     * @param triangle A triangle.
     * @param result   Array where orientation is stored.
     * @throws IllegalArgumentException  Raised if provided array does not have
     *                                   length 3.
     * @throws CoincidentPointsException Raised if vertices of provided triangle
     *                                   are too close to each other.
     */
    public static void orientation(final Triangle3D triangle, final double[] result) throws CoincidentPointsException {
        orientation(triangle.getVertex1(), triangle.getVertex2(), triangle.getVertex3(), result);
    }

    /**
     * Computes orientation of provided 3D triangle and stores the result in
     * provided array.
     *
     * @param triangle  A triangle.
     * @param result    Array where orientation is stored.
     * @param threshold Threshold to determine whether vertices of provided
     *                  triangle are coincident or not.
     * @throws IllegalArgumentException  Raised if provided threshold is negative
     *                                   or if array does not have length 3.
     * @throws CoincidentPointsException Raised if vertices of provided triangle
     *                                   are too close to each other.
     */
    public static void orientation(
            final Triangle3D triangle, final double[] result, final double threshold) throws CoincidentPointsException {
        orientation(triangle.getVertex1(), triangle.getVertex2(), triangle.getVertex3(), result, threshold);
    }

    /**
     * Returns orientation of 3D triangle formed by provided vertices.
     *
     * @param vertex1 1st vertex.
     * @param vertex2 2nd vertex.
     * @param vertex3 3rd vertex.
     * @return Array containing triangle orientation.
     * @throws CoincidentPointsException Raised if provided vertices are too
     *                                   close to each other.
     */
    public static double[] orientation(
            final Point3D vertex1, final Point3D vertex2, final Point3D vertex3) throws CoincidentPointsException {
        return orientation(vertex1, vertex2, vertex3, DEFAULT_THRESHOLD);
    }

    /**
     * Returns orientation of 3D triangle formed by provided vertices.
     *
     * @param vertex1   1st vertex.
     * @param vertex2   2nd vertex.
     * @param vertex3   3rd vertex.
     * @param threshold Threshold to determine whether provided vertices are
     *                  coincident or not.
     * @return Array containing triangle orientation.
     * @throws IllegalArgumentException  Raised if provided threshold is negative.
     * @throws CoincidentPointsException Raised if provided vertices are too
     *                                   close to each other.
     */
    public static double[] orientation(
            final Point3D vertex1, final Point3D vertex2, final Point3D vertex3, final double threshold)
            throws CoincidentPointsException {
        final var result = new double[INHOM_COORDS];
        orientation(vertex1, vertex2, vertex3, result, threshold);
        return result;
    }

    /**
     * Computes orientation of 3D triangle formed by provided vertices and
     * stores the result in provided array.
     *
     * @param vertex1 1st vertex.
     * @param vertex2 2nd vertex.
     * @param vertex3 3rd vertex.
     * @param result  Array where triangle orientation is stored.
     * @throws IllegalArgumentException  Raised if provided array does not have
     *                                   length 3.
     * @throws CoincidentPointsException Raised if provided vertices are too
     *                                   close to each other.
     */
    public static void orientation(
            final Point3D vertex1, final Point3D vertex2, final Point3D vertex3, final double[] result)
            throws CoincidentPointsException {
        orientation(vertex1, vertex2, vertex3, result, DEFAULT_THRESHOLD);
    }

    /**
     * Computes orientation of 3D triangle formed by provided vertices and
     * stores the result in provided array.
     *
     * @param vertex1   1st vertex.
     * @param vertex2   2nd vertex.
     * @param vertex3   3rd vertex.
     * @param result    Array where triangle orientation is stored.
     * @param threshold Threshold to determine whether provided vertices are
     *                  coincident or not.
     * @throws IllegalArgumentException  Raised if threshold is negative or if
     *                                   provided array does not have length 3.
     * @throws CoincidentPointsException Raised if provided vertices are too
     *                                   close to each other.
     */
    public static void orientation(
            final Point3D vertex1, final Point3D vertex2, final Point3D vertex3, final double[] result,
            final double threshold) throws CoincidentPointsException {

        if (threshold < MIN_THRESHOLD) {
            throw new IllegalArgumentException();
        }
        if (result.length != INHOM_COORDS) {
            throw new IllegalArgumentException();
        }

        final var inhomX1 = vertex1.getInhomX();
        final var inhomY1 = vertex1.getInhomY();
        final var inhomZ1 = vertex1.getInhomZ();

        final var inhomX2 = vertex2.getInhomX();
        final var inhomY2 = vertex2.getInhomY();
        final var inhomZ2 = vertex2.getInhomZ();

        final var inhomX3 = vertex3.getInhomX();
        final var inhomY3 = vertex3.getInhomY();
        final var inhomZ3 = vertex3.getInhomZ();

        // given triangle ABC made by vectors ab and ac
        final var abX = inhomX2 - inhomX1;
        final var abY = inhomY2 - inhomY1;
        final var abZ = inhomZ2 - inhomZ1;

        final var acX = inhomX3 - inhomX1;
        final var acY = inhomY3 - inhomY1;
        final var acZ = inhomZ3 - inhomZ1;

        // the area of ABC is half the modulus of the cross product of
        // vectors ab and ac
        var crossX = abY * acZ - abZ * acY;
        var crossY = abZ * acX - abX * acZ;
        var crossZ = abX * acY - abY * acX;
        // normalize orientation vector
        var norm = Math.sqrt(crossX * crossX + crossY * crossY + crossZ * crossZ);

        if (norm < threshold) {
            throw new CoincidentPointsException();
        }

        crossX /= norm;
        crossY /= norm;
        crossZ /= norm;

        result[0] = crossX;
        result[1] = crossY;
        result[2] = crossZ;
    }

    /**
     * Returns the angle formed by the two provided triangles, assuming that
     * each triangle forms a plane.
     *
     * @param triangle1 1st triangle.
     * @param triangle2 2nd triangle.
     * @return Angle formed by the two provided triangles expressed in radians.
     * @throws CoincidentPointsException Raised if vertices in a triangle are
     *                                   too close. This usually indicates numerical instability or triangle
     *                                   degeneracy.
     */
    public static double getAngleBetweenTriangles(final Triangle3D triangle1, final Triangle3D triangle2)
            throws CoincidentPointsException {
        return getAngleBetweenTriangles(triangle1.getOrientation(), triangle2.getOrientation());
    }

    /**
     * Internal method to compute the angle between two triangles using the
     * vectors containing the director vector of their corresponding planes
     * (i.e. their orientations).
     *
     * @param orientation1 Orientation of 1st triangle.
     * @param orientation2 Orientation of 2nd triangle.
     * @return Angle formed by the two triangles expressed in radians.
     * @throws IllegalArgumentException Raised if provided orientation arrays
     *                                  don't have length 3.
     */
    private static double getAngleBetweenTriangles(
            final double[] orientation1, final double[] orientation2) {
        if (orientation1.length != INHOM_COORDS ||
                orientation2.length != INHOM_COORDS) {
            throw new IllegalArgumentException();
        }

        final var x1 = orientation1[0];
        final var y1 = orientation1[1];
        final var z1 = orientation1[2];

        final var x2 = orientation2[0];
        final var y2 = orientation2[1];
        final var z2 = orientation2[2];

        final var norm1 = Math.sqrt(x1 * x1 + y1 * y1 + z1 * z1);
        final var norm2 = Math.sqrt(x2 * x2 + y2 * y2 + z2 * z2);

        final var dotProduct = (x1 * x2 + y1 * y2 + z1 * z2) / (norm1 * norm2);

        return Math.acos(dotProduct);
    }
}