This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit a81b6f8cf270b81e947a9b5a5067a5b755a05487 Author: jsorel <[email protected]> AuthorDate: Mon Oct 19 10:11:02 2026 +0200 feat(Geometry): improve earclipping, add contains and intersections tests --- .../apache/sis/geometries/operation/Contains.java | 141 +++++- .../sis/geometries/operation/Intersection.java | 549 +++++++++++++++++++++ .../operation/triangulate/EarClipping.java | 65 ++- .../main/org/apache/sis/maths/Maths.java | 28 ++ .../sis/geometries/operation/Contains3DTest.java | 182 +++++++ .../geometries/operation/Intersection3DTest.java | 264 ++++++++++ .../operation/triangulate/EarClippingTest.java | 42 ++ .../test/org/apache/sis/maths/MathsTest.java | 28 ++ 8 files changed, 1258 insertions(+), 41 deletions(-) diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/Contains.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/Contains.java index 293338f1bf..ec0c60ddfc 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/Contains.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/Contains.java @@ -18,56 +18,72 @@ package org.apache.sis.geometries.operation; import org.apache.sis.geometries.Point; import org.apache.sis.geometries.DataPointsType; +import org.apache.sis.geometries.GeometryCollection; import org.apache.sis.geometries.curve.LineString; +import org.apache.sis.geometries.operation.triangulate.EarClipping; +import org.apache.sis.geometries.solid.Polyhedron; import org.apache.sis.geometries.surface.Polygon; import org.apache.sis.maths.Array; import org.apache.sis.maths.Cursor; import org.apache.sis.maths.Maths; -import org.apache.sis.maths.Vectors; +import org.apache.sis.maths.ReadOnly; import org.apache.sis.maths.Vector; /** - * Constains 2D processors. + * Contains processors. * * @author Johann Sorel (Geomatys) */ public final class Contains { + /** + * The axes of the (x,y) plane, for the tests done in two dimensions. + */ + private static final int[] XY = {0, 1}; + private Contains(){} /** - * Test if point is within polygon using Winding Number algorithm. + * Test if point is within a ring using Winding Number algorithm, in the plane of the two given axes. + * The ring and the point can have more dimensions: for example a ring in a vertical plane can be tested + * in the plane given by {@link Maths#projectionAxes(double[])}. A point on the ring gets an arbitrary answer. * http://geomalgorithms.com/a03-_inclusion.html * http://en.wikipedia.org/wiki/Point_in_polygon * - * @param ring not null - * @param point not null + * @param ring positions of the closed ring, the last one at the location of the first one + * @param point the point to test + * @param axes indices of the two coordinates of the plane in which the test is done + * @return whether the point is inside the ring */ - private static boolean contains(Array ring, Vector<?> point) { + public static boolean contains(Array ring, ReadOnly.Vector<?> point, int[] axes) { + final long n = ring.getLength(); + if (n == 0) { + return false; + } + final int u = axes[0], v = axes[1]; + final double px = point.get(u), py = point.get(v); final Cursor cursor = ring.cursor(); - - int windingNumber = 0; - Vector<?> current; - Vector<?> previous; cursor.moveTo(0); - current = cursor.samples(); - previous = Vectors.create(current.getSampleSystem(), current.getDataType()); - final double pointY = point.get(1); - for (long i = 1, n = ring.getLength(); i < n; i++){ - previous.set(current); + Vector<?> current = cursor.samples(); + double x = current.get(u), y = current.get(v); + int windingNumber = 0; + for (long i = 1; i < n; i++){ + final double previousX = x, previousY = y; cursor.moveTo(i); current = cursor.samples(); + x = current.get(u); + y = current.get(v); - if (previous.get(1) <= pointY){ - if (current.get(1) > pointY){ - if (Maths.lineSide(previous, current, point) > 0){ + if (previousY <= py){ + if (y > py){ + if (Maths.lineSide(previousX, previousY, x, y, px, py) > 0){ windingNumber++; } } } else { - if (current.get(1) <= pointY){ - if (Maths.lineSide(previous, current, point) < 0){ + if (y <= py){ + if (Maths.lineSide(previousX, previousY, x, y, px, py) < 0){ windingNumber--; } } @@ -78,6 +94,87 @@ public final class Contains { return windingNumber != 0; } + /** + * Returns the generalized winding number of a surface made of triangles around a point: the sum of the solid + * angles of the triangles seen from the point, divided by 4π. The number is 1 inside a closed surface whose + * triangles are oriented outward (counterclockwise seen from outside), −1 if they are oriented inward, and 0 + * outside. The numbers of several surfaces add: a point inside the void of a solid is inside the outward + * surface of the solid and inside the inward surface of the void, which gives 0. The number of a surface + * which is not closed is between those values, and the number of a point on a surface is a fraction, + * for example ½ on a face. + * + * <p>The solid angle of each triangle is computed with the formula of Van Oosterom and Strackee. + * A triangle whose plane contains the point is ignored: seen from its plane, a triangle has no solid + * angle, unless the point is on it, which is the case of a point on the surface.</p> + * + * @param xyz the coordinates of the vertices, three per vertex. + * @param triangles indices of the three vertices of each triangle, one triangle after the other. + * @param point the point, of three coordinates. + * @return the winding number, close to an integer for a point which is not on a closed surface. + * + * @see <a href="https://doi.org/10.1145/2461912.2461916">Jacobson et al., Robust inside-outside segmentation using generalized winding numbers</a> + */ + public static double windingNumber(final double[] xyz, final int[] triangles, final double[] point) { + double sum = 0; + for (int i = 0; i + 2 < triangles.length; i += 3) { + final int a = triangles[i] * 3, b = triangles[i+1] * 3, c = triangles[i+2] * 3; + final double ax = xyz[a] - point[0], ay = xyz[a+1] - point[1], az = xyz[a+2] - point[2]; + final double bx = xyz[b] - point[0], by = xyz[b+1] - point[1], bz = xyz[b+2] - point[2]; + final double cx = xyz[c] - point[0], cy = xyz[c+1] - point[1], cz = xyz[c+2] - point[2]; + final double la = Math.sqrt(ax*ax + ay*ay + az*az); + final double lb = Math.sqrt(bx*bx + by*by + bz*bz); + final double lc = Math.sqrt(cx*cx + cy*cy + cz*cz); + final double det = ax * (by*cz - bz*cy) + ay * (bz*cx - bx*cz) + az * (bx*cy - by*cx); + if (det != 0) { + final double den = la*lb*lc + (ax*bx + ay*by + az*bz) * lc + (ax*cx + ay*cy + az*cz) * lb + (bx*cx + by*cy + bz*cz) * la; + sum += Math.atan2(det, den); // half of the solid angle. + } + } + return sum / (2 * Math.PI); + } + + /** + * Polyhedron contains Point test. The point is inside the polyhedron if it is inside the exterior shell and + * outside all the interior shells, as told by the {@linkplain #windingNumber winding number} of each shell. + * Each shell is tested alone, so the orientation of its faces does not matter as long as it is consistent. + * A point on the boundary of the polyhedron gets an arbitrary answer. + * + * @throws OperationException if the geometries do not have the same CRS, or are not three-dimensional, + * or if a face cannot be triangulated. + */ + public static boolean contains(Polyhedron polyhedron, Point candidate) throws OperationException { + ProcessorUtils.ensureSameCRS(polyhedron, candidate); + final double[] point = candidate.getPosition().toArrayDouble(); + if (point.length != 3) { + throw new OperationException("A polyhedron contains a point in 3 dimensions only."); + } + if (!isInside(polyhedron.getExteriorShell(), point)) { + return false; + } + for (final GeometryCollection<Polygon> shell : polyhedron.getInteriorShells()) { + if (isInside(shell, point)) { + //point is within a void + return false; + } + } + return true; + } + + /** + * Returns whether the given point is inside the given closed shell, whatever the orientation of its faces. + * The winding numbers of the faces are added, each face being split in triangles. + */ + private static boolean isInside(final GeometryCollection<Polygon> shell, final double[] point) { + final EarClipping triangulator = new EarClipping(); + double winding = 0; + for (int i = 0, n = shell.getNumGeometries(); i < n; i++) { + final EarClipping.Triangulation face = triangulator.triangulateIndexed(shell.getGeometryN(i)); + final double[] xyz = face.vertices().getAttributeArray(DataPointsType.ATT_POSITION).toArrayDouble(); + winding += windingNumber(xyz, face.triangles(), point); + } + return Math.abs(winding) > 0.5; + } + /** * Polygon contains Point test. */ @@ -86,7 +183,7 @@ public final class Contains { { //check exterior final Array coords = polygon.getExteriorRing().getDataPoints().getAttributeArray(DataPointsType.ATT_POSITION); - if (!contains(coords, candidate.getPosition())) { + if (!contains(coords, candidate.getPosition(), XY)) { //point is outside the exterior ring return false; } @@ -96,7 +193,7 @@ public final class Contains { for (int i = 0, n = polygon.getNumInteriorRing(); i < n; i++) { final LineString hole = polygon.getInteriorRingN(i); final Array coords = hole.getDataPoints().getAttributeArray(DataPointsType.ATT_POSITION); - if (contains(coords, candidate.getPosition())) { + if (contains(coords, candidate.getPosition(), XY)) { //point is within a hole return false; } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/Intersection.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/Intersection.java index db21ba0719..44526319ae 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/Intersection.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/Intersection.java @@ -18,6 +18,7 @@ package org.apache.sis.geometries.operation; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -39,10 +40,12 @@ import org.apache.sis.geometries.surface.PreparedTIN; import org.apache.sis.geometries.surface.Triangle; import org.apache.sis.maths.Array; import org.apache.sis.maths.Cursor; +import org.apache.sis.maths.Maths; import org.apache.sis.maths.Matrices; import org.apache.sis.maths.NDArrays; import org.apache.sis.maths.ReadOnly; import org.apache.sis.maths.Vector; +import org.apache.sis.maths.Vectors; import static org.apache.sis.maths.Maths.clamp; @@ -208,6 +211,552 @@ public final class Intersection { return intersection; } + /** + * Intersection of two segments in a plane. Each point of the intersection is given by its position on both + * segments, as a ratio from 0 at the start of the segment to 1 at its end. A ratio is exactly 0 or 1 when the + * point is an end of the segment. + * + * @param type whether the segments are disjoint, meet at one point, or overlap along their common line. + * @param startOnFirst ratio on the first segment of the single point, or of the start of the overlap, NaN if none. + * @param endOnFirst ratio on the first segment of the end of the overlap, greater than {@code startOnFirst}, + * or {@code startOnFirst} for a single point. + * @param startOnSecond ratio on the second segment of the point at {@code startOnFirst}. + * @param endOnSecond ratio on the second segment of the point at {@code endOnFirst}. + * + * @see #segmentSegment2D(double[], double[], double[], double[], int[]) + */ + public record SegmentSegment(Type type, double startOnFirst, double endOnFirst, double startOnSecond, double endOnSecond) { + /** + * Kinds of intersection of two segments. + */ + public enum Type { + /** The segments do not intersect. */ + NONE, + /** The segments cross or touch at a single point. */ + POINT, + /** The segments are on the same line and share a part of it. */ + OVERLAP + } + + /** + * The intersection of disjoint segments. + */ + public static final SegmentSegment NONE = new SegmentSegment(Type.NONE, Double.NaN, Double.NaN, Double.NaN, Double.NaN); + + /** + * Returns the intersection made of a single point, at the given ratios on the two segments. + */ + static SegmentSegment point(final double onFirst, final double onSecond) { + return new SegmentSegment(Type.POINT, onFirst, onFirst, onSecond, onSecond); + } + } + + /** + * Point where a segment touches a triangle, located on both. Edge <var>i</var> of the triangle goes from + * its corner <var>i</var> to its corner (<var>i</var>+1) modulo 3. + * + * @param type where the point is on the triangle, or why there is no point. + * @param feature index of the edge or of the corner of the triangle on which the point is, or -1. + * @param ratio position of the point on the segment, from 0 at its start to 1 at its end, + * exactly 0 or 1 if the point is an end of the segment. NaN if there is no point. + * + * @see #segmentTriangle(double[], double[], double[], double[], double[], double) + * @see #segmentInTriangle2D(double[], double[], double[], double[], double[], int[]) + */ + public record SegmentTriangle(Type type, int feature, double ratio) { + /** + * Where a segment touches a triangle. + */ + public enum Type { + /** The segment does not touch the triangle. */ + NONE, + /** The segment lies in the plane of the triangle, and shall be intersected with it in that plane. */ + COPLANAR, + /** The segment touches the triangle inside it. */ + INTERIOR, + /** The segment touches the triangle on the edge given by the feature index. */ + EDGE, + /** The segment touches the triangle on the corner given by the feature index. */ + VERTEX + } + + /** + * A segment which does not touch the triangle. + */ + public static final SegmentTriangle NONE = new SegmentTriangle(Type.NONE, -1, Double.NaN); + + /** + * A segment in the plane of the triangle. + */ + public static final SegmentTriangle COPLANAR = new SegmentTriangle(Type.COPLANAR, -1, Double.NaN); + + /** + * Returns the point inside the triangle, on an edge or on a corner, from the number of edges on which + * the point is. This is used after testing the side of the point relative to the three edges. + * + * @param zeros number of edges on which the point is. + * @param zeroEdge index of an edge on which the point is. + * @param otherEdge index of an edge on which the point is not. + * @param ratio position of the point on the segment. + * @return the point, or {@code null} if it is on the three edges, which happens with a degenerate triangle. + */ + static SegmentTriangle locate(final int zeros, final int zeroEdge, final int otherEdge, final double ratio) { + switch (zeros) { + case 0: return new SegmentTriangle(Type.INTERIOR, -1, ratio); + case 1: return new SegmentTriangle(Type.EDGE, zeroEdge, ratio); + case 2: return new SegmentTriangle(Type.VERTEX, (otherEdge + 2) % 3, ratio); // corner shared by the two other edges. + default: return null; + } + } + } + + /** + * End of the intersection of two triangles: a point where an edge of one triangle touches the other triangle. + * + * @param edgeOfFirst whether the edge belongs to the first triangle, the point being then on the second triangle. + * @param edge index of the edge, which goes from the corner of same index to the next corner. + * @param contact where the edge touches the other triangle, with the position of the point on the edge. + * @param position coordinates of the point, interpolated on the edge. + * + * @see #triangleTriangle(double[], double[], double[], double[], double[], double[], double) + */ + public record TriangleEdgeContact(boolean edgeOfFirst, int edge, SegmentTriangle contact, double[] position) { + } + + /** + * Intersection of two triangles: nothing, a point or a segment, unless the triangles are in the same plane. + * + * @param type kind of intersection. + * @param start first end of the intersection, or {@code null} if the triangles are disjoint or coplanar. + * @param end second end of the intersection, the same as {@code start} for a single point. + * + * @see #triangleTriangle(double[], double[], double[], double[], double[], double[], double) + */ + public record TriangleTriangle(Type type, TriangleEdgeContact start, TriangleEdgeContact end) { + /** + * Kinds of intersection of two triangles. + */ + public enum Type { + /** The triangles do not intersect. */ + DISJOINT, + /** The triangles touch at a single point. */ + POINT, + /** The triangles cross or touch along a segment. */ + SEGMENT, + /** The triangles are in the same plane, and shall be intersected in that plane. */ + COPLANAR + } + + /** Intersection of disjoint triangles. */ + public static final TriangleTriangle DISJOINT = new TriangleTriangle(Type.DISJOINT, null, null); + + /** Intersection of triangles in the same plane. */ + public static final TriangleTriangle COPLANAR = new TriangleTriangle(Type.COPLANAR, null, null); + } + + /** + * Computes the intersection of two segments in the plane of the two given axes. The side tests are robust, + * so whether the segments intersect, and whether a point of the intersection is an end of a segment, are exact. + * Only the ratios of the points inside the segments are approximations. + * + * @param a0 start of the first segment. + * @param a1 end of the first segment. + * @param b0 start of the second segment. + * @param b1 end of the second segment. + * @param axes indices of the two coordinates of the plane, for example from {@link Maths#projectionAxes(double[])}. + * @return the intersection of the two segments. + */ + public static SegmentSegment segmentSegment2D(double[] a0, double[] a1, double[] b0, double[] b1, int[] axes) { + final int u = axes[0], v = axes[1]; + //sides of the ends of each segment relative to the line of the other segment. + final double d0 = side(b0, b1, a0, u, v), d1 = side(b0, b1, a1, u, v); + final double e0 = side(a0, a1, b0, u, v), e1 = side(a0, a1, b1, u, v); + if (sameSide(d0, d1) || sameSide(e0, e1)) { + return SegmentSegment.NONE; + } + if ((d0 == 0 && d1 == 0) || (e0 == 0 && e1 == 0)) { + return collinear(a0, a1, b0, b1, u, v); + } + return SegmentSegment.point(ratio(d0, d1), ratio(e0, e1)); + } + + /** + * Computes the intersection of two segments on the same line. The positions are compared along the axis + * where the segments are the longest, so that a line parallel to one of the axes is not degenerate. + */ + private static SegmentSegment collinear(double[] a0, double[] a1, double[] b0, double[] b1, int u, int v) { + final int k = (Math.abs(a1[u] - a0[u]) + Math.abs(b1[u] - b0[u]) >= Math.abs(a1[v] - a0[v]) + Math.abs(b1[v] - b0[v])) ? u : v; + final double lower = Math.max(Math.min(a0[k], a1[k]), Math.min(b0[k], b1[k])); + final double upper = Math.min(Math.max(a0[k], a1[k]), Math.max(b0[k], b1[k])); + if (!(lower <= upper)) { + return SegmentSegment.NONE; + } + double startOnFirst = along(a0[k], a1[k], lower), startOnSecond = along(b0[k], b1[k], lower); + if (lower == upper) { + return SegmentSegment.point(startOnFirst, startOnSecond); + } + double endOnFirst = along(a0[k], a1[k], upper), endOnSecond = along(b0[k], b1[k], upper); + if (startOnFirst > endOnFirst) { + //the first segment goes toward decreasing values along the axis. + double t = startOnFirst; startOnFirst = endOnFirst; endOnFirst = t; + t = startOnSecond; startOnSecond = endOnSecond; endOnSecond = t; + } + return new SegmentSegment(SegmentSegment.Type.OVERLAP, startOnFirst, endOnFirst, startOnSecond, endOnSecond); + } + + /** + * Returns the position of a value between the values at the start and at the end of a segment along an axis, + * exactly 0 or 1 at the ends. + */ + private static double along(final double start, final double end, final double value) { + if (value == start) return 0; + if (value == end) return 1; + return (value - start) / (end - start); + } + + /** + * Returns the position of the point where a segment crosses a line or a plane, from the signed distances of + * the segment ends to that line or plane, or any values proportional to those distances. The position is + * exactly 0 or 1 if an end is on the line or plane. + */ + private static double ratio(final double start, final double end) { + if (start == 0) return 0; + if (end == 0) return 1; + return clamp(start / (start - end), 0, 1); + } + + /** + * Returns whether two values are both strictly positive or both strictly negative. + */ + private static boolean sameSide(final double s1, final double s2) { + return (s1 > 0 && s2 > 0) || (s1 < 0 && s2 < 0); + } + + /** + * Returns whether one value is strictly positive and the other one strictly negative. + */ + private static boolean opposite(final double s1, final double s2) { + return (s1 > 0 && s2 < 0) || (s1 < 0 && s2 > 0); + } + + /** + * Returns the robust side of point r relative to the line from p to q, in the plane of the axes u and v. + * Positive on the left, negative on the right, zero on the line. + */ + private static double side(final double[] p, final double[] q, final double[] r, final int u, final int v) { + return Maths.lineSide(p[u], p[v], q[u], q[v], r[u], r[v]); + } + + /** + * Returns the normal (b−a)×(c−a) of a triangle, whose length is twice the area of the triangle. + */ + private static double[] normal(final double[] a, final double[] b, final double[] c) { + final double ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2]; + final double vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2]; + return new double[] {uy*vz - uz*vy, uz*vx - ux*vz, ux*vy - uy*vx}; + } + + /** + * Computes where a segment touches a triangle, in three dimensions. An end of the segment within the tolerance + * of the plane of the triangle is on that plane, as decided by {@link Maths#planeSide Maths.planeSide(…)}. + * If both ends are on the plane, the segment is {@linkplain SegmentTriangle.Type#COPLANAR coplanar} and its + * intersection with the triangle is given by {@link #segmentInTriangle2D segmentInTriangle2D(…)}. + * + * <p>When the segment crosses the plane, where it crosses the triangle is decided by the robust sides of + * its line relative to the three edges, so that a segment crossing an edge or a corner is found exactly on it. + * Only the ratio of a point inside the segment is an approximation. When an end of the segment is on the plane, + * that end is located in the triangle with robust side tests in the plane.</p> + * + * @param s0 start of the segment. + * @param s1 end of the segment. + * @param a first corner of the triangle. + * @param b second corner of the triangle. + * @param c third corner of the triangle. + * @param tolerance largest distance to the plane of the triangle for considering an end of the segment on it. + * @return where the segment touches the triangle. + */ + public static SegmentTriangle segmentTriangle(double[] s0, double[] s1, double[] a, double[] b, double[] c, double tolerance) { + final int side0 = Maths.planeSide(a, b, c, s0, tolerance); + final int side1 = Maths.planeSide(a, b, c, s1, tolerance); + if (side0 == 0 && side1 == 0) { + return SegmentTriangle.COPLANAR; + } + if (side0 == side1) { + return SegmentTriangle.NONE; // both ends strictly on the same side. + } + final double[][] corners = {a, b, c}; + final double[] normal = normal(a, b, c); + if (side0 == 0 || side1 == 0) { + final int[] axes = Maths.projectionAxes(normal); + return (side0 == 0) ? locate2D(s0, corners, axes[0], axes[1], 0) + : locate2D(s1, corners, axes[0], axes[1], 1); + } + /* + * The segment crosses the plane between its ends. The sign of the tetrahedron made by the segment and + * an edge tells on which side of that edge the line of the segment passes. The line passes inside the + * triangle if the three signs are the same, and on an edge or a corner if one or two of them are zero. + */ + int positive = 0, negative = 0, zeros = 0, zeroEdge = -1, otherEdge = -1; + for (int i = 0; i < 3; i++) { + final double[] p = corners[i], q = corners[(i + 1) % 3]; + final double sign = ShewchukPredicates.orient3d(s0[0], s0[1], s0[2], s1[0], s1[1], s1[2], + p[0], p[1], p[2], q[0], q[1], q[2]); + if (sign > 0) {positive++; otherEdge = i;} + else if (sign < 0) {negative++; otherEdge = i;} + else {zeros++; zeroEdge = i;} + } + if (positive != 0 && negative != 0) { + return SegmentTriangle.NONE; + } + final double d0 = normal[0] * (s0[0] - a[0]) + normal[1] * (s0[1] - a[1]) + normal[2] * (s0[2] - a[2]); + final double d1 = normal[0] * (s1[0] - a[0]) + normal[1] * (s1[1] - a[1]) + normal[2] * (s1[2] - a[2]); + final SegmentTriangle point = SegmentTriangle.locate(zeros, zeroEdge, otherEdge, ratio(d0, d1)); + return (point != null) ? point : SegmentTriangle.COPLANAR; + } + + /** + * Locates a point of the plane of a triangle relative to that triangle, with robust side tests in the plane + * of the two given axes. + * + * @param p the point to locate. + * @param corners the three corners of the triangle. + * @param ratio position of the point on the segment for which the point is located. + * @return where the point is on the triangle, or {@link SegmentTriangle#NONE} if outside or if the triangle is degenerate. + */ + private static SegmentTriangle locate2D(final double[] p, final double[][] corners, final int u, final int v, final double ratio) { + final double orientation = Math.signum(side(corners[0], corners[1], corners[2], u, v)); + if (orientation == 0) { + return SegmentTriangle.NONE; + } + int zeros = 0, zeroEdge = -1, otherEdge = -1; + for (int i = 0; i < 3; i++) { + final double s = orientation * side(corners[i], corners[(i + 1) % 3], p, u, v); + if (s < 0) { + return SegmentTriangle.NONE; + } + if (s == 0) {zeros++; zeroEdge = i;} + else {otherEdge = i;} + } + final SegmentTriangle point = SegmentTriangle.locate(zeros, zeroEdge, otherEdge, ratio); + return (point != null) ? point : SegmentTriangle.NONE; + } + + /** + * Computes the part of a segment inside a triangle, both in the same plane, with robust side tests in the plane + * of the two given axes. The result gives the two ends of that part, each with its position on the segment and + * its location on the triangle: an end is an end of the segment inside the triangle, or a point where the + * segment crosses an edge or passes through a corner. Whether a point is on an edge or a corner, or is an end + * of the segment, is exact. Only the ratios of the points inside the segment are approximations. + * + * @param s0 start of the segment. + * @param s1 end of the segment. + * @param a first corner of the triangle. + * @param b second corner of the triangle. + * @param c third corner of the triangle. + * @param axes indices of the two coordinates of the plane, for example from {@link Maths#projectionAxes(double[])}. + * @return the ends of the part of the segment inside the triangle by increasing ratio: none if the segment + * does not touch the triangle, one if it touches it at a single point, two otherwise. + * Their types are {@code INTERIOR}, {@code EDGE} or {@code VERTEX}. + */ + public static List<SegmentTriangle> segmentInTriangle2D(double[] s0, double[] s1, double[] a, double[] b, double[] c, int[] axes) { + final int u = axes[0], v = axes[1]; + final double[][] corners = {a, b, c}; + if (side(a, b, c, u, v) == 0) { + return List.of(); // a degenerate triangle has no interior. + } + final List<SegmentTriangle> points = new ArrayList<>(4); + final SegmentTriangle start = locate2D(s0, corners, u, v, 0); + if (start != SegmentTriangle.NONE) { + points.add(start); + } + final boolean isPoint = (s0[u] == s1[u] && s0[v] == s1[v]); + if (!isPoint) { + final SegmentTriangle end = locate2D(s1, corners, u, v, 1); + if (end != SegmentTriangle.NONE) { + points.add(end); + } + /* + * The corners on the line of the segment, between its ends. + * The ends of the segment have been located above. + */ + final double dx = s1[u] - s0[u], dy = s1[v] - s0[v]; + final double[] sides = new double[3]; + for (int j = 0; j < 3; j++) { + final double[] p = corners[j]; + sides[j] = side(s0, s1, p, u, v); + if (sides[j] == 0 && !(p[u] == s0[u] && p[v] == s0[v]) && !(p[u] == s1[u] && p[v] == s1[v])) { + final double t = ((p[u] - s0[u]) * dx + (p[v] - s0[v]) * dy) / (dx*dx + dy*dy); + if (t > 0 && t < 1) { + points.add(new SegmentTriangle(SegmentTriangle.Type.VERTEX, j, t)); + } + } + } + /* + * The edges crossed between their corners, by the segment between its ends. + */ + for (int i = 0; i < 3; i++) { + final int j = (i + 1) % 3; + if (opposite(sides[i], sides[j])) { + final double d0 = side(corners[i], corners[j], s0, u, v); + final double d1 = side(corners[i], corners[j], s1, u, v); + if (opposite(d0, d1)) { + points.add(new SegmentTriangle(SegmentTriangle.Type.EDGE, i, ratio(d0, d1))); + } + } + } + } + switch (points.size()) { + case 0: return List.of(); + case 1: return List.of(points.get(0)); + default: { + //the triangle is convex, so the part inside it goes from the first point to the last one. + points.sort(Comparator.comparingDouble(SegmentTriangle::ratio)); + return List.of(points.get(0), points.get(points.size() - 1)); + } + } + } + + /** + * Computes the intersection of two triangles in three dimensions. A corner of a triangle within the tolerance + * of the plane of the other triangle is on that plane, as decided by {@link Maths#planeSide Maths.planeSide(…)}. + * If the three corners of a triangle are on the plane of the other one, the triangles are + * {@linkplain TriangleTriangle.Type#COPLANAR coplanar} and shall be intersected in that plane, + * for example with {@link #segmentInTriangle2D segmentInTriangle2D(…)}. + * + * <p>Otherwise the intersection is on the line where the two planes meet, and each of its ends is a point + * where an edge of a triangle touches the other triangle, as given by {@link #segmentTriangle segmentTriangle(…)}. + * An edge in the plane of the other triangle is intersected with it in that plane.</p> + * + * @param a0 first corner of the first triangle. + * @param a1 second corner of the first triangle. + * @param a2 third corner of the first triangle. + * @param b0 first corner of the second triangle. + * @param b1 second corner of the second triangle. + * @param b2 third corner of the second triangle. + * @param tolerance largest distance to the plane of a triangle for considering a point on it. + * @return the intersection of the two triangles. + */ + public static TriangleTriangle triangleTriangle(double[] a0, double[] a1, double[] a2, + double[] b0, double[] b1, double[] b2, double tolerance) + { + final double[][] first = {a0, a1, a2}, second = {b0, b1, b2}; + final int[] sidesOfSecond = sides(first, second, tolerance); + final int[] sidesOfFirst = sides(second, first, tolerance); + if (allOnSameSide(sidesOfSecond) || allOnSameSide(sidesOfFirst)) { + return TriangleTriangle.DISJOINT; + } + if (Arrays.equals(sidesOfSecond, new int[3]) || Arrays.equals(sidesOfFirst, new int[3])) { + return TriangleTriangle.COPLANAR; + } + final List<TriangleEdgeContact> contacts = new ArrayList<>(); + addEdgeContacts(first, second, true, tolerance, contacts); + addEdgeContacts(second, first, false, tolerance, contacts); + if (contacts.isEmpty()) { + return TriangleTriangle.DISJOINT; + } + /* + * All points are on the line where the two planes meet. + * The ends of the intersection are the two extreme points along that line. + */ + final double[] direction = Vectors.cross(normal(a0, a1, a2), normal(b0, b1, b2)); + TriangleEdgeContact start = null, end = null; + double min = Double.POSITIVE_INFINITY, max = Double.NEGATIVE_INFINITY; + for (final TriangleEdgeContact contact : contacts) { + final double t = Vectors.dot(contact.position(), direction); + if (t < min) {min = t; start = contact;} + if (t > max) {max = t; end = contact;} + } + if (start == end || locations(start).equals(locations(end)) || Arrays.equals(start.position(), end.position())) { + return new TriangleTriangle(TriangleTriangle.Type.POINT, start, start); + } + return new TriangleTriangle(TriangleTriangle.Type.SEGMENT, start, end); + } + + /** + * Returns the sides of the corners of a triangle relative to the plane of another triangle. + */ + private static int[] sides(final double[][] plane, final double[][] triangle, final double tolerance) { + final int[] sides = new int[3]; + for (int i = 0; i < 3; i++) { + sides[i] = Maths.planeSide(plane[0], plane[1], plane[2], triangle[i], tolerance); + } + return sides; + } + + /** + * Returns whether the three sides are all strictly positive or all strictly negative. + */ + private static boolean allOnSameSide(final int[] sides) { + return sameSide(sides[0], sides[1]) && sameSide(sides[1], sides[2]); + } + + /** + * Adds the points where the edges of a triangle touch another triangle. + * + * @param edges the triangle whose edges are tested. + * @param other the triangle touched by the edges. + * @param edgeOfFirst whether the triangle of the edges is the first one given to {@code triangleTriangle(…)}. + * @param tolerance largest distance to the plane of a triangle for considering a point on it. + * @param contacts where to add the points. + */ + private static void addEdgeContacts(final double[][] edges, final double[][] other, final boolean edgeOfFirst, + final double tolerance, final List<TriangleEdgeContact> contacts) + { + for (int i = 0; i < 3; i++) { + final double[] p = edges[i], q = edges[(i + 1) % 3]; + final SegmentTriangle contact = segmentTriangle(p, q, other[0], other[1], other[2], tolerance); + switch (contact.type()) { + case NONE: break; + case COPLANAR: { + final int[] axes = Maths.projectionAxes(normal(other[0], other[1], other[2])); + for (final SegmentTriangle point : segmentInTriangle2D(p, q, other[0], other[1], other[2], axes)) { + contacts.add(new TriangleEdgeContact(edgeOfFirst, i, point, interpolate(p, q, point.ratio()))); + } + break; + } + default: { + contacts.add(new TriangleEdgeContact(edgeOfFirst, i, contact, interpolate(p, q, contact.ratio()))); + break; + } + } + } + } + + /** + * Returns the three first coordinates of the point at the given ratio between two points, + * exactly one of them at ratio 0 or 1. + */ + private static double[] interpolate(final double[] p, final double[] q, final double ratio) { + if (ratio == 0) return Arrays.copyOf(p, 3); + if (ratio == 1) return Arrays.copyOf(q, 3); + final double[] point = new double[3]; + for (int k = 0; k < 3; k++) { + point[k] = p[k] + ratio * (q[k] - p[k]); + } + return point; + } + + /** + * Where a point is on a triangle: inside, or on the edge or the corner of the given index. + */ + private record Location(SegmentTriangle.Type type, int index) { + } + + /** + * Returns where the point of the given contact is on the first triangle and on the second triangle. + * Two contacts found from different edges are the same point if they are at the same locations. + */ + private static List<Location> locations(final TriangleEdgeContact contact) { + final double ratio = contact.contact().ratio(); + final int edge = contact.edge(); + final Location onEdge; + if (ratio == 0) onEdge = new Location(SegmentTriangle.Type.VERTEX, edge); + else if (ratio == 1) onEdge = new Location(SegmentTriangle.Type.VERTEX, (edge + 1) % 3); + else onEdge = new Location(SegmentTriangle.Type.EDGE, edge); + final Location onOther = new Location(contact.contact().type(), contact.contact().feature()); + return contact.edgeOfFirst() ? List.of(onEdge, onOther) : List.of(onOther, onEdge); + } + /** * Intersection operations on BSpline curves and surfaces. */ diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/triangulate/EarClipping.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/triangulate/EarClipping.java index 00ed3a038d..d5ace07d12 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/triangulate/EarClipping.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/triangulate/EarClipping.java @@ -30,7 +30,7 @@ import org.apache.sis.geometries.surface.Polygon; import org.apache.sis.maths.Array; import org.apache.sis.maths.Maths; import org.apache.sis.maths.NDArrays; -import org.apache.sis.maths.Vector; +import org.apache.sis.maths.ReadOnly; import org.apache.sis.util.ArraysExt; @@ -82,15 +82,16 @@ public class EarClipping { private DataPoints source; /** - * Positions of the polygon, with their coordinates in the order used by the triangulation. - * The triangulation works on the two first coordinates, in the plane where the polygon has its largest area. + * Positions of the vertices, with their coordinates in the order used by the triangulation. + * The triangulation works on the two first coordinates. For a polygon, those positions are + * the ones of {@link #source} in the plane where the polygon has its largest area. */ - private Vector<?>[] working; + private ReadOnly.Vector<?>[] working; /** - * Indices in {@link #source} of the vertices of the ring joining the holes to the exterior ring, without - * repeated consecutive positions and without the closing position. The ring has the orientation of the - * exterior ring. + * Indices in {@link #working} of the vertices of the ring to triangulate, without repeated consecutive + * positions and without the closing position. For a polygon, this is the ring joining the holes to the + * exterior ring, which has the orientation of the exterior ring. */ private int[] ring; @@ -112,7 +113,7 @@ public class EarClipping { private Angle[] angles; /** - * Indices in {@link #source} of the three vertices of each triangle found so far. + * Indices in {@link #working} of the three vertices of each triangle found so far. */ private final List<int[]> triangles = new ArrayList<>(); @@ -127,13 +128,39 @@ public class EarClipping { public Triangulation triangulateIndexed(Polygon geometry) throws OperationException { source = geometry.getDataPoints(); final int[] axes = workingAxes(geometry.getExteriorRing().getDataPoints().getAttributeArray(DataPointsType.ATT_POSITION)); - working = new Vector<?>[source.size()]; + working = new ReadOnly.Vector<?>[source.size()]; Arrays.setAll(working, (i) -> source.getPosition(i).swizzle(axes)); triangles.clear(); run(HoleToExterior.ring(geometry)); return result(); } + /** + * Triangulates a simple ring given by the indices of its vertices in an array of positions. + * The triangulation works on the two first coordinates of the positions: a ring in three dimensions + * shall be projected first, for example with {@link ReadOnly.Vector#swizzle(int...)} and the axes given by + * {@link Maths#projectionAxes(double[])}. Consecutive vertices at the same position are a single vertex. + * + * @param positions positions of the vertices, of which the ring may use only some. + * @param indices indices in {@code positions} of the vertices of the ring, in order. The ring can be closed, + * with its last vertex at the location of the first one, or not. + * @return indices in {@code positions} of the three corners of each triangle, one triangle after the other. + * The triangles have the orientation of the ring. + * @throws OperationException if the ring cannot be triangulated, for example because it intersects itself. + */ + public int[] triangulate(final ReadOnly.Vector<?>[] positions, int[] indices) throws OperationException { + working = positions; + triangles.clear(); + final int n = indices.length; + if (n != 0 && !positions[indices[0]].equals(positions[indices[n - 1]])) { + //the triangulation expects a closed ring. + indices = Arrays.copyOf(indices, n + 1); + indices[n] = indices[0]; + } + run(indices); + return triangles.stream().flatMapToInt(Arrays::stream).toArray(); + } + /** * Triangulates the given polygon into an indexed mesh holding all the attributes of the polygon. * @@ -152,7 +179,7 @@ public class EarClipping { /** * Returns the order of the coordinates to use for triangulating a polygon with the given positions, - * to be given to {@link Vector#swizzle(int...)}. The triangulation works on the two first coordinates + * to be given to {@link ReadOnly.Vector#swizzle(int...)}. The triangulation works on the two first coordinates * of that order. With three or more dimensions, the three first axes are in the {@linkplain Maths#projectionAxes * projection order} of the {@linkplain Maths#calculatePolygonNormal(Array) normal of the polygon}: the axis along * which the normal is the largest goes after the two axes of the projection plane, so that a vertical polygon is @@ -189,7 +216,7 @@ public class EarClipping { * triangle with its two neighbors contains no other vertex. Cutting it removes the vertex from the * ring, which stays a valid polygon, until only one triangle is left. * - * @param joined indices in {@link #source} of the ring positions, the last one at the location of the first one. + * @param joined indices in {@link #working} of the ring positions, the last one at the location of the first one. * @throws OperationException if the vertices left without ear still enclose an area. */ private void run(final int[] joined) throws OperationException { @@ -239,19 +266,19 @@ public class EarClipping { * @param start a vertex which is not cut yet. */ private boolean isDegenerate(final int start) { - final Vector<?> origin = position(start); + final ReadOnly.Vector<?> origin = position(start); final double ox = origin.get(0), oy = origin.get(1); double area = 0; // twice the signed area. int vertex = start; do { - final Vector<?> a = position(vertex), b = position(next[vertex]); + final ReadOnly.Vector<?> a = position(vertex), b = position(next[vertex]); area += (a.get(0) - ox) * (b.get(1) - oy) - (b.get(0) - ox) * (a.get(1) - oy); vertex = next[vertex]; } while (vertex != start); double xmin = Double.POSITIVE_INFINITY, xmax = Double.NEGATIVE_INFINITY; double ymin = Double.POSITIVE_INFINITY, ymax = Double.NEGATIVE_INFINITY; for (final int index : ring) { - final Vector<?> p = working[index]; + final ReadOnly.Vector<?> p = working[index]; xmin = Math.min(xmin, p.get(0)); xmax = Math.max(xmax, p.get(0)); ymin = Math.min(ymin, p.get(1)); ymax = Math.max(ymax, p.get(1)); } @@ -262,7 +289,7 @@ public class EarClipping { /** * Returns the working position of the given vertex of the ring. */ - private Vector<?> position(final int vertex) { + private ReadOnly.Vector<?> position(final int vertex) { return working[ring[vertex]]; } @@ -283,11 +310,11 @@ public class EarClipping { * so a copy located on a corner of the triangle does not prevent the ear. */ private boolean isEar(final int vertex) { - final Vector<?> t1 = position(previous[vertex]); - final Vector<?> t2 = position(vertex); - final Vector<?> t3 = position(next[vertex]); + final ReadOnly.Vector<?> t1 = position(previous[vertex]); + final ReadOnly.Vector<?> t2 = position(vertex); + final ReadOnly.Vector<?> t3 = position(next[vertex]); for (int i = next[next[vertex]]; i != previous[vertex]; i = next[i]) { - final Vector<?> p = position(i); + final ReadOnly.Vector<?> p = position(i); if (!isConvex(i) && !p.equals(t1) && !p.equals(t2) && !p.equals(t3) && Maths.inTriangle(t1, t2, t3, p)) { return false; } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Maths.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Maths.java index 185505bc55..799a9bed0e 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Maths.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/maths/Maths.java @@ -167,6 +167,34 @@ public final class Maths { return (normal[drop] < 0) ? new int[] {v, u, drop} : new int[] {u, v, drop}; } + /** + * Returns the side of a point relative to the plane of a triangle, with a tolerance. The point is on the plane + * if its distance to the plane is not greater than the tolerance. Otherwise its side is given by the robust + * {@link ShewchukPredicates#orient3d orient3d} predicate, so that the side is right even very close to the plane + * when the tolerance is zero. Only the three first coordinates are used. + * + * @param a first corner of the triangle + * @param b second corner of the triangle + * @param c third corner of the triangle + * @param p point to test + * @param tolerance largest distance to the plane for considering the point on it, zero for an exact test + * @return +1 if the point is on the side toward which the normal (b−a)×(c−a) points, −1 if it is on the + * other side, 0 if it is on the plane. A degenerate triangle has no plane: every point is on it. + */ + public static int planeSide(double[] a, double[] b, double[] c, double[] p, double tolerance) { + final double ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2]; + final double vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2]; + final double nx = uy*vz - uz*vy, ny = uz*vx - ux*vz, nz = ux*vy - uy*vx; + //the distance to the plane multiplied by the length of the normal. + final double distance = nx * (p[0] - a[0]) + ny * (p[1] - a[1]) + nz * (p[2] - a[2]); + if (Math.abs(distance) <= tolerance * Math.sqrt(nx*nx + ny*ny + nz*nz)) { + return 0; + } + //orient3d is positive below the plane, on the side opposite to the normal. + return (int) -Math.signum(ShewchukPredicates.orient3d(a[0], a[1], a[2], b[0], b[1], b[2], + c[0], c[1], c[2], p[0], p[1], p[2])); + } + public static double[] subtract(float[] A, float[] B){ return new double[] { A[0] - (double) B[0], diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/Contains3DTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/Contains3DTest.java new file mode 100644 index 0000000000..9e703703ab --- /dev/null +++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/Contains3DTest.java @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.sis.geometries.operation; + +import java.util.Arrays; +import java.util.List; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.apache.sis.geometries.Geometries; +import org.apache.sis.geometries.GeometryCollection; +import org.apache.sis.geometries.GeometryFactory; +import org.apache.sis.geometries.curve.LinearRing; +import org.apache.sis.geometries.solid.Polyhedron; +import org.apache.sis.geometries.solid.polyhedron.Icosahedron; +import org.apache.sis.geometries.surface.Polygon; +import org.apache.sis.maths.Array; +import org.apache.sis.maths.Maths; +import org.apache.sis.maths.NDArrays; +import org.apache.sis.maths.Vectors; +import org.apache.sis.referencing.CommonCRS; + +// Test dependencies +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + + +/** + * Tests the three-dimensional and planar tests of {@link Contains}: winding number of a surface, + * polyhedron containing a point, and ring containing a point in any plane. + * + * @author Johann Sorel (Geomatys) + */ +public class Contains3DTest { + + private static final double EPS = 1E-9; + + private static final CoordinateReferenceSystem CRS3D = Geometries.getUndefinedCRS(3); + + /** + * Indices of the corners of the twelve triangles of a cube, oriented outward. The corner of index + * <var>x</var> + 2<var>y</var> + 4<var>z</var> is at the minimum (0) or maximum (1) of each axis. + */ + private static final int[] CUBE = { + 0,2,3, 0,3,1, // z = 0 + 4,5,7, 4,7,6, // z = 1 + 0,1,5, 0,5,4, // y = 0 + 2,6,7, 2,7,3, // y = 1 + 0,4,6, 0,6,2, // x = 0 + 1,3,7, 1,7,5 // x = 1 + }; + + /** + * Returns the coordinates of the eight corners of the cube between the given minimum and maximum on all axes. + */ + private static double[] corners(final double min, final double max) { + final double[] xyz = new double[8 * 3]; + for (int i = 0; i < 8; i++) { + xyz[i*3 ] = ((i & 1) == 0) ? min : max; + xyz[i*3+1] = ((i & 2) == 0) ? min : max; + xyz[i*3+2] = ((i & 4) == 0) ? min : max; + } + return xyz; + } + + /** + * Returns the triangles of the cube, reversed so that they are oriented inward, with indices shifted by the given offset. + */ + private static int[] inward(final int offset) { + final int[] triangles = new int[CUBE.length]; + for (int i = 0; i < CUBE.length; i += 3) { + triangles[i ] = CUBE[i ] + offset; + triangles[i+1] = CUBE[i+2] + offset; + triangles[i+2] = CUBE[i+1] + offset; + } + return triangles; + } + + /** + * The winding number is 1 inside a closed surface oriented outward, −1 if oriented inward, 0 outside, + * and ½ on a face. + */ + @Test + public void testWindingNumber() { + final double[] xyz = corners(0, 1); + assertEquals( 1, Contains.windingNumber(xyz, CUBE, new double[] {0.5, 0.5, 0.5}), EPS); + assertEquals( 1, Contains.windingNumber(xyz, CUBE, new double[] {0.9, 0.1, 0.3}), EPS); + assertEquals( 0, Contains.windingNumber(xyz, CUBE, new double[] {2.0, 0.5, 0.5}), EPS); + assertEquals( 0, Contains.windingNumber(xyz, CUBE, new double[] {-3, 7, 0.2}), EPS); + assertEquals(-1, Contains.windingNumber(xyz, inward(0), new double[] {0.5, 0.5, 0.5}), EPS); + assertEquals( 0.5, Contains.windingNumber(xyz, CUBE, new double[] {0.3, 0.6, 0.0}), EPS, "On a face."); + } + + /** + * The winding numbers of an outer surface and of the inward surface of its void add, + * giving 0 inside the void. + */ + @Test + public void testWindingNumberWithVoid() { + final double[] outer = corners(0, 4), inner = corners(1, 3); + final double[] xyz = Arrays.copyOf(outer, outer.length + inner.length); + System.arraycopy(inner, 0, xyz, outer.length, inner.length); + final int[] triangles = Arrays.copyOf(CUBE, CUBE.length * 2); + System.arraycopy(inward(8), 0, triangles, CUBE.length, CUBE.length); + assertEquals(1, Contains.windingNumber(xyz, triangles, new double[] {0.5, 0.5, 0.5}), EPS, "In the solid."); + assertEquals(0, Contains.windingNumber(xyz, triangles, new double[] {2, 2, 2 }), EPS, "In the void."); + assertEquals(0, Contains.windingNumber(xyz, triangles, new double[] {5, 2, 2 }), EPS, "Outside."); + } + + /** + * A polyhedron contains the points inside its exterior shell and outside its interior shells. + * The interior shell is oriented as the exterior one, which shall not matter. + */ + @Test + public void testPolyhedron() { + final GeometryFactory f = GeometryFactory.DEFAULT; + final Polyhedron cube = f.createPolyhedron(cube(0, 4), List.of()); + assertTrue (Contains.contains(cube, f.createPoint(CRS3D, 2, 2, 2))); + assertTrue (Contains.contains(cube, f.createPoint(CRS3D, 0.1, 3.9, 0.2))); + assertFalse(Contains.contains(cube, f.createPoint(CRS3D, 5, 2, 2))); + + final Polyhedron hollow = f.createPolyhedron(cube(0, 4), List.of(cube(1, 3))); + assertTrue (Contains.contains(hollow, f.createPoint(CRS3D, 0.5, 2, 2)), "In the solid."); + assertFalse(Contains.contains(hollow, f.createPoint(CRS3D, 2, 2, 2)), "In the void."); + assertFalse(Contains.contains(hollow, f.createPoint(CRS3D, 5, 2, 2)), "Outside."); + + final Polyhedron icosahedron = new Icosahedron(); + assertTrue (Contains.contains(icosahedron, f.createPoint(icosahedron.getCoordinateReferenceSystem(), 0.1, -0.2, 0.3))); + assertFalse(Contains.contains(icosahedron, f.createPoint(icosahedron.getCoordinateReferenceSystem(), 0.9, 0.9, 0))); + + assertThrows(OperationException.class, () -> Contains.contains(cube, f.createPoint(CommonCRS.WGS84.geographic3D(), 2, 2, 2))); + } + + /** + * A ring in a vertical plane contains a point when tested in the plane of its normal. + */ + @Test + public void testRingInPlane() { + final Array ring = NDArrays.of(3, new double[] {0,2,0, 4,2,0, 4,2,4, 0,2,4, 0,2,0}); + final int[] axes = Maths.projectionAxes(Maths.calculatePolygonNormal(ring)); + assertTrue (Contains.contains(ring, Vectors.createDouble(3).set(new double[] {1, 2, 3}), axes)); + assertFalse(Contains.contains(ring, Vectors.createDouble(3).set(new double[] {5, 2, 3}), axes)); + assertFalse(Contains.contains(ring, Vectors.createDouble(3).set(new double[] {1, 2, 5}), axes)); + } + + /** + * Returns the six faces of the cube between the given minimum and maximum on all axes, oriented outward. + */ + private static GeometryCollection<Polygon> cube(final double min, final double max) { + final double[] xyz = corners(min, max); + final GeometryFactory f = GeometryFactory.DEFAULT; + final Polygon[] faces = new Polygon[6]; + for (int i = 0; i < 6; i++) { + //the two triangles of a face are (a,b,c) and (a,c,d), so the ring of the face is a, b, c, d. + final int a = CUBE[i*6], b = CUBE[i*6+1], c = CUBE[i*6+2], d = CUBE[i*6+5]; + final double[] coordinates = new double[5 * 3]; + int k = 0; + for (final int corner : new int[] {a, b, c, d, a}) { + System.arraycopy(xyz, corner * 3, coordinates, k, 3); + k += 3; + } + faces[i] = f.createPolygon(ring(coordinates), List.of()); + } + return f.createMultiPolygon(CRS3D, faces); + } + + private static LinearRing ring(final double... coordinates) { + return GeometryFactory.DEFAULT.createLinearRing(GeometryFactory.DEFAULT.createDataPoints(NDArrays.of(CRS3D, coordinates))); + } +} diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/Intersection3DTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/Intersection3DTest.java new file mode 100644 index 0000000000..3c241dfac0 --- /dev/null +++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/Intersection3DTest.java @@ -0,0 +1,264 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.sis.geometries.operation; + +import java.util.List; +import org.apache.sis.geometries.operation.Intersection.SegmentSegment; +import org.apache.sis.geometries.operation.Intersection.SegmentTriangle; +import org.apache.sis.geometries.operation.Intersection.TriangleTriangle; +import org.apache.sis.maths.Maths; +import org.apache.sis.maths.Vectors; + +// Test dependencies +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + + +/** + * Tests the intersections of segments and triangles of {@link Intersection}. + * + * @author Johann Sorel (Geomatys) + */ +public class Intersection3DTest { + + private static final double EPS = 1E-12; + + /** + * The axes of the (x,y) plane. + */ + private static final int[] XY = {0, 1}; + + /** + * The triangle used by most tests, in the (x,y) plane, counterclockwise seen from above. + */ + private static final double[] A = {0, 0, 0}, B = {4, 0, 0}, C = {0, 4, 0}; + + private static double[] p(final double... coordinates) { + return coordinates; + } + + /** + * Segments crossing, touching at an end, parallel, or apart. + */ + @Test + public void testSegmentSegment() { + SegmentSegment r = Intersection.segmentSegment2D(p(0,0), p(2,2), p(0,2), p(2,0), XY); + assertEquals(SegmentSegment.Type.POINT, r.type()); + assertEquals(0.5, r.startOnFirst(), EPS); + assertEquals(0.5, r.startOnSecond(), EPS); + + r = Intersection.segmentSegment2D(p(0,0), p(1,0), p(1,0), p(1,1), XY); + assertEquals(SegmentSegment.Type.POINT, r.type(), "Touching at their ends."); + assertEquals(1, r.startOnFirst(), "The end of a segment shall be exact."); + assertEquals(0, r.startOnSecond(), "The end of a segment shall be exact."); + + r = Intersection.segmentSegment2D(p(0,0), p(2,0), p(1,0), p(1,1), XY); + assertEquals(SegmentSegment.Type.POINT, r.type(), "An end on the other segment."); + assertEquals(0.5, r.startOnFirst(), EPS); + assertEquals(0, r.startOnSecond()); + + assertSame(SegmentSegment.NONE, Intersection.segmentSegment2D(p(0,0), p(1,0), p(0,1), p(1,1), XY), "Parallel."); + assertSame(SegmentSegment.NONE, Intersection.segmentSegment2D(p(0,0), p(1,0), p(2,-1), p(2,1), XY), "Lines crossing apart."); + + //segments in the vertical plane y = 5, in its (x,z) axes. + r = Intersection.segmentSegment2D(p(0,5,0), p(2,5,2), p(0,5,2), p(2,5,0), new int[] {0, 2}); + assertEquals(SegmentSegment.Type.POINT, r.type()); + assertEquals(0.5, r.startOnFirst(), EPS); + } + + /** + * Segments on the same line, overlapping, touching at an end, or apart. + */ + @Test + public void testCollinearSegments() { + SegmentSegment r = Intersection.segmentSegment2D(p(0,0), p(4,0), p(3,0), p(1,0), XY); + assertEquals(SegmentSegment.Type.OVERLAP, r.type()); + assertEquals(0.25, r.startOnFirst(), EPS); + assertEquals(0.75, r.endOnFirst(), EPS); + assertEquals(1, r.startOnSecond(), "The point at (1,0) is the end of the second segment."); + assertEquals(0, r.endOnSecond(), "The point at (3,0) is the start of the second segment."); + + r = Intersection.segmentSegment2D(p(4,0), p(0,0), p(1,0), p(3,0), XY); + assertEquals(SegmentSegment.Type.OVERLAP, r.type(), "First segment in the opposite direction."); + assertEquals(0.25, r.startOnFirst(), EPS); + assertEquals(0.75, r.endOnFirst(), EPS); + assertEquals(1, r.startOnSecond()); + assertEquals(0, r.endOnSecond()); + + r = Intersection.segmentSegment2D(p(0,0), p(1,1), p(1,1), p(2,2), XY); + assertEquals(SegmentSegment.Type.POINT, r.type(), "Touching at their ends."); + assertEquals(1, r.startOnFirst()); + assertEquals(0, r.startOnSecond()); + + assertSame(SegmentSegment.NONE, Intersection.segmentSegment2D(p(0,0), p(1,0), p(2,0), p(3,0), XY)); + } + + /** + * A segment crossing the plane of the triangle inside it, on an edge, on a corner, or outside. + */ + @Test + public void testSegmentTriangle() { + assertContact(SegmentTriangle.Type.INTERIOR, -1, 0.5, p(1,1,-1), p(1,1,1)); + assertContact(SegmentTriangle.Type.EDGE, 0, 0.5, p(2,0,-1), p(2,0,1)); + assertContact(SegmentTriangle.Type.EDGE, 1, 0.5, p(2,2,-1), p(2,2,1)); + assertContact(SegmentTriangle.Type.EDGE, 2, 0.5, p(0,2,-1), p(0,2,1)); + assertContact(SegmentTriangle.Type.VERTEX, 1, 0.5, p(4,0,-1), p(4,0,1)); + assertContact(SegmentTriangle.Type.VERTEX, 0, 0.25, p(0,0,-1), p(0,0,3)); + assertSame(SegmentTriangle.NONE, Intersection.segmentTriangle(p(5,5,-1), p(5,5,1), A, B, C, 0), "Outside."); + assertSame(SegmentTriangle.NONE, Intersection.segmentTriangle(p(1,1, 1), p(1,1,2), A, B, C, 0), "Above."); + } + + /** + * A segment with an end on the plane of the triangle, or in that plane. + */ + @Test + public void testSegmentEndOnTriangle() { + assertContact(SegmentTriangle.Type.INTERIOR, -1, 0, p(1,1,0), p(1,1,5)); + assertContact(SegmentTriangle.Type.EDGE, 0, 1, p(2,0,3), p(2,0,0)); + assertContact(SegmentTriangle.Type.VERTEX, 2, 0, p(0,4,0), p(1,1,1)); + assertSame(SegmentTriangle.NONE, Intersection.segmentTriangle(p(5,5,0), p(5,5,1), A, B, C, 0), "End on the plane, outside."); + assertSame(SegmentTriangle.COPLANAR, Intersection.segmentTriangle(p(1,1,0), p(3,0,0), A, B, C, 0)); + //an end near the plane is on it only within the tolerance. + assertSame(SegmentTriangle.NONE, Intersection.segmentTriangle(p(1,1,1E-12), p(1,1,5), A, B, C, 0)); + final SegmentTriangle r = Intersection.segmentTriangle(p(1,1,1E-12), p(1,1,5), A, B, C, 1E-9); + assertEquals(SegmentTriangle.Type.INTERIOR, r.type()); + assertEquals(0, r.ratio()); + } + + /** + * A segment crossing a triangle in an oblique plane, with coordinates which are not integers. + */ + @Test + public void testSegmentObliqueTriangle() { + final double[] o = {0.1, 0.2, 0.3}, u = {1/3., 2/3., 2/3.}, v = {2/3., 1/3., -2/3.}; + final double[] n = Vectors.cross(u, v); + final double[] a = o, b = Vectors.add(o, Vectors.scale(u, 4)), c = Vectors.add(o, Vectors.scale(v, 4)); + final double[] inside = Vectors.add(o, Vectors.add(u, v)); + final double[] outside = Vectors.add(o, Vectors.scale(Vectors.add(u, v), 3)); + SegmentTriangle r = Intersection.segmentTriangle(Vectors.add(inside, Vectors.scale(n, -1)), Vectors.add(inside, Vectors.scale(n, 3)), a, b, c, 0); + assertEquals(SegmentTriangle.Type.INTERIOR, r.type()); + assertEquals(0.25, r.ratio(), EPS); + r = Intersection.segmentTriangle(Vectors.add(outside, Vectors.scale(n, -1)), Vectors.add(outside, Vectors.scale(n, 1)), a, b, c, 0); + assertEquals(SegmentTriangle.Type.NONE, r.type()); + } + + /** + * Segments in the plane of the triangle: inside, crossing, through a corner, along an edge, or outside. + */ + @Test + public void testSegmentInTriangle() { + List<SegmentTriangle> r = Intersection.segmentInTriangle2D(p(1,1,0), p(2,1,0), A, B, C, XY); + assertPoints(r, SegmentTriangle.Type.INTERIOR, -1, 0, SegmentTriangle.Type.INTERIOR, -1, 1); + + r = Intersection.segmentInTriangle2D(p(-1,1,0), p(5,1,0), A, B, C, XY); + assertPoints(r, SegmentTriangle.Type.EDGE, 2, 1/6., SegmentTriangle.Type.EDGE, 1, 2/3.); + + r = Intersection.segmentInTriangle2D(p(1,1,0), p(5,1,0), A, B, C, XY); + assertPoints(r, SegmentTriangle.Type.INTERIOR, -1, 0, SegmentTriangle.Type.EDGE, 1, 0.5); + + r = Intersection.segmentInTriangle2D(p(-1,-1,0), p(2,2,0), A, B, C, XY); + assertPoints(r, SegmentTriangle.Type.VERTEX, 0, 1/3., SegmentTriangle.Type.EDGE, 1, 1); + + r = Intersection.segmentInTriangle2D(p(-1,0,0), p(2,0,0), A, B, C, XY); + assertPoints(r, SegmentTriangle.Type.VERTEX, 0, 1/3., SegmentTriangle.Type.EDGE, 0, 1); + + r = Intersection.segmentInTriangle2D(p(4,0,0), p(5,-1,0), A, B, C, XY); + assertEquals(1, r.size(), "Touching at a corner only."); + assertEquals(SegmentTriangle.Type.VERTEX, r.get(0).type()); + assertEquals(1, r.get(0).feature()); + assertEquals(0, r.get(0).ratio()); + + assertTrue(Intersection.segmentInTriangle2D(p(5,5,0), p(6,6,0), A, B, C, XY).isEmpty()); + + //a triangle in the vertical plane y = 2, projected on the plane of its normal. + final double[] a = {0,2,0}, b = {4,2,0}, c = {0,2,4}; + final int[] axes = Maths.projectionAxes(Maths.calculateNormal(a, b, c)); + r = Intersection.segmentInTriangle2D(p(1,2,1), p(1,2,5), a, b, c, axes); + assertPoints(r, SegmentTriangle.Type.INTERIOR, -1, 0, SegmentTriangle.Type.EDGE, 1, 0.5); + } + + /** + * Triangles crossing, touching at a point or along an edge, coplanar or apart. + */ + @Test + public void testTriangleTriangle() { + //a vertical triangle crossing the interior of the horizontal one. + TriangleTriangle r = Intersection.triangleTriangle(A, B, C, p(1,1,-1), p(1,1,1), p(2.5,1,0), 0); + assertEquals(TriangleTriangle.Type.SEGMENT, r.type()); + assertEnds(r, p(1,1,0), p(2.5,1,0)); + + //a vertical triangle crossing an edge of the horizontal one. + r = Intersection.triangleTriangle(A, B, C, p(1,-1,-1), p(1,-1,1), p(1,2,0), 0); + assertEquals(TriangleTriangle.Type.SEGMENT, r.type()); + assertEnds(r, p(1,0,0), p(1,2,0)); + + //a vertical triangle standing on an edge of the horizontal one. + r = Intersection.triangleTriangle(A, B, C, p(1,0,0), p(3,0,0), p(2,0,2), 0); + assertEquals(TriangleTriangle.Type.SEGMENT, r.type()); + assertEnds(r, p(1,0,0), p(3,0,0)); + + //a triangle touching the horizontal one by a corner. + r = Intersection.triangleTriangle(A, B, C, p(1,1,0), p(2,1,1), p(1,2,1), 0); + assertEquals(TriangleTriangle.Type.POINT, r.type()); + assertSame(r.start(), r.end()); + assertArrayEquals(p(1,1,0), r.start().position()); + + assertSame(TriangleTriangle.COPLANAR, Intersection.triangleTriangle(A, B, C, p(1,1,0), p(5,1,0), p(1,5,0), 0)); + assertSame(TriangleTriangle.DISJOINT, Intersection.triangleTriangle(A, B, C, p(10,10,1), p(11,10,1), p(10,11,2), 0), "Apart."); + assertSame(TriangleTriangle.DISJOINT, Intersection.triangleTriangle(A, B, C, p(5,5,-1), p(5,5,1), p(6,6,0), 0), + "Crossing the plane outside of the triangle."); + } + + /** + * Verifies the result of the intersection of a segment with the triangle {@link #A}, {@link #B}, {@link #C}. + */ + private static void assertContact(SegmentTriangle.Type type, int feature, double ratio, double[] s0, double[] s1) { + final SegmentTriangle r = Intersection.segmentTriangle(s0, s1, A, B, C, 0); + assertEquals(type, r.type()); + assertEquals(feature, r.feature()); + assertEquals(ratio, r.ratio(), EPS); + } + + /** + * Verifies the two ends of the part of a segment inside a triangle. + */ + private static void assertPoints(final List<SegmentTriangle> points, + final SegmentTriangle.Type type1, final int feature1, final double ratio1, + final SegmentTriangle.Type type2, final int feature2, final double ratio2) + { + assertEquals(2, points.size()); + assertEquals(type1, points.get(0).type()); + assertEquals(feature1, points.get(0).feature()); + assertEquals(ratio1, points.get(0).ratio(), EPS); + assertEquals(type2, points.get(1).type()); + assertEquals(feature2, points.get(1).feature()); + assertEquals(ratio2, points.get(1).ratio(), EPS); + } + + /** + * Verifies the ends of the intersection of two triangles, in any order. + */ + private static void assertEnds(final TriangleTriangle r, final double[] p1, final double[] p2) { + final double[] start = r.start().position(), end = r.end().position(); + if (Math.abs(start[0] - p1[0]) + Math.abs(start[1] - p1[1]) + Math.abs(start[2] - p1[2]) < EPS) { + assertArrayEquals(p2, end, EPS); + } else { + assertArrayEquals(p1, end, EPS); + assertArrayEquals(p2, start, EPS); + } + } +} diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/triangulate/EarClippingTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/triangulate/EarClippingTest.java index a649a46445..0a1c535918 100644 --- a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/triangulate/EarClippingTest.java +++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/triangulate/EarClippingTest.java @@ -28,6 +28,7 @@ import org.apache.sis.geometries.operation.OperationException; import org.apache.sis.geometries.surface.Polygon; import org.apache.sis.maths.Maths; import org.apache.sis.maths.NDArrays; +import org.apache.sis.maths.ReadOnly; import org.apache.sis.maths.Vectors; import org.apache.sis.referencing.CommonCRS; @@ -127,6 +128,47 @@ public class EarClippingTest { assertThrows(OperationException.class, () -> new EarClipping().triangulateIndexed(bowTie)); } + /** + * Triangulates a concave ring given by the indices of its vertices in an array of positions, some of them + * not in the ring. The ring gives n − 2 triangles covering its area, made of its vertices only and turning + * in its direction, whether it is closed or not. + */ + @Test + public void testTriangulateRing() { + final ReadOnly.Vector<?>[] positions = { + vector(9, 9), // not in the ring. + vector(0, 0), vector(4, 0), vector(4, 4), vector(2, 1), vector(0, 4) + }; + assertRingTriangles(positions, new int[] {1, 2, 3, 4, 5}, true); + assertRingTriangles(positions, new int[] {1, 2, 3, 4, 5, 1}, true); + assertRingTriangles(positions, new int[] {5, 4, 3, 2, 1}, false); + + final ReadOnly.Vector<?>[] bowTie = {vector(0, 0), vector(2, 2), vector(2, 0), vector(0, 2)}; + assertThrows(OperationException.class, () -> new EarClipping().triangulate(bowTie, new int[] {0, 1, 2, 3})); + } + + /** + * Triangulates the given ring of five vertices, of area 10, and verifies the triangles. + */ + private static void assertRingTriangles(final ReadOnly.Vector<?>[] positions, final int[] ring, final boolean counterclockwise) { + final int[] triangles = new EarClipping().triangulate(positions, ring); + assertEquals((5 - 2) * 3, triangles.length, "Expected n − 2 triangles."); + double area = 0; + for (int i = 0; i < triangles.length; i += 3) { + for (int k = 0; k < 3; k++) { + assertNotEquals(0, triangles[i+k], "The position is not a vertex of the ring."); + } + final double side = Maths.lineSide(positions[triangles[i]], positions[triangles[i+1]], positions[triangles[i+2]]); + assertTrue(counterclockwise ? side > 0 : side < 0, "The triangle shall turn in the direction of the ring."); + area += Math.abs(side) / 2; + } + assertEquals(10, area, 1E-12); + } + + private static ReadOnly.Vector<?> vector(final double x, final double y) { + return Vectors.createDouble(2).set(new double[] {x, y}); + } + /** * Triangulates the given polygon and verifies that each triangle turns in the direction of the exterior ring, * which is that its normal has the direction of the normal of the ring. Also verifies the area and the number diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/maths/MathsTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/maths/MathsTest.java index 140dde08a1..d997e4f5c0 100644 --- a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/maths/MathsTest.java +++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/maths/MathsTest.java @@ -217,6 +217,34 @@ public class MathsTest { assertThrows(IllegalArgumentException.class, () -> Maths.projectionAxes(new double[] {0, 1})); } + /** + * Tests {@link Maths#planeSide(double[], double[], double[], double[], double)} on a horizontal triangle, + * with and without tolerance, and on a triangle in an oblique plane with coordinates which are not integers. + */ + @Test + public void testPlaneSide() { + final double[] a = {0, 0, 0}, b = {1, 0, 0}, c = {0, 1, 0}; + assertEquals( 1, Maths.planeSide(a, b, c, new double[] {0.3, 0.3, 1}, 0), "Above, toward the normal."); + assertEquals(-1, Maths.planeSide(a, b, c, new double[] {0.3, 0.3, -1}, 0), "Below."); + assertEquals(-1, Maths.planeSide(a, c, b, new double[] {0.3, 0.3, 1}, 0), "Reversed triangle."); + assertEquals( 0, Maths.planeSide(a, b, c, new double[] {5, 5, 0}, 0), "On the plane, outside of the triangle."); + assertEquals( 1, Maths.planeSide(a, b, c, new double[] {0.3, 0.3, 1E-12}, 0)); + assertEquals( 0, Maths.planeSide(a, b, c, new double[] {0.3, 0.3, 1E-12}, 1E-9), "Within the tolerance."); + assertEquals( 1, Maths.planeSide(a, b, c, new double[] {0.3, 0.3, 2E-9}, 1E-9), "Beyond the tolerance."); + assertEquals( 0, Maths.planeSide(a, b, new double[] {2, 0, 0}, new double[] {0, 0, 1}, 0), "Degenerate triangle."); + /* + * A triangle made of two orthonormal vectors u and v of an oblique plane, whose normal is u × v. + */ + final double[] o = {0.1, 0.2, 0.3}, u = {1/3., 2/3., 2/3.}, v = {2/3., 1/3., -2/3.}; + final double[] n = Vectors.cross(u, v); + final double[] ou = Vectors.add(o, u), ov = Vectors.add(o, v); + final double[] inPlane = Vectors.add(o, Vectors.add(Vectors.scale(u, 0.4), Vectors.scale(v, 0.3))); + assertEquals( 1, Maths.planeSide(o, ou, ov, Vectors.add(inPlane, Vectors.scale(n, 1E-3)), 0)); + assertEquals(-1, Maths.planeSide(o, ou, ov, Vectors.add(inPlane, Vectors.scale(n, -1E-3)), 0)); + assertEquals( 0, Maths.planeSide(o, ou, ov, Vectors.add(inPlane, Vectors.scale(n, 1E-3)), 1E-2)); + assertEquals( 0, Maths.planeSide(o, ou, ov, inPlane, 1E-12), "Rounding errors within the tolerance."); + } + /** * Tests that {@link Maths#projectionAxes(double[])} keeps the orientation: a triangle whose normal has the * direction given to the method is counterclockwise once projected, and the reversed triangle clockwise.
