Merge pull request #2458 from SiboVG/issue-2444
[#2444] Add Constrained Delaungay Triangulation for OBJ exporting
This commit is contained in:
commit
19cafe9eac
@ -38,5 +38,6 @@
|
||||
<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/js-scriptengine-22.1.0.1.jar"/>
|
||||
<classpathentry kind="lib" path="lib/jts-core-1.19.0.jar"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
</classpath>
|
||||
|
@ -307,6 +307,15 @@
|
||||
<SOURCES />
|
||||
</library>
|
||||
</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">
|
||||
<library>
|
||||
<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.
@ -1556,6 +1556,8 @@ OBJOptionChooser.checkbox.removeOffset.ttip = <html>If true, remove the offset o
|
||||
OBJOptionChooser.btn.showAdvanced = Show Advanced options
|
||||
OBJOptionChooser.checkbox.triangulate = Triangulate mesh
|
||||
OBJOptionChooser.checkbox.triangulate.ttip = If true, triangulate the mesh before exporting (convert all quads or high-order polygons to a triangle).
|
||||
OBJOptionChooser.lbl.triangulationMethod = Triangulation method:
|
||||
OBJOptionChooser.lbl.triangulationMethod.ttip = Select the desired algorithm to use for the triangulation.
|
||||
OBJOptionChooser.checkbox.sRGB = Export colors in sRGB
|
||||
OBJOptionChooser.checkbox.sRGB.ttip = <html>If true, export colors in sRGB instead of a linear color scheme.<br>Is useful for instance when exporting for use in Blender.</html>
|
||||
OBJOptionChooser.lbl.Scaling = Scaling:
|
||||
@ -1574,6 +1576,12 @@ LevelOfDetail.LOW_QUALITY = Low quality
|
||||
LevelOfDetail.NORMAL_QUALITY = Normal quality
|
||||
LevelOfDetail.HIGH_QUALITY = High quality
|
||||
|
||||
! TriangulationMethod
|
||||
TriangulationMethod.SIMPLE = Simple (fast)
|
||||
TriangulationMethod.SIMPLE.ttip = Simple triangulation method (fast, but may produce poor results)
|
||||
TriangulationMethod.DELAUNAY = Delaunay (recommended)
|
||||
TriangulationMethod.DELAUNAY.ttip = Constrained Delaunay triangulation method (recommended, but may be slow for large models)
|
||||
|
||||
! ThrustCurveMotorSelectionPanel
|
||||
TCMotorSelPan.lbl.Selrocketmotor = Select rocket motor:
|
||||
TCMotorSelPan.checkbox.hideSimilar = Hide very similar thrust curves
|
||||
|
@ -218,6 +218,10 @@ public final class DefaultObj implements Obj {
|
||||
return faces.size();
|
||||
}
|
||||
|
||||
public List<ObjFace> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 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];
|
||||
|
@ -71,10 +71,36 @@ public final class DefaultObjGroup implements ObjGroup {
|
||||
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() {
|
||||
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();
|
||||
|
@ -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.
|
||||
@ -71,6 +76,43 @@ public class ObjUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public enum TriangulationMethod {
|
||||
SIMPLE(trans.get("TriangulationMethod.SIMPLE"), trans.get("TriangulationMethod.SIMPLE.ttip"), "SIMPLE"),
|
||||
DELAUNAY(trans.get("TriangulationMethod.DELAUNAY"), trans.get("TriangulationMethod.DELAUNAY.ttip"), "DELAUNAY");
|
||||
|
||||
private final String label;
|
||||
private final String tooltip;
|
||||
private final String exportLabel;
|
||||
|
||||
TriangulationMethod(String label, String tooltip, String exportLabel) {
|
||||
this.label = label;
|
||||
this.tooltip = tooltip;
|
||||
this.exportLabel = exportLabel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public String getTooltip() {
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
public String getExportLabel() {
|
||||
return exportLabel;
|
||||
}
|
||||
|
||||
public static TriangulationMethod fromExportLabel(String exportLabel) {
|
||||
for (TriangulationMethod tm : TriangulationMethod.values()) {
|
||||
if (tm.getExportLabel().equals(exportLabel)) {
|
||||
return tm;
|
||||
}
|
||||
}
|
||||
return TriangulationMethod.DELAUNAY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Offset the indices by the given offset
|
||||
@ -335,6 +377,80 @@ public class ObjUtils {
|
||||
return normalizeVector(new DefaultFloatTuple(x, y, z));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the normal vector of a triangle defined by three vertices.
|
||||
*
|
||||
* @param v1 The first vertex of the triangle.
|
||||
* @param v2 The second vertex of the triangle.
|
||||
* @param v3 The third vertex of the triangle.
|
||||
* @return The normal vector of the triangle.
|
||||
*/
|
||||
public static FloatTuple calculateNormalVector(FloatTuple v1, FloatTuple v2, FloatTuple v3) {
|
||||
FloatTuple u = subtractVectors(v2, v1);
|
||||
FloatTuple v = subtractVectors(v3, v1);
|
||||
|
||||
return normalizeVector(crossProduct(u, v));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the normal vector for a given face of the object.
|
||||
*
|
||||
* @param obj The object.
|
||||
* @param face The face of the object for which to calculate the normal vector.
|
||||
* @return The calculated normal vector.
|
||||
*/
|
||||
public static FloatTuple calculateNormalVector(DefaultObj obj, DefaultObjFace face) {
|
||||
FloatTuple[] vertices = getVertices(obj, face);
|
||||
return calculateNormalNewell(vertices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the normal of a polygon using the Newell's method.
|
||||
*
|
||||
* @param vertices a list of vertices representing the polygon
|
||||
* @return the normalized normal vector of the polygon
|
||||
*/
|
||||
private static FloatTuple calculateNormalNewell(FloatTuple[] vertices) {
|
||||
float x = 0f;
|
||||
float y = 0f;
|
||||
float z = 0f;
|
||||
for (int i = 0; i < vertices.length; i++) {
|
||||
FloatTuple current = vertices[i];
|
||||
FloatTuple next = vertices[(i + 1) % vertices.length];
|
||||
|
||||
x += (current.getY() - next.getY()) * (current.getZ() + next.getZ());
|
||||
y += (current.getZ() - next.getZ()) * (current.getX() + next.getX());
|
||||
z += (current.getX() - next.getX()) * (current.getY() + next.getY());
|
||||
}
|
||||
return normalizeVector(new DefaultFloatTuple(x, y, z));
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtracts two vectors.
|
||||
*
|
||||
* @param v1 the first vector
|
||||
* @param v2 the second vector
|
||||
* @return a new FloatTuple representing the subtraction of v2 from v1
|
||||
*/
|
||||
public static FloatTuple subtractVectors(FloatTuple v1, FloatTuple v2) {
|
||||
return new DefaultFloatTuple(v1.getX() - v2.getX(), v1.getY() - v2.getY(), v1.getZ() - v2.getZ());
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the cross product of two vectors.
|
||||
*
|
||||
* @param v1 the first vector
|
||||
* @param v2 the second vector
|
||||
* @return the cross product of the given vectors
|
||||
*/
|
||||
public static FloatTuple crossProduct(FloatTuple v1, FloatTuple v2) {
|
||||
return new DefaultFloatTuple(
|
||||
v1.getY() * v2.getZ() - v1.getZ() * v2.getY(),
|
||||
v1.getZ() * v2.getX() - v1.getX() * v2.getZ(),
|
||||
v1.getX() * v2.getY() - v1.getY() * v2.getX()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the average of a list of vertices.
|
||||
* @param vertices The list of vertices
|
||||
@ -499,4 +615,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<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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,329 @@
|
||||
package net.sf.openrocket.file.wavefrontobj;
|
||||
|
||||
import de.javagl.obj.FloatTuple;
|
||||
import de.javagl.obj.ObjFace;
|
||||
import net.sf.openrocket.util.MathUtil;
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public abstract class TriangulationHelper {
|
||||
private static final Logger log = LoggerFactory.getLogger(TriangulationHelper.class);
|
||||
|
||||
public static DefaultObj simpleTriangulate(DefaultObj obj) {
|
||||
return de.javagl.obj.ObjUtils.triangulate(obj, new DefaultObj());
|
||||
}
|
||||
|
||||
/**
|
||||
* Triangulates an OBJ object using constrained Delaunay triangulation.
|
||||
*
|
||||
* @param input The object to triangulate.
|
||||
* @return A new object with the triangulated faces.
|
||||
*/
|
||||
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;
|
||||
} else if (face.getNumVertices() < 3) {
|
||||
log.debug("Face has less than 3 vertices, skipping");
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates constrained Delaunay triangulation faces based on a given object and face.
|
||||
*
|
||||
* @param obj The input object containing 3D polygon data.
|
||||
* @param face The specific face of the object to be triangulated.
|
||||
* @return A list of generated object faces representing the triangulated faces.
|
||||
*/
|
||||
public static List<ObjFace> generateCDTFaces(DefaultObj obj, DefaultObjFace face) {
|
||||
// Retrieve the vertex mapping to normal indices and texture coordinate indices
|
||||
Map<Integer, Integer> vertexToNormalMap = mapVertexIndicesToNormalIndices(face);
|
||||
Map<Integer, Integer> vertexToTexCoordMap = mapVertexIndicesToTexCoordIndices(face);
|
||||
|
||||
// Calculate the face normal
|
||||
Coordinate normal = vertexToCoordinate(ObjUtils.calculateNormalVector(obj, face));
|
||||
|
||||
// Project the 3D face to a 2D polygon with only X and Y coordinates.
|
||||
// This is necessary because the JTS library only works with 2D polygons for triangulation.
|
||||
PolygonWithOriginalIndices polygonWithIndices = createProjectedPolygon(obj, face, normal);
|
||||
Polygon polygon = polygonWithIndices.polygon();
|
||||
Map<Coordinate3D, Integer> vertexIndexMap = polygonWithIndices.vertexIndexMap();
|
||||
|
||||
// Triangulate the polygon
|
||||
ConstrainedDelaunayTriangulator triangulator = new ConstrainedDelaunayTriangulator(polygon);
|
||||
List<Tri> triangles = triangulator.getTriangles();
|
||||
|
||||
// Create the new faces to add to the OBJ
|
||||
List<ObjFace> newFaces = new ArrayList<>();
|
||||
for (Tri tri : triangles) {
|
||||
// Map the 2D triangle vertices back to the original vertex indices
|
||||
int[] vertexIndices = new int[3];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
Coordinate coord = tri.getCoordinate(i);
|
||||
vertexIndices[i] = getVertexIndexFromCoord(vertexIndexMap, coord);
|
||||
}
|
||||
|
||||
// Calculate the normal of the triangle, and verify that it has the same orientation as the original face
|
||||
// If it does not, invert the vertex order to ensure the normal points in the same direction
|
||||
// This is necessary for correct face culling.
|
||||
Coordinate triangleNormal = calculateNormal(
|
||||
vertexToCoordinate(obj.getVertex(vertexIndices[0])),
|
||||
vertexToCoordinate(obj.getVertex(vertexIndices[1])),
|
||||
vertexToCoordinate(obj.getVertex(vertexIndices[2])));
|
||||
if (normalsHaveDifferentDirection(triangleNormal, normal)) {
|
||||
int temp = vertexIndices[0];
|
||||
vertexIndices[0] = vertexIndices[2];
|
||||
vertexIndices[2] = temp;
|
||||
}
|
||||
|
||||
// Add the new face to the list
|
||||
if (vertexIndices[0] != -1 && vertexIndices[1] != -1 && vertexIndices[2] != -1) {
|
||||
// Map the vertex indices to normal and texture coordinate indices
|
||||
int[] normalIndices = vertexToNormalMap != null ?
|
||||
new int[] {vertexToNormalMap.get(vertexIndices[0]), vertexToNormalMap.get(vertexIndices[1]), vertexToNormalMap.get(vertexIndices[2])}
|
||||
: null;
|
||||
int[] texCoordIndices = vertexToTexCoordMap != null ?
|
||||
new int[] {vertexToTexCoordMap.get(vertexIndices[0]), vertexToTexCoordMap.get(vertexIndices[1]), vertexToTexCoordMap.get(vertexIndices[2])}
|
||||
: null;
|
||||
|
||||
// Create and add the new face
|
||||
DefaultObjFace newFace = new DefaultObjFace(vertexIndices, texCoordIndices, normalIndices);
|
||||
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.
|
||||
* @param normal the normal of the face to determine its orientation in 3D space
|
||||
* @return A polygon in 2D space suitable for use with JTS.
|
||||
*/
|
||||
private static PolygonWithOriginalIndices createProjectedPolygon(DefaultObj obj, DefaultObjFace face, Coordinate normal) {
|
||||
// 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 = projectVertexOntoXYPlane(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));
|
||||
}
|
||||
|
||||
// Create the polygon
|
||||
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 projectVertexOntoXYPlane(Coordinate3D vertex, Coordinate normal) {
|
||||
// If the normal is a zero vector, the polygon is degenerate and cannot be projected
|
||||
if (MathUtil.equals(normal.x, 0) && MathUtil.equals(normal.y, 0) && MathUtil.equals(normal.z, 0)) {
|
||||
throw new IllegalArgumentException("Cannot project a degenerate polygon onto a 2D plane");
|
||||
}
|
||||
// If the normal is parallel to the Z-axis, the polygon is already 2D
|
||||
if (MathUtil.equals(normal.x, 0) && MathUtil.equals(normal.y, 0)) {
|
||||
return new Coordinate(vertex.coordinate().x, vertex.coordinate().y);
|
||||
}
|
||||
|
||||
Coordinate u = crossProduct(normal, new Coordinate(0, 0, 1));
|
||||
Coordinate w = crossProduct(normal, u);
|
||||
|
||||
double x2D = dotProduct(vertex.coordinate(), u);
|
||||
double y2D = dotProduct(vertex.coordinate(), w);
|
||||
|
||||
return new Coordinate(x2D, y2D);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the vertex indices of a face to the normal indices of the same face.
|
||||
*
|
||||
* @param face The face for which to map the vertex indices to normal indices.
|
||||
* @return A map that maps the vertex indices to the normal indices, or null if the face does not contain normal indices.
|
||||
*/
|
||||
private static Map<Integer, Integer> mapVertexIndicesToNormalIndices(DefaultObjFace face) {
|
||||
int[] normalIndices = face.getNormalIndices();
|
||||
if (normalIndices == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<Integer, Integer> vertexToNormalMap = new HashMap<>();
|
||||
int[] vertexIndices = face.getVertexIndices();
|
||||
for (int i = 0; i < vertexIndices.length; i++) {
|
||||
vertexToNormalMap.put(vertexIndices[i], normalIndices[i]);
|
||||
}
|
||||
return vertexToNormalMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps vertex indices to texture coordinate indices.
|
||||
*
|
||||
* @param face The face object containing the vertex and texture coordinate indices.
|
||||
* @return A map that maps vertex indices to texture coordinate indices, or null if the face does
|
||||
* not contain texture coordinate indices.
|
||||
*/
|
||||
private static Map<Integer, Integer> mapVertexIndicesToTexCoordIndices(DefaultObjFace face) {
|
||||
int[] texCoordIndices = face.getTexCoordIndices();
|
||||
if (texCoordIndices == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<Integer, Integer> vertexToTexCoordMap = new HashMap<>();
|
||||
int[] vertexIndices = face.getVertexIndices();
|
||||
for (int i = 0; i < vertexIndices.length; i++) {
|
||||
vertexToTexCoordMap.put(vertexIndices[i], texCoordIndices[i]);
|
||||
}
|
||||
return vertexToTexCoordMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a FloatTuple vertex to a Coordinate object.
|
||||
*
|
||||
* @param vertex The vertex to convert.
|
||||
* @return The converted Coordinate object.
|
||||
*/
|
||||
private static Coordinate vertexToCoordinate(FloatTuple vertex) {
|
||||
return new Coordinate(vertex.getX(), vertex.getY(), vertex.getZ());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the vertex index from the given coordinate in the vertex index map.
|
||||
*
|
||||
* @param vertexIndexMap The map containing the vertex coordinates and their corresponding indices.
|
||||
* @param coord The coordinate to retrieve the vertex index for.
|
||||
* @return The vertex index if the coordinate is found in the map, or -1 if not found.
|
||||
*/
|
||||
private static int getVertexIndexFromCoord(Map<Coordinate3D, Integer> vertexIndexMap, Coordinate coord) {
|
||||
for (Map.Entry<Coordinate3D, Integer> entry : vertexIndexMap.entrySet()) {
|
||||
Coordinate key = entry.getKey().coordinate();
|
||||
if (key.equals3D(coord)) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether two normals have different directions.
|
||||
*
|
||||
* @param normal1 The first normal.
|
||||
* @param normal2 The second normal.
|
||||
* @return true if the two normals have different directions, false otherwise.
|
||||
*/
|
||||
private static boolean normalsHaveDifferentDirection(Coordinate normal1, Coordinate normal2) {
|
||||
return dotProduct(normal1, normal2) < 0;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// ==================================== Basic Vector Math ====================================
|
||||
|
||||
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 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 = magnitude(vector);
|
||||
if (magnitude == 0) {
|
||||
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));
|
||||
}
|
||||
|
||||
// ==================================== Helper classes ====================================
|
||||
|
||||
// Helper class to wrap a Polygon and its original vertex indices
|
||||
private record PolygonWithOriginalIndices(Polygon polygon, Map<Coordinate3D, Integer> vertexIndexMap) { }
|
||||
|
||||
// Helper class to wrap Coordinate and override equals and hashCode to account for all 3 dimensions
|
||||
private record Coordinate3D(Coordinate 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -33,6 +33,10 @@ public class OBJExportOptions {
|
||||
* If true, triangulate all faces (convert quads and higher-order polygons to triangles)
|
||||
*/
|
||||
private boolean triangulate;
|
||||
/**
|
||||
* The method to use for triangulation.
|
||||
*/
|
||||
private ObjUtils.TriangulationMethod triangulationMethod;
|
||||
/**
|
||||
* If true, use sRGB colors instead of linear color space.
|
||||
*/
|
||||
@ -94,6 +98,14 @@ public class OBJExportOptions {
|
||||
this.triangulate = triangulate;
|
||||
}
|
||||
|
||||
public ObjUtils.TriangulationMethod getTriangulationMethod() {
|
||||
return triangulationMethod;
|
||||
}
|
||||
|
||||
public void setTriangulationMethod(ObjUtils.TriangulationMethod triangulationMethod) {
|
||||
this.triangulationMethod = triangulationMethod;
|
||||
}
|
||||
|
||||
public boolean isExportAppearance() {
|
||||
return exportAppearance;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import net.sf.openrocket.file.wavefrontobj.DefaultMtl;
|
||||
import net.sf.openrocket.file.wavefrontobj.DefaultMtlWriter;
|
||||
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
|
||||
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
|
||||
import net.sf.openrocket.file.wavefrontobj.TriangulationHelper;
|
||||
import net.sf.openrocket.file.wavefrontobj.export.components.BodyTubeExporter;
|
||||
import net.sf.openrocket.file.wavefrontobj.export.components.FinSetExporter;
|
||||
import net.sf.openrocket.file.wavefrontobj.export.components.LaunchLugExporter;
|
||||
@ -174,7 +175,14 @@ public class OBJExporterFactory {
|
||||
|
||||
// Triangulate mesh
|
||||
if (this.options.isTriangulate()) {
|
||||
obj = de.javagl.obj.ObjUtils.triangulate(obj, new DefaultObj());
|
||||
ObjUtils.TriangulationMethod triangulationMethod = this.options.getTriangulationMethod();
|
||||
if (triangulationMethod == ObjUtils.TriangulationMethod.DELAUNAY) {
|
||||
obj = TriangulationHelper.constrainedDelaunayTriangulate(obj);
|
||||
} else if (triangulationMethod == ObjUtils.TriangulationMethod.SIMPLE) {
|
||||
obj = TriangulationHelper.simpleTriangulate(obj);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported triangulation method: " + triangulationMethod);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove position offset
|
||||
|
@ -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;
|
||||
|
||||
|
@ -128,6 +128,7 @@ public abstract class Preferences implements ChangeSource {
|
||||
private static final String OBJ_EXPORT_AS_SEPARATE_FILES = "ExportAsSeparateFiles";
|
||||
private static final String OBJ_REMOVE_OFFSET = "RemoveOffset";
|
||||
private static final String OBJ_TRIANGULATE = "Triangulate";
|
||||
private static final String OBJ_TRIANGULATION_METHOD = "TriangulationMethod";
|
||||
private static final String OBJ_SRGB = "sRGB";
|
||||
private static final String OBJ_LOD = "LOD";
|
||||
private static final String OBJ_SCALING = "Scaling";
|
||||
@ -1051,6 +1052,7 @@ public abstract class Preferences implements ChangeSource {
|
||||
objExportOptionsNode.putBoolean(OBJ_EXPORT_AS_SEPARATE_FILES, options.isExportAsSeparateFiles());
|
||||
objExportOptionsNode.putBoolean(OBJ_REMOVE_OFFSET, options.isRemoveOffset());
|
||||
objExportOptionsNode.putBoolean(OBJ_TRIANGULATE, options.isTriangulate());
|
||||
objExportOptionsNode.put(OBJ_TRIANGULATION_METHOD, options.getTriangulationMethod().getExportLabel());
|
||||
objExportOptionsNode.putBoolean(OBJ_SRGB, options.isUseSRGB());
|
||||
|
||||
objExportOptionsNode.putFloat(OBJ_SCALING, options.getScaling());
|
||||
@ -1081,6 +1083,9 @@ public abstract class Preferences implements ChangeSource {
|
||||
options.setExportAsSeparateFiles(objExportOptionsNode.getBoolean(OBJ_EXPORT_AS_SEPARATE_FILES, false));
|
||||
options.setRemoveOffset(objExportOptionsNode.getBoolean(OBJ_REMOVE_OFFSET, true));
|
||||
options.setTriangulate(objExportOptionsNode.getBoolean(OBJ_TRIANGULATE, true));
|
||||
options.setTriangulationMethod(ObjUtils.TriangulationMethod.fromExportLabel(
|
||||
objExportOptionsNode.get(OBJ_TRIANGULATION_METHOD, ObjUtils.TriangulationMethod.DELAUNAY.getExportLabel())
|
||||
));
|
||||
options.setUseSRGB(objExportOptionsNode.getBoolean(OBJ_SRGB, false));
|
||||
|
||||
options.setScaling(objExportOptionsNode.getFloat(OBJ_SCALING, 1000));
|
||||
|
@ -175,6 +175,7 @@ public class OBJExporterFactoryTest {
|
||||
bodyTube.setFilled(true);
|
||||
|
||||
options.setTriangulate(true);
|
||||
options.setTriangulationMethod(ObjUtils.TriangulationMethod.DELAUNAY);
|
||||
options.setRemoveOffset(false);
|
||||
options.setExportAppearance(true);
|
||||
options.setScaling(1000);
|
||||
@ -193,6 +194,14 @@ public class OBJExporterFactoryTest {
|
||||
//// Just hope for no exceptions :)
|
||||
assertEquals(warnings.size(), 1);
|
||||
|
||||
// Test simple triangulation
|
||||
options.setTriangulationMethod(ObjUtils.TriangulationMethod.SIMPLE);
|
||||
|
||||
exporterFactory = new OBJExporterFactory(components, rocket.getSelectedConfiguration(), tempFile.toFile(), options, warnings);
|
||||
exporterFactory.doExport();
|
||||
//// Just hope for no exceptions :)
|
||||
assertEquals(warnings.size(), 1);
|
||||
|
||||
// Clean up
|
||||
Files.delete(tempFile);
|
||||
}
|
||||
|
@ -145,6 +145,7 @@
|
||||
<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/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"/>
|
||||
|
||||
<!-- JOGL libraries need to be jar-in-jar -->
|
||||
|
@ -15,11 +15,13 @@ import net.sf.openrocket.unit.UnitGroup;
|
||||
|
||||
import javax.swing.AbstractButton;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.DefaultListCellRenderer;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JList;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JSeparator;
|
||||
@ -29,6 +31,7 @@ import javax.swing.UIManager;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.Window;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
@ -53,6 +56,8 @@ public class OBJOptionChooser extends JPanel {
|
||||
private final JCheckBox exportAsSeparateFiles;
|
||||
private final JCheckBox removeOffset;
|
||||
private final JCheckBox triangulate;
|
||||
private final JLabel tmLabel;
|
||||
private final JComboBox<ObjUtils.TriangulationMethod> triangulationMethod;
|
||||
private final JCheckBox sRGB;
|
||||
private final JComboBox<ObjUtils.LevelOfDetail> LOD;
|
||||
private final DoubleModel scalingModel;
|
||||
@ -223,11 +228,15 @@ public class OBJOptionChooser extends JPanel {
|
||||
@Override
|
||||
public void itemStateChanged(ItemEvent e) {
|
||||
if (e.getStateChange() == ItemEvent.SELECTED) {
|
||||
tmLabel.setEnabled(true);
|
||||
triangulationMethod.setEnabled(true);
|
||||
// Disable the export appearance, it is not supported in combination with triangulate
|
||||
exportAppearance.setEnabled(false);
|
||||
exportAppearance.setSelected(false);
|
||||
exportAppearance.setToolTipText(trans.get("OBJOptionChooser.checkbox.exportAppearance.ttip.triangulate"));
|
||||
} else {
|
||||
tmLabel.setEnabled(false);
|
||||
triangulationMethod.setEnabled(false);
|
||||
// Re-enable
|
||||
exportAppearance.setEnabled(true);
|
||||
exportAppearance.setSelected(opts.isExportAppearance());
|
||||
@ -236,6 +245,17 @@ public class OBJOptionChooser extends JPanel {
|
||||
}
|
||||
});
|
||||
|
||||
//// Triangulation method
|
||||
this.tmLabel = new JLabel(trans.get("OBJOptionChooser.lbl.triangulationMethod"));
|
||||
this.tmLabel.setToolTipText(trans.get("OBJOptionChooser.lbl.triangulationMethod.ttip"));
|
||||
advancedOptionsPanel.add(this.tmLabel, "spanx, split 2");
|
||||
this.triangulationMethod = new JComboBox<>(ObjUtils.TriangulationMethod.values());
|
||||
this.triangulationMethod.setToolTipText(trans.get("OBJOptionChooser.lbl.triangulationMethod.ttip"));
|
||||
this.triangulationMethod.setRenderer(new TriangulationMethodRenderer());
|
||||
destroyTheMagic(triangulationMethod);
|
||||
addOptimizationListener(triangulationMethod);
|
||||
advancedOptionsPanel.add(triangulationMethod, "growx, wrap unrel");
|
||||
|
||||
//// Level of detail
|
||||
JLabel LODLabel = new JLabel(trans.get("OBJOptionChooser.lbl.LevelOfDetail"));
|
||||
LODLabel.setToolTipText(trans.get("OBJOptionChooser.lbl.LevelOfDetail.ttip"));
|
||||
@ -394,6 +414,7 @@ public class OBJOptionChooser extends JPanel {
|
||||
if (!opts.isTriangulate()) {
|
||||
this.exportAppearance.setSelected(opts.isExportAppearance());
|
||||
}
|
||||
this.triangulationMethod.setSelectedItem(opts.getTriangulationMethod());
|
||||
this.sRGB.setSelected(opts.isUseSRGB());
|
||||
|
||||
this.scalingModel.setValue(opts.getScaling());
|
||||
@ -422,6 +443,7 @@ public class OBJOptionChooser extends JPanel {
|
||||
opts.setExportAsSeparateFiles(exportAsSeparateFiles.isSelected());
|
||||
opts.setRemoveOffset(removeOffset.isSelected());
|
||||
opts.setTriangulate(triangulate.isSelected());
|
||||
opts.setTriangulationMethod((ObjUtils.TriangulationMethod) triangulationMethod.getSelectedItem());
|
||||
opts.setUseSRGB(sRGB.isSelected());
|
||||
opts.setScaling((float) scalingModel.getValue());
|
||||
opts.setLOD((ObjUtils.LevelOfDetail) LOD.getSelectedItem());
|
||||
@ -441,6 +463,7 @@ public class OBJOptionChooser extends JPanel {
|
||||
options.setRemoveOffset(true);
|
||||
options.setScaling(1000);
|
||||
options.setTriangulate(true);
|
||||
options.setTriangulationMethod(ObjUtils.TriangulationMethod.DELAUNAY);
|
||||
options.setLOD(ObjUtils.LevelOfDetail.HIGH_QUALITY);
|
||||
|
||||
loadOptions(options);
|
||||
@ -453,6 +476,7 @@ public class OBJOptionChooser extends JPanel {
|
||||
*/
|
||||
private boolean isOptimizedFor3DPrinting(OBJExportOptions options) {
|
||||
return !options.isExportMotors() && !options.isExportAppearance() && options.isTriangulate() &&
|
||||
options.getTriangulationMethod() == ObjUtils.TriangulationMethod.DELAUNAY &&
|
||||
options.getLOD() == ObjUtils.LevelOfDetail.HIGH_QUALITY && options.isRemoveOffset() && options.getScaling() == 1000;
|
||||
}
|
||||
|
||||
@ -581,6 +605,22 @@ public class OBJOptionChooser extends JPanel {
|
||||
}
|
||||
}
|
||||
|
||||
private static class TriangulationMethodRenderer extends DefaultListCellRenderer {
|
||||
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList list, Object value,
|
||||
int index, boolean isSelected, boolean cellHasFocus) {
|
||||
|
||||
JComponent comp = (JComponent) super.getListCellRendererComponent(list,
|
||||
value, index, isSelected, cellHasFocus);
|
||||
|
||||
if (index > -1 && value instanceof ObjUtils.TriangulationMethod) {
|
||||
list.setToolTipText(((ObjUtils.TriangulationMethod) value).getTooltip());
|
||||
}
|
||||
return comp;
|
||||
}
|
||||
}
|
||||
|
||||
/*private void coordTransComboAction(ItemEvent e, JComboBox<Axis> otherCombo) {
|
||||
if (e.getStateChange() != ItemEvent.SELECTED) {
|
||||
return;
|
||||
|
@ -80,6 +80,7 @@ public class AboutDialog extends JDialog {
|
||||
"Darklaf (dark theme)" + href("https://github.com/weisJ/darklaf", true, true) + "<br>" +
|
||||
"jSystemThemeDetector" + href("https://github.com/Dansoftowner/jSystemThemeDetector", true, true) + "<br>" +
|
||||
"Obj" + href("https://github.com/javagl/Obj", true, true) + "<br>" +
|
||||
"JTS" + href("https://github.com/locationtech/jts", true, true) + "<br>" +
|
||||
"<br>" +
|
||||
"<b>OpenRocket gratefully acknowledges our use of the following databases:</b><br>" +
|
||||
"<br>" +
|
||||
|
Loading…
x
Reference in New Issue
Block a user