From 1866637a66ccd83ea945fd7e8f56fdb0ca6ca849 Mon Sep 17 00:00:00 2001 From: Craig Taverner Date: Wed, 18 Apr 2018 09:09:40 +0200 Subject: [PATCH] First draft of a 3d polygon, called PolygonMesh --- .../java/org/amanzi/spatial/core/Point.java | 8 + .../org/amanzi/spatial/core/PolygonMesh.java | 191 ++++++++++++++++++ .../amanzi/spatial/core/PolygonMeshTest.java | 97 +++++++++ 3 files changed, 296 insertions(+) create mode 100644 core/src/main/java/org/amanzi/spatial/core/PolygonMesh.java create mode 100644 core/src/test/java/org/amanzi/spatial/core/PolygonMeshTest.java diff --git a/core/src/main/java/org/amanzi/spatial/core/Point.java b/core/src/main/java/org/amanzi/spatial/core/Point.java index 18040f8..cc44ebd 100644 --- a/core/src/main/java/org/amanzi/spatial/core/Point.java +++ b/core/src/main/java/org/amanzi/spatial/core/Point.java @@ -65,4 +65,12 @@ public double[] getCoordinate() { public String toString() { return format("Point%s", Arrays.toString(coordinate)); } + + public Point withShift(double... shifts) { + double[] shifted = Arrays.copyOf(this.coordinate, this.coordinate.length); + for (int i = 0; i < shifted.length; i++) { + shifted[i] += shifts[i]; + } + return new Point(shifted); + } } \ No newline at end of file diff --git a/core/src/main/java/org/amanzi/spatial/core/PolygonMesh.java b/core/src/main/java/org/amanzi/spatial/core/PolygonMesh.java new file mode 100644 index 0000000..d36f194 --- /dev/null +++ b/core/src/main/java/org/amanzi/spatial/core/PolygonMesh.java @@ -0,0 +1,191 @@ +package org.amanzi.spatial.core; + +import java.lang.reflect.Array; +import java.util.*; + +import static java.lang.String.format; + +public class PolygonMesh implements Polygon { + private Point[] vertices; + private Edge[] edges; + private Face[] faces; + + private PolygonMesh(Point[] vertices, Edge[] edges, Face[] faces) { + this.vertices = vertices; + this.edges = edges; + this.faces = faces; + } + + @Override + public int dimension() { + return 3; + } + + @Override + public Point[] getPoints() { + return vertices; + } + + public Edge[] getEdges() { + return edges; + } + + public Face[] getFaces() { + return faces; + } + + @Override + public boolean isSimple() { + return false; + } + + @Override + public SimplePolygon[] getShells() { + return new SimplePolygon[0]; + } + + @Override + public SimplePolygon[] getHoles() { + return new SimplePolygon[0]; + } + + @Override + public MultiPolygon withShell(Polygon shell) { + return null; + } + + @Override + public MultiPolygon withHole(Polygon hole) { + return null; + } + + public static PolygonMeshBuilder start() { + return new PolygonMeshBuilder(); + } + + public static class Edge { + int startVertex; + int endVertex; + + Edge(int startVertex, int endVertex) { + if (startVertex < endVertex) { + this.startVertex = startVertex; + this.endVertex = endVertex; + } else { + this.startVertex = endVertex; + this.endVertex = startVertex; + } + } + + @Override + public int hashCode() { + return 31 * startVertex + endVertex; + } + + public boolean equals(Edge other) { + return this.startVertex == other.startVertex && this.endVertex == other.endVertex; + } + + @Override + public boolean equals(Object other) { + return other instanceof Edge && equals((Edge) other); + } + + @Override + public String toString() { + return format("Edge[%d,%d]", startVertex, endVertex); + } + } + + public static class Face { + int[] edges; + + private Face(int[] edges) { + this.edges = edges; + Arrays.sort(this.edges); + } + + @Override + public int hashCode() { + return Arrays.hashCode(edges); + } + + public boolean equals(Face other) { + return Arrays.equals(this.edges, other.edges); + } + + @Override + public boolean equals(Object other) { + return other instanceof Face && equals((Face) other); + } + + @Override + public String toString() { + return "Face" + Arrays.toString(edges); + } + } + + public static class PolygonMeshBuilder { + LinkedHashMap vertices = new LinkedHashMap<>(); + LinkedHashMap edges = new LinkedHashMap<>(); + LinkedHashMap faces = new LinkedHashMap<>(); + + public PolygonMeshBuilder addFace(SimplePolygon simple) { + Point previous = null; + Integer previousId = 0; + Point[] points = simple.getPoints(); + int[] faceEdges = new int[points.length - 1]; + int edgeIndex = 0; + for (Point vertex : simple.getPoints()) { + Integer vertexId = vertices.get(vertex); + if (vertexId == null) { + vertexId = vertices.size(); + vertices.put(vertex, vertexId); + } + if (previous != null) { + Edge edge = new Edge(previousId, vertexId); + Integer edgeId = edges.get(edge); + if (edgeId == null) { + edgeId = edges.size(); + edges.put(edge, edgeId); + } + faceEdges[edgeIndex] = edgeId; + edgeIndex += 1; + } + previous = vertex; + previousId = vertexId; + } + Face face = new Face(faceEdges); + Integer faceId = faces.get(face); + if (faceId == null) { + faceId = faces.size(); + faces.put(face, faceId); + } + return this; + } + + public PolygonMesh build() { + Point[] v = new MakeArray<>(Point.class).fromMap(vertices); + Edge[] e = new MakeArray<>(Edge.class).fromMap(edges); + Face[] f = new MakeArray<>(Face.class).fromMap(faces); + return new PolygonMesh(v, e, f); + } + + private static class MakeArray { + private Class c; + + private MakeArray(Class c) { + this.c = c; + } + + private T[] fromMap(HashMap map) { + @SuppressWarnings("unchecked") + T[] array = (T[]) Array.newInstance(c, map.size()); + for (Map.Entry entry : map.entrySet()) { + array[entry.getValue()] = entry.getKey(); + } + return array; + } + } + } +} diff --git a/core/src/test/java/org/amanzi/spatial/core/PolygonMeshTest.java b/core/src/test/java/org/amanzi/spatial/core/PolygonMeshTest.java new file mode 100644 index 0000000..dd534f1 --- /dev/null +++ b/core/src/test/java/org/amanzi/spatial/core/PolygonMeshTest.java @@ -0,0 +1,97 @@ +package org.amanzi.spatial.core; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static java.lang.String.format; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +public class PolygonMeshTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void shouldHaveCorrectHashCode() { + assertThat("Should have same hashcode", new PolygonMesh.Edge(1, 2).hashCode(), equalTo(new PolygonMesh.Edge(2, 1).hashCode())); + } + + @Test + public void shouldWorkWithBox() { + PolygonMesh box = makeBox(new Point(0, 0, 0), new double[]{10, 10, 10}); + debugPolygonMesh("Box", box); + assertThat("Box should have six faces", box.getFaces().length, equalTo(6)); + assertThat("Box should have eight vertices", box.getPoints().length, equalTo(8)); + assertThat("Box should have twelve edges", box.getEdges().length, equalTo(12)); + } + + @Test + public void shouldWorkWithIcosohedron() { + PolygonMesh icosohedron = makeIcosohedron(new Point(0, 0, 0), 10); + debugPolygonMesh("Icosohedron", icosohedron); + assertThat("Icosohedron should have eight faces", icosohedron.getFaces().length, equalTo(8)); + assertThat("Icosohedron should have six vertices", icosohedron.getPoints().length, equalTo(6)); + assertThat("Icosohedron should have twelve edges", icosohedron.getEdges().length, equalTo(12)); + } + + private void debugPolygonMesh(String name, PolygonMesh mesh) { + System.out.println(name + " has " + mesh.getPoints().length + " vertices"); + int index = 0; + Point[] vertices = mesh.getPoints(); + for (Point p : vertices) { + System.out.println("\t" + index + ":\t" + p); + index++; + } + System.out.println(name + " has " + mesh.getEdges().length + " edges"); + index = 0; + for (PolygonMesh.Edge e : mesh.getEdges()) { + System.out.println(format("\t%d:\t%s\t[%s, %s]", index, e, vertices[e.startVertex], vertices[e.endVertex])); + index++; + } + System.out.println(name + " has " + mesh.getFaces().length + " faces"); + index = 0; + for (PolygonMesh.Face f : mesh.getFaces()) { + System.out.println(format("\t%d:\t%s", index, f)); + index++; + } + + } + + private PolygonMesh makeBox(Point p000, double[] width) { + Point p100 = p000.withShift(width[0], 0, 0); + Point p110 = p000.withShift(width[0], width[1], 0); + Point p010 = p000.withShift(0, width[1], 0); + Point p001 = p000.withShift(0, 0, width[2]); + Point p101 = p100.withShift(0, 0, width[2]); + Point p111 = p110.withShift(0, 0, width[2]); + Point p011 = p010.withShift(0, 0, width[2]); + return PolygonMesh.start() + .addFace(Polygon.simple(p000, p100, p110, p010)) // back face + .addFace(Polygon.simple(p001, p101, p111, p011)) // front face + .addFace(Polygon.simple(p000, p001, p011, p010)) // left side + .addFace(Polygon.simple(p100, p101, p111, p110)) // right side + .addFace(Polygon.simple(p010, p011, p111, p110)) // top + .addFace(Polygon.simple(p000, p001, p101, p100)) // bottom + .build(); + } + + private PolygonMesh makeIcosohedron(Point p000, double radius) { + Point pp00 = p000.withShift(radius, 0, 0); + Point p0p0 = p000.withShift(0, radius, 0); + Point pn00 = p000.withShift(-radius, 0, 0); + Point p0n0 = p000.withShift(0, -radius, 0); + Point p00p = p000.withShift(0, 0, radius); + Point p00n = p000.withShift(0, 0, -radius); + return PolygonMesh.start() + .addFace(Polygon.simple(p00p, pp00, p0p0)) // upper +x+y + .addFace(Polygon.simple(p00p, pn00, p0p0)) // upper -x+y + .addFace(Polygon.simple(p00p, pn00, p0n0)) // upper -x-y + .addFace(Polygon.simple(p00p, pp00, p0n0)) // upper +x-y + .addFace(Polygon.simple(p00n, pp00, p0p0)) // lower +x+y + .addFace(Polygon.simple(p00n, pn00, p0p0)) // lower -x+y + .addFace(Polygon.simple(p00n, pn00, p0n0)) // lower -x-y + .addFace(Polygon.simple(p00n, pp00, p0n0)) // lower +x-y + .build(); + } +}