[#2444] Add first rudimentary CDT triangulation
This commit is contained in:
parent
5e61ffb491
commit
77844f80a2
@ -38,5 +38,6 @@
|
|||||||
<classpathentry kind="lib" path="lib/istack-commons-runtime.jar"/>
|
<classpathentry kind="lib" path="lib/istack-commons-runtime.jar"/>
|
||||||
<classpathentry kind="lib" path="lib/graal-sdk-22.1.0.1.jar"/>
|
<classpathentry kind="lib" path="lib/graal-sdk-22.1.0.1.jar"/>
|
||||||
<classpathentry kind="lib" path="lib/js-scriptengine-22.1.0.1.jar"/>
|
<classpathentry kind="lib" path="lib/js-scriptengine-22.1.0.1.jar"/>
|
||||||
|
<classpathentry kind="lib" path="lib/jts-core-1.19.0.jar"/>
|
||||||
<classpathentry kind="output" path="bin"/>
|
<classpathentry kind="output" path="bin"/>
|
||||||
</classpath>
|
</classpath>
|
||||||
|
@ -307,6 +307,15 @@
|
|||||||
<SOURCES />
|
<SOURCES />
|
||||||
</library>
|
</library>
|
||||||
</orderEntry>
|
</orderEntry>
|
||||||
|
<orderEntry type="module-library">
|
||||||
|
<library>
|
||||||
|
<CLASSES>
|
||||||
|
<root url="jar://$MODULE_DIR$/lib/jts-core-1.19.0.jar!/" />
|
||||||
|
</CLASSES>
|
||||||
|
<JAVADOC />
|
||||||
|
<SOURCES />
|
||||||
|
</library>
|
||||||
|
</orderEntry>
|
||||||
<orderEntry type="module-library">
|
<orderEntry type="module-library">
|
||||||
<library>
|
<library>
|
||||||
<CLASSES>
|
<CLASSES>
|
||||||
|
BIN
core/lib/jts-core-1.19.0.jar
Normal file
BIN
core/lib/jts-core-1.19.0.jar
Normal file
Binary file not shown.
@ -218,6 +218,10 @@ public final class DefaultObj implements Obj {
|
|||||||
return faces.size();
|
return faces.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<ObjFace> getFaces() {
|
||||||
|
return faces;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ObjFace getFace(int index) {
|
public ObjFace getFace(int index) {
|
||||||
return faces.get(index);
|
return faces.get(index);
|
||||||
@ -458,6 +462,14 @@ public final class DefaultObj implements Obj {
|
|||||||
addFace(v, null, null);
|
addFace(v, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeFace(int index) {
|
||||||
|
faces.remove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeFace(ObjFace face) {
|
||||||
|
faces.remove(face);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addFaceWithTexCoords(int... v) {
|
public void addFaceWithTexCoords(int... v) {
|
||||||
addFace(v, v, null);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,8 @@ package net.sf.openrocket.file.wavefrontobj;
|
|||||||
|
|
||||||
import de.javagl.obj.ObjFace;
|
import de.javagl.obj.ObjFace;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default implementation of an ObjFace
|
* Default implementation of an ObjFace
|
||||||
*/
|
*/
|
||||||
@ -63,6 +65,12 @@ public final class DefaultObjFace implements ObjFace {
|
|||||||
this.normalIndices = normalIndices;
|
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
|
@Override
|
||||||
public boolean containsTexCoordIndices() {
|
public boolean containsTexCoordIndices() {
|
||||||
@ -74,6 +82,18 @@ public final class DefaultObjFace implements ObjFace {
|
|||||||
return normalIndices != null;
|
return normalIndices != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int[] getVertexIndices() {
|
||||||
|
return vertexIndices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getTexCoordIndices() {
|
||||||
|
return texCoordIndices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getNormalIndices() {
|
||||||
|
return normalIndices;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getVertexIndex(int number) {
|
public int getVertexIndex(int number) {
|
||||||
return this.vertexIndices[number];
|
return this.vertexIndices[number];
|
||||||
|
@ -71,10 +71,36 @@ public final class DefaultObjGroup implements ObjGroup {
|
|||||||
faces.add(face);
|
faces.add(face);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addFaces(List<ObjFace> 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<ObjFace> getFaces() {
|
public List<ObjFace> getFaces() {
|
||||||
return faces;
|
return faces;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this group contains the given face
|
||||||
|
* @param face
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean containsFace(ObjFace face) {
|
||||||
|
return faces.contains(face);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getNumFaces() {
|
public int getNumFaces() {
|
||||||
return faces.size();
|
return faces.size();
|
||||||
|
@ -5,11 +5,16 @@ import de.javagl.obj.FloatTuples;
|
|||||||
import de.javagl.obj.Obj;
|
import de.javagl.obj.Obj;
|
||||||
import de.javagl.obj.ObjFace;
|
import de.javagl.obj.ObjFace;
|
||||||
import de.javagl.obj.ObjGroup;
|
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.l10n.Translator;
|
||||||
import net.sf.openrocket.startup.Application;
|
import net.sf.openrocket.startup.Application;
|
||||||
import net.sf.openrocket.util.Coordinate;
|
import net.sf.openrocket.util.Coordinate;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility methods for working with {@link Obj} objects.
|
* Utility methods for working with {@link Obj} objects.
|
||||||
@ -499,4 +504,124 @@ public class ObjUtils {
|
|||||||
}
|
}
|
||||||
return FloatTuples.create(fr, fg, fb);
|
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<DefaultObjFace, DefaultObjFace> 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<String> activatedGroupNames = input.getActivatedGroupNames(face);
|
||||||
|
if (activatedGroupNames != null) {
|
||||||
|
output.setActiveGroupNames(activatedGroupNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
String activatedMaterialGroupName = input.getActivatedMaterialGroupName(face);
|
||||||
|
if (activatedMaterialGroupName != null) {
|
||||||
|
output.setActiveMaterialGroupName(activatedMaterialGroupName);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,291 @@
|
|||||||
package net.sf.openrocket.file.wavefrontobj;
|
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 abstract class TriangulationHelper {
|
||||||
public static DefaultObj simpleTriangulate(DefaultObj obj) {
|
public static DefaultObj simpleTriangulate(DefaultObj obj) {
|
||||||
return de.javagl.obj.ObjUtils.triangulate(obj, new DefaultObj());
|
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<ObjFace> 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<ObjFace> newFaces = generateCDTFaces(input, (DefaultObjFace) face);
|
||||||
|
|
||||||
|
// Add the triangulated faces
|
||||||
|
for (ObjFace newFace : newFaces) {
|
||||||
|
output.addFace(newFace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ObjFace> generateCDTFaces(DefaultObj obj, DefaultObjFace face) {
|
||||||
|
PolygonWithOriginalIndices polygonWithIndices = createProjectedPolygon(obj, face);
|
||||||
|
Polygon polygon = polygonWithIndices.getPolygon();
|
||||||
|
Map<Coordinate3D, Integer> vertexIndexMap = polygonWithIndices.getVertexIndexMap();
|
||||||
|
|
||||||
|
ConstrainedDelaunayTriangulator triangulator = new ConstrainedDelaunayTriangulator(polygon);
|
||||||
|
List<Tri> triangles = triangulator.getTriangles();
|
||||||
|
|
||||||
|
List<ObjFace> 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<Coordinate> projectedCoords = new ArrayList<>();
|
||||||
|
Map<Coordinate3D, Integer> 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<Coordinate3D, Integer> vertexIndexMap, Coordinate coord) {
|
||||||
|
for (Map.Entry<Coordinate3D, Integer> 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<Coordinate3D, Integer> vertexIndexMap;
|
||||||
|
|
||||||
|
public PolygonWithOriginalIndices(Polygon polygon, Map<Coordinate3D, Integer> vertexIndexMap) {
|
||||||
|
this.polygon = polygon;
|
||||||
|
this.vertexIndexMap = vertexIndexMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Polygon getPolygon() {
|
||||||
|
return polygon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Coordinate3D, Integer> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -175,7 +175,7 @@ public class OBJExporterFactory {
|
|||||||
|
|
||||||
// Triangulate mesh
|
// Triangulate mesh
|
||||||
if (this.options.isTriangulate()) {
|
if (this.options.isTriangulate()) {
|
||||||
obj = TriangulationHelper.simpleTriangulate(obj);
|
obj = TriangulationHelper.constrainedDelaunayTriangulate(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove position offset
|
// Remove position offset
|
||||||
|
@ -3,6 +3,7 @@ package net.sf.openrocket.file.wavefrontobj.export.shapes;
|
|||||||
import com.sun.istack.NotNull;
|
import com.sun.istack.NotNull;
|
||||||
import net.sf.openrocket.file.wavefrontobj.CoordTransform;
|
import net.sf.openrocket.file.wavefrontobj.CoordTransform;
|
||||||
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
|
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.DefaultObjFace;
|
||||||
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
|
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
|
||||||
|
|
||||||
|
@ -145,6 +145,7 @@
|
|||||||
<zipfileset src="${core.dir}/lib/logback-classic-1.2.11.jar"/>
|
<zipfileset src="${core.dir}/lib/logback-classic-1.2.11.jar"/>
|
||||||
<zipfileset src="${core.dir}/lib/logback-core-1.2.11.jar"/>
|
<zipfileset src="${core.dir}/lib/logback-core-1.2.11.jar"/>
|
||||||
<zipfileset src="${core.dir}/lib/obj-0.4.0.jar"/>
|
<zipfileset src="${core.dir}/lib/obj-0.4.0.jar"/>
|
||||||
|
<zipfileset src="${core.dir}/lib/jts-core-1.19.0.jar"/>
|
||||||
<zipfileset src="${lib.dir}/rsyntaxtextarea-3.2.0.jar"/>
|
<zipfileset src="${lib.dir}/rsyntaxtextarea-3.2.0.jar"/>
|
||||||
|
|
||||||
<!-- JOGL libraries need to be jar-in-jar -->
|
<!-- JOGL libraries need to be jar-in-jar -->
|
||||||
|
Loading…
x
Reference in New Issue
Block a user