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 @@ +