diff --git a/core/.classpath b/core/.classpath
index 7b26f669d..47c188a43 100644
--- a/core/.classpath
+++ b/core/.classpath
@@ -38,5 +38,6 @@
+
diff --git a/core/OpenRocket Core.iml b/core/OpenRocket Core.iml
index 3b9cedd08..453e9a5d8 100644
--- a/core/OpenRocket Core.iml
+++ b/core/OpenRocket Core.iml
@@ -307,6 +307,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/core/lib/jts-core-1.19.0.jar b/core/lib/jts-core-1.19.0.jar
new file mode 100644
index 000000000..f465913d5
Binary files /dev/null and b/core/lib/jts-core-1.19.0.jar differ
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObj.java b/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObj.java
index 284aab922..97f0cdc54 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObj.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObj.java
@@ -218,6 +218,10 @@ public final class DefaultObj implements Obj {
return faces.size();
}
+ public List getFaces() {
+ return faces;
+ }
+
@Override
public ObjFace getFace(int index) {
return faces.get(index);
@@ -458,6 +462,14 @@ public final class DefaultObj implements Obj {
addFace(v, null, null);
}
+ public void removeFace(int index) {
+ faces.remove(index);
+ }
+
+ public void removeFace(ObjFace face) {
+ faces.remove(face);
+ }
+
@Override
public void addFaceWithTexCoords(int... v) {
addFace(v, v, null);
@@ -603,5 +615,22 @@ public final class DefaultObj implements Obj {
}
}
}
+
+ /**
+ * Creates a clone of this object.
+ *
+ * @param cloneFacesAndGroups Whether the faces should be cloned
+ * @return a new DefaultObj object with the same properties as this object
+ */
+ public DefaultObj clone(boolean cloneFacesAndGroups) {
+ DefaultObj newObj = new DefaultObj();
+ newObj.setMtlFileNames(getMtlFileNames());
+ ObjUtils.copyAllVertices(this, newObj);
+ if (cloneFacesAndGroups) {
+ ObjUtils.copyAllFacesAndGroups(this, newObj);
+ }
+
+ return newObj;
+ }
}
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObjEdge.java b/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObjEdge.java
new file mode 100644
index 000000000..134c6a5e1
--- /dev/null
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObjEdge.java
@@ -0,0 +1,65 @@
+package net.sf.openrocket.file.wavefrontobj;
+
+public class DefaultObjEdge {
+ /**
+ * The vertex index of the start of this edge
+ */
+ private final int startVertexIndex;
+ /**
+ * The vertex index of the end of this edge
+ */
+ private final int endVertexIndex;
+
+ /**
+ * The normal index of the start of this edge
+ */
+ private final int startNormalIndex;
+ /**
+ * The normal index of the end of this edge
+ */
+ private final int endNormalIndex;
+
+ /**
+ * The texture coordinate index of the start of this edge
+ */
+ private final int startTexCoordIndex;
+ /**
+ * The texture coordinate index of the end of this edge
+ */
+ private final int endTexCoordIndex;
+
+
+ public DefaultObjEdge(int startVertexIndex, int endVertexIndex, int startTexCoordIndex, int endTexCoordIndex, int startNormalIndex, int endNormalIndex) {
+ this.startVertexIndex = startVertexIndex;
+ this.endVertexIndex = endVertexIndex;
+ this.startTexCoordIndex = startTexCoordIndex;
+ this.endTexCoordIndex = endTexCoordIndex;
+ this.startNormalIndex = startNormalIndex;
+ this.endNormalIndex = endNormalIndex;
+ }
+
+ public static DefaultObjEdge[] createEdges(DefaultObjFace faces) {
+ DefaultObjEdge[] edges = new DefaultObjEdge[faces.getNumVertices()-1];
+ for (int i = 0; i < faces.getNumVertices()-1; i++) {
+ int startVertexIndex = faces.getVertexIndex(i);
+ int endVertexIndex = faces.getVertexIndex((i + 1));
+
+ int startNormalIndex = -1;
+ int endNormalIndex = -1;
+ if (faces.containsNormalIndices()) {
+ startNormalIndex = faces.getNormalIndex(i);
+ endNormalIndex = faces.getNormalIndex((i + 1));
+ }
+
+ int startTexCoordIndex = -1;
+ int endTexCoordIndex = -1;
+ if (faces.containsTexCoordIndices()) {
+ startTexCoordIndex = faces.getTexCoordIndex(i);
+ endTexCoordIndex = faces.getTexCoordIndex((i + 1));
+ }
+
+ edges[i] = new DefaultObjEdge(startVertexIndex, endVertexIndex, startTexCoordIndex, endTexCoordIndex, startNormalIndex, endNormalIndex);
+ }
+ return edges;
+ }
+}
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObjFace.java b/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObjFace.java
index 09773de1c..4b4edb322 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObjFace.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObjFace.java
@@ -30,6 +30,8 @@ package net.sf.openrocket.file.wavefrontobj;
import de.javagl.obj.ObjFace;
+import java.util.Arrays;
+
/**
* Default implementation of an ObjFace
*/
@@ -63,6 +65,12 @@ public final class DefaultObjFace implements ObjFace {
this.normalIndices = normalIndices;
}
+ public DefaultObjFace(DefaultObjFace face) {
+ this.vertexIndices = Arrays.copyOf(face.vertexIndices, face.vertexIndices.length);
+ this.texCoordIndices = face.texCoordIndices != null ? Arrays.copyOf(face.texCoordIndices, face.texCoordIndices.length) : null;
+ this.normalIndices = face.normalIndices != null ? Arrays.copyOf(face.normalIndices, face.normalIndices.length) : null;
+ }
+
@Override
public boolean containsTexCoordIndices() {
@@ -74,6 +82,18 @@ public final class DefaultObjFace implements ObjFace {
return normalIndices != null;
}
+ public int[] getVertexIndices() {
+ return vertexIndices;
+ }
+
+ public int[] getTexCoordIndices() {
+ return texCoordIndices;
+ }
+
+ public int[] getNormalIndices() {
+ return normalIndices;
+ }
+
@Override
public int getVertexIndex(int number) {
return this.vertexIndices[number];
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObjGroup.java b/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObjGroup.java
index 8e9236687..1453141c3 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObjGroup.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/DefaultObjGroup.java
@@ -71,10 +71,36 @@ public final class DefaultObjGroup implements ObjGroup {
faces.add(face);
}
+ public void addFaces(List faces) {
+ this.faces.addAll(faces);
+ }
+
+ /**
+ * Remove the given face from this group
+ *
+ * @param face The face to remove
+ */
+ public void removeFace(ObjFace face) {
+ faces.remove(face);
+ }
+
+ /**
+ * Returns the faces in this group
+ * @return The faces in this group
+ */
public List getFaces() {
return faces;
}
+ /**
+ * Returns whether this group contains the given face
+ * @param face
+ * @return
+ */
+ public boolean containsFace(ObjFace face) {
+ return faces.contains(face);
+ }
+
@Override
public int getNumFaces() {
return faces.size();
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/ObjUtils.java b/core/src/net/sf/openrocket/file/wavefrontobj/ObjUtils.java
index 0931fc57d..f7feec2d5 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/ObjUtils.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/ObjUtils.java
@@ -5,11 +5,16 @@ import de.javagl.obj.FloatTuples;
import de.javagl.obj.Obj;
import de.javagl.obj.ObjFace;
import de.javagl.obj.ObjGroup;
+import de.javagl.obj.ReadableObj;
+import de.javagl.obj.WritableObj;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.util.Coordinate;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* Utility methods for working with {@link Obj} objects.
@@ -499,4 +504,124 @@ public class ObjUtils {
}
return FloatTuples.create(fr, fg, fb);
}
+
+ /**
+ * Returns an array of FloatTuples representing the vertices of the object
+ *
+ * @param obj The DefaultObj object from which to retrieve the vertices
+ * @param vertexIndices An array of vertex indices specifying which vertices to retrieve
+ * @return An array of FloatTuples representing the vertices
+ */
+ public static FloatTuple[] getVertices(DefaultObj obj, int[] vertexIndices) {
+ FloatTuple[] vertices = new FloatTuple[vertexIndices.length];
+ for (int i = 0; i < vertexIndices.length; i++) {
+ vertices[i] = obj.getVertex(vertexIndices[i]);
+ }
+ return vertices;
+ }
+
+ public static FloatTuple[] getVertices(DefaultObj obj, DefaultObjFace face) {
+ return getVertices(obj, face.getVertexIndices());
+ }
+
+ public static DefaultObjFace createFaceWithNewIndices(ObjFace face, int... n) {
+ int[] v = new int[n.length];
+ int[] vt = null;
+ int[] vn = null;
+
+ for (int i = 0; i < n.length; i++) {
+ v[i] = face.getVertexIndex(n[i]);
+ }
+
+ if (face.containsTexCoordIndices()) {
+ vt = new int[n.length];
+
+ for (int i = 0; i < n.length; i++) {
+ vt[i] = face.getTexCoordIndex(n[i]);
+ }
+ }
+
+ if (face.containsNormalIndices()) {
+ vn = new int[n.length];
+
+ for (int i = 0; i < n.length; i++) {
+ vn[i] = face.getNormalIndex(n[i]);
+ }
+ }
+
+ return new DefaultObjFace(v, vt, vn);
+ }
+
+ /**
+ * Copy all vertices, texture coordinates and normals from the input to the output
+ * @param input The input object
+ * @param output The output object
+ */
+ public static void copyAllVertices(ReadableObj input, WritableObj output) {
+ for (int i = 0; i < input.getNumVertices(); i++) {
+ output.addVertex(input.getVertex(i));
+ }
+
+ for (int i = 0; i < input.getNumTexCoords(); i++) {
+ output.addTexCoord(input.getTexCoord(i));
+ }
+
+ for (int i = 0; i < input.getNumNormals(); i++) {
+ output.addNormal(input.getNormal(i));
+ }
+ }
+
+ /**
+ * Copy all faces and groups from the input to the output
+ * @param source The source object
+ * @param target The target object
+ */
+ public static void copyAllFacesAndGroups(DefaultObj source, DefaultObj target) {
+ // Store the copied faces so we don't end up adding multiple copies of the same face
+ Map srcToTarFaceMap = new HashMap<>();
+
+ // Copy the groups (and their faces)
+ for (int i = 0; i < source.getNumGroups(); i++) {
+ DefaultObjGroup srcGroup = (DefaultObjGroup) source.getGroup(i);
+ DefaultObjGroup tarGroup = new DefaultObjGroup(srcGroup.getName());
+ for (int j = 0; j < srcGroup.getNumFaces(); j++) {
+ DefaultObjFace srcFace = (DefaultObjFace) srcGroup.getFace(j);
+ DefaultObjFace storedFace = srcToTarFaceMap.get(srcFace);
+
+ DefaultObjFace tarFace = storedFace != null ? storedFace : new DefaultObjFace(srcFace);
+ tarGroup.addFace(tarFace);
+ srcToTarFaceMap.put(srcFace, tarFace);
+ }
+ target.addGroup(tarGroup);
+ }
+
+ // Copy the faces
+ for (int i = 0; i < source.getNumFaces(); i++) {
+ DefaultObjFace srcFace = (DefaultObjFace) source.getFace(i);
+ DefaultObjFace tarFace = srcToTarFaceMap.get(srcFace);
+ tarFace = tarFace != null ? tarFace : new DefaultObjFace(srcFace);
+ target.addFace(tarFace);
+ }
+ }
+
+ /**
+ * Activates the groups and materials specified by the given face in the input object,
+ * and sets the active groups and material in the output object accordingly.
+ *
+ * @param input The input object from which to activate the groups and materials
+ * @param face The face containing the groups and materials to activate
+ * @param output The output object in which to set the active groups and materials
+ */
+ public static void activateGroups(ReadableObj input, ObjFace face, WritableObj output) {
+ Set activatedGroupNames = input.getActivatedGroupNames(face);
+ if (activatedGroupNames != null) {
+ output.setActiveGroupNames(activatedGroupNames);
+ }
+
+ String activatedMaterialGroupName = input.getActivatedMaterialGroupName(face);
+ if (activatedMaterialGroupName != null) {
+ output.setActiveMaterialGroupName(activatedMaterialGroupName);
+ }
+
+ }
}
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/TriangulationHelper.java b/core/src/net/sf/openrocket/file/wavefrontobj/TriangulationHelper.java
index fdacd8c44..d8a8cae38 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/TriangulationHelper.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/TriangulationHelper.java
@@ -1,7 +1,291 @@
package net.sf.openrocket.file.wavefrontobj;
+import de.javagl.obj.FloatTuple;
+import de.javagl.obj.ObjFace;
+import de.javagl.obj.ObjGroup;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Polygon;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.triangulate.polygon.ConstrainedDelaunayTriangulator;
+import org.locationtech.jts.triangulate.tri.Tri;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
public abstract class TriangulationHelper {
public static DefaultObj simpleTriangulate(DefaultObj obj) {
return de.javagl.obj.ObjUtils.triangulate(obj, new DefaultObj());
}
+
+ public static DefaultObj constrainedDelaunayTriangulate(DefaultObj obj, DefaultObjFace face) {
+ // Create a new OBJ that will contain the triangulated faces, and copy all the vertices and MTL file names from the original OBJ
+ DefaultObj newObj = obj.clone(true);
+
+ // Generate the new triangulated faces
+ List newFaces = generateCDTFaces(obj, face);
+
+ // Add the triangulated faces
+ for (ObjFace newFace : newFaces) {
+ newObj.addFace(newFace);
+ }
+
+ // Remove the old face
+ obj.removeFace(face);
+ for (ObjGroup group : obj.getGroups()) {
+ DefaultObjGroup g = (DefaultObjGroup) group;
+ if (g.containsFace(face)) {
+ g.removeFace(face);
+ g.addFaces(newFaces);
+ }
+ }
+
+ return newObj;
+ }
+
+ public static DefaultObj constrainedDelaunayTriangulate(DefaultObj input) {
+ // Create a new OBJ that will contain the triangulated faces, and copy all the vertices and MTL file names from the original OBJ
+ DefaultObj output = input.clone(false);
+
+ for (ObjFace face : input.getFaces()) {
+ ObjUtils.activateGroups(input, face, output);
+ if (face.getNumVertices() == 3) {
+ output.addFace(face);
+ continue;
+ }
+
+ // Generate the new triangulated faces
+ List newFaces = generateCDTFaces(input, (DefaultObjFace) face);
+
+ // Add the triangulated faces
+ for (ObjFace newFace : newFaces) {
+ output.addFace(newFace);
+ }
+ }
+
+ return output;
+ }
+
+ public static List generateCDTFaces(DefaultObj obj, DefaultObjFace face) {
+ PolygonWithOriginalIndices polygonWithIndices = createProjectedPolygon(obj, face);
+ Polygon polygon = polygonWithIndices.getPolygon();
+ Map vertexIndexMap = polygonWithIndices.getVertexIndexMap();
+
+ ConstrainedDelaunayTriangulator triangulator = new ConstrainedDelaunayTriangulator(polygon);
+ List triangles = triangulator.getTriangles();
+
+ List newFaces = new ArrayList<>();
+ for (Tri tri : triangles) {
+ int[] vertexIndices = new int[3];
+ for (int i = 0; i < 3; i++) {
+ Coordinate coord = tri.getCoordinate(i);
+ vertexIndices[i] = getNearbyValue(vertexIndexMap, coord);
+ }
+ if (vertexIndices[0] != -1 && vertexIndices[1] != -1 && vertexIndices[2] != -1) {
+ //DefaultObjFace newFace = ObjUtils.createFaceWithNewIndices(face, vertexIndices);
+ DefaultObjFace newFace = new DefaultObjFace(vertexIndices, null, null);
+ newFaces.add(newFace);
+ }
+ }
+
+ return newFaces;
+ }
+
+ /**
+ * Projects 3D coordinates of a polygon onto a 2D plane for further processing with JTS.
+ * The projection minimizes distortion by aligning the polygon's normal with the Z-axis.
+ *
+ * @param obj The input OBJ containing 3D polygon data.
+ * @param face The specific face of the OBJ to be projected and triangulated.
+ * @return A polygon in 2D space suitable for use with JTS.
+ */
+ private static PolygonWithOriginalIndices createProjectedPolygon(DefaultObj obj, DefaultObjFace face) {
+ // Calculate the normal of the polygon to determine its orientation in 3D space
+ Coordinate normal = calculateNormal(
+ vertexToCoordinate(obj.getVertex(face.getVertexIndices()[0])),
+ vertexToCoordinate(obj.getVertex(face.getVertexIndices()[1])),
+ vertexToCoordinate(obj.getVertex(face.getVertexIndices()[2])));
+
+ // Create a list for storing the projected 2D coordinates
+ List projectedCoords = new ArrayList<>();
+ Map vertexIndexMap = new HashMap<>();
+
+ // Project each vertex onto the 2D plane
+ for (int vertexIndex : face.getVertexIndices()) {
+ FloatTuple vertex = obj.getVertex(vertexIndex);
+ Coordinate3D originalCoord = new Coordinate3D(vertexToCoordinate(vertex));
+ Coordinate projectedCoord = projectVertexOntoPlane(originalCoord, normal);
+ projectedCoord = new Coordinate(projectedCoord.x, projectedCoord.y);
+ projectedCoords.add(projectedCoord);
+
+ vertexIndexMap.put(new Coordinate3D(projectedCoord), vertexIndex);
+ }
+
+ // Ensure polygon closure by repeating the first coordinate at the end if necessary
+ if (!projectedCoords.isEmpty() && !projectedCoords.get(0).equals3D(projectedCoords.get(projectedCoords.size() - 1))) {
+ projectedCoords.add(projectedCoords.get(0));
+ }
+
+ GeometryFactory factory = new GeometryFactory();
+ Polygon polygon = factory.createPolygon(projectedCoords.toArray(new Coordinate[0]));
+
+ return new PolygonWithOriginalIndices(polygon, vertexIndexMap);
+ }
+
+ /**
+ * Projects a vertex onto a plane by rotating it so the face normal aligns with the Z-axis.
+ *
+ * @param vertex The 3D vertex to project.
+ * @param normal The normal vector of the polygon's face.
+ * @return The projected 2D coordinate of the vertex.
+ */
+ private static Coordinate projectVertexOntoPlane(Coordinate3D vertex, Coordinate normal) {
+ Coordinate zAxis = new Coordinate(0, 0, 1);
+ Coordinate rotationAxis = crossProduct(normal, zAxis);
+ double rotationAngle = Math.acos(dotProduct(normal, zAxis) / (magnitude(normal) * magnitude(zAxis)));
+
+ // Normalize the rotation axis
+ double axisLength = magnitude(rotationAxis);
+ if (axisLength > 0) {
+ rotationAxis.x /= axisLength;
+ rotationAxis.y /= axisLength;
+ rotationAxis.z /= axisLength;
+ }
+
+ // Use Rodrigues' rotation formula or a rotation matrix to rotate the vertex
+ Coordinate rotatedVertex = rotateVertex(vertex, rotationAxis, rotationAngle);
+
+ return new Coordinate(rotatedVertex.x, rotatedVertex.y); // Projected vertex
+ }
+
+ /**
+ * Rotates a vertex around a given axis by a specified angle.
+ *
+ * @param vertex3D The vertex to rotate.
+ * @param axis The axis of rotation.
+ * @param angle The angle of rotation in radians.
+ * @return The rotated vertex.
+ */
+ private static Coordinate rotateVertex(Coordinate3D vertex3D, Coordinate axis, double angle) {
+ Coordinate vertex = vertex3D.getCoordinate();
+ double cosTheta = Math.cos(angle);
+ double sinTheta = Math.sin(angle);
+ double x = vertex.x, y = vertex.y, z = vertex.z;
+ double u = axis.x, v = axis.y, w = axis.z;
+
+ // Apply Rodrigues' rotation formula
+ double v1 = u * x + v * y + w * z;
+ double xPrime = u * v1 * (1d - cosTheta) + x * cosTheta + (-w * y + v * z) * sinTheta;
+ double yPrime = v * v1 * (1d - cosTheta) + y * cosTheta + (w * x - u * z) * sinTheta;
+ double zPrime = w * v1 * (1d - cosTheta) + z * cosTheta + (-v * x + u * y) * sinTheta;
+
+ return new Coordinate(xPrime, yPrime, zPrime);
+ }
+
+ /**
+ * Computes the cross product of two vectors.
+ *
+ * @param v1 The first vector.
+ * @param v2 The second vector.
+ * @return The cross product.
+ */
+ private static Coordinate crossProduct(Coordinate v1, Coordinate v2) {
+ return new Coordinate(
+ v1.y * v2.z - v1.z * v2.y,
+ v1.z * v2.x - v1.x * v2.z,
+ v1.x * v2.y - v1.y * v2.x
+ );
+ }
+
+ private static Coordinate vertexToCoordinate(FloatTuple vertex) {
+ return new Coordinate(vertex.getX(), vertex.getY(), vertex.getZ());
+ }
+
+
+ private static int getNearbyValue(Map vertexIndexMap, Coordinate coord) {
+ for (Map.Entry entry : vertexIndexMap.entrySet()) {
+ Coordinate key = entry.getKey().getCoordinate();
+ if (key.equals3D(coord)) {
+ return entry.getValue();
+ }
+ }
+ return -1; // Or any default value.
+ }
+
+
+ private static double dotProduct(Coordinate v1, Coordinate v2) {
+ return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
+ }
+
+
+ private static Coordinate subtract(Coordinate v1, Coordinate v2) {
+ return new Coordinate(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z);
+ }
+
+ private static Coordinate normalize(Coordinate vector) {
+ double magnitude = Math.sqrt(dotProduct(vector, vector)); // Calculate magnitude
+ if (magnitude == 0) {
+ // Handle potential divide by zero if the vector is a zero vector
+ return new Coordinate(0, 0, 0);
+ }
+
+ return new Coordinate(vector.x / magnitude, vector.y / magnitude, vector.z / magnitude);
+ }
+
+ private static double magnitude(Coordinate vector) {
+ return Math.sqrt(dotProduct(vector, vector));
+ }
+
+ public static Coordinate calculateNormal(Coordinate p1, Coordinate p2, Coordinate p3) {
+ Coordinate u = subtract(p2, p1);
+ Coordinate v = subtract(p3, p1);
+
+ return normalize(crossProduct(u, v));
+ }
+
+ private static class PolygonWithOriginalIndices {
+ private final Polygon polygon;
+ private final Map vertexIndexMap;
+
+ public PolygonWithOriginalIndices(Polygon polygon, Map vertexIndexMap) {
+ this.polygon = polygon;
+ this.vertexIndexMap = vertexIndexMap;
+ }
+
+ public Polygon getPolygon() {
+ return polygon;
+ }
+
+ public Map getVertexIndexMap() {
+ return vertexIndexMap;
+ }
+ }
+
+ // Helper class to wrap Coordinate and override equals and hashCode to account for all 3 dimensions
+ private static class Coordinate3D {
+ private Coordinate coordinate;
+
+ public Coordinate3D(Coordinate coordinate) {
+ this.coordinate = coordinate;
+ }
+
+ public Coordinate getCoordinate() {
+ return coordinate;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Coordinate3D that = (Coordinate3D) o;
+ return coordinate.equals3D(that.coordinate);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(coordinate.x, coordinate.y, coordinate.z);
+ }
+ }
}
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactory.java b/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactory.java
index 8b416ca66..cbfb760e1 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactory.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactory.java
@@ -175,7 +175,7 @@ public class OBJExporterFactory {
// Triangulate mesh
if (this.options.isTriangulate()) {
- obj = TriangulationHelper.simpleTriangulate(obj);
+ obj = TriangulationHelper.constrainedDelaunayTriangulate(obj);
}
// Remove position offset
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/export/shapes/PolygonExporter.java b/core/src/net/sf/openrocket/file/wavefrontobj/export/shapes/PolygonExporter.java
index e380b3408..0c348b994 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/export/shapes/PolygonExporter.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/export/shapes/PolygonExporter.java
@@ -3,6 +3,7 @@ package net.sf.openrocket.file.wavefrontobj.export.shapes;
import com.sun.istack.NotNull;
import net.sf.openrocket.file.wavefrontobj.CoordTransform;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
+import net.sf.openrocket.file.wavefrontobj.DefaultObjEdge;
import net.sf.openrocket.file.wavefrontobj.DefaultObjFace;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
diff --git a/swing/build.xml b/swing/build.xml
index af8a1d3f5..287b1e764 100644
--- a/swing/build.xml
+++ b/swing/build.xml
@@ -145,6 +145,7 @@
+