[#604] Implement Wavefront OBJ 3D export (PHEW!)

This commit is contained in:
SiboVG 2023-08-04 11:20:08 +02:00
parent d2bb552807
commit c4315e83aa
35 changed files with 3951 additions and 23 deletions

View File

@ -53,7 +53,7 @@ OpenRocket needs help to become even better. Implementing features, writing docu
- Daniel Williams, pod support, maintainer
- Joe Pfeiffer (maintainer)
- Billy Olsen (maintainer)
- Sibo Van Gool (maintainer)
- Sibo Van Gool (3D OBJ export, maintainer)
- Neil Weinstock (tester, icons, forum support)
- H. Craig Miller (tester)

View File

@ -10,6 +10,7 @@
<libelement value="jar://$MODULE_DIR$/lib/aopalliance.jar!/" />
<libelement value="jar://$MODULE_DIR$/lib/slf4j-api-1.7.5.jar!/" />
<libelement value="jar://$MODULE_DIR$/lib/annotation-detector-3.0.2.jar!/" />
<libelement value="jar://$MODULE_DIR$/lib/obj-0.4.0.jar!/" />
<libelement value="jar://$MODULE_DIR$/../lib-test/hamcrest-core-2.2.jar!/" />
<libelement value="jar://$MODULE_DIR$/../lib-test/hamcrest-2.2.jar!/" />
<libelement value="jar://$MODULE_DIR$/../lib-test/jmock-2.12.0.jar!/" />
@ -224,6 +225,15 @@
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/lib/obj-0.4.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>

BIN
core/lib/obj-0.4.0.jar Normal file

Binary file not shown.

View File

@ -1554,6 +1554,8 @@ main.menu.file.exportAs.RockSim = RockSim (.rkt)
main.menu.file.exportAs.RockSim.desc = Export design to RockSim file
main.menu.file.exportAs.RASAero = RASAero II (.CDX1)
main.menu.file.exportAs.RASAero.desc = Export design to RASAero II file
main.menu.file.exportAs.WavefrontOBJ= Wavefront OBJ (.obj)
main.menu.file.exportAs.WavefrontOBJ.desc = Export the selected components to a Wavefront OBJ 3D file
main.menu.file.print = Print design info\u2026
main.menu.file.print.desc = Print design specifications, including the parts list and fin templates, print or save as PDF
main.menu.file.close = Close design

View File

@ -0,0 +1,246 @@
package net.sf.openrocket.file.wavefrontobj;
/*
* www.javagl.de - Obj
*
* Copyright (c) 2008-2015 Marco Hutter - http://www.javagl.de
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import de.javagl.obj.FloatTuple;
import java.util.Arrays;
/**
* Default implementation of a {@link FloatTuple}
*/
public final class DefaultFloatTuple implements FloatTuple {
/**
* The values of this tuple
*/
private final float[] values;
/**
* Creates a new DefaultFloatTuple with the given values
*
* @param values The values
*/
public DefaultFloatTuple(float[] values) {
this.values = values;
}
/**
* Creates a new DefaultFloatTuple with the given values
*
* @param x The x value
* @param y The y value
* @param z The z value
* @param w The w value
*/
DefaultFloatTuple(float x, float y, float z, float w) {
this(new float[]{x,y,z,w});
}
/**
* Creates a new DefaultFloatTuple with the given values
*
* @param x The x value
* @param y The y value
* @param z The z value
*/
DefaultFloatTuple(float x, float y, float z) {
this(new float[]{x,y,z});
}
/**
* Creates a new DefaultFloatTuple with the given values
*
* @param x The x value
* @param y The y value
*/
DefaultFloatTuple(float x, float y) {
this(new float[]{x,y});
}
/**
* Creates a new DefaultFloatTuple with the given value
*
* @param x The x value
*/
DefaultFloatTuple(float x) {
this(new float[]{x});
}
/**
* Copy constructor.
*
* @param other The other FloatTuple
*/
DefaultFloatTuple(FloatTuple other) {
this(getValues(other));
}
/**
* Returns the values of the given {@link FloatTuple} as an array
*
* @param f The {@link FloatTuple}
* @return The values
*/
private static float[] getValues(FloatTuple f) {
if (f instanceof DefaultFloatTuple)
{
DefaultFloatTuple other = (DefaultFloatTuple)f;
return other.values.clone();
}
float values[] = new float[f.getDimensions()];
for (int i=0; i<values.length; i++)
{
values[i] = f.get(i);
}
return values;
}
@Override
public float get(int index) {
return values[index];
}
@Override
public float getX() {
return values[0];
}
/**
* Set the given component of this tuple
*
* @param x The component to set
* @throws IndexOutOfBoundsException If this tuple has less than 1
* dimensions
*/
public void setX(float x) {
values[0] = x;
}
@Override
public float getY() {
return values[1];
}
/**
* Set the given component of this tuple
*
* @param y The component to set
* @throws IndexOutOfBoundsException If this tuple has less than 2
* dimensions
*/
public void setY(float y) {
values[1] = y;
}
@Override
public float getZ() {
return values[2];
}
/**
* Set the given component of this tuple
*
* @param z The component to set
* @throws IndexOutOfBoundsException If this tuple has less than 3
* dimensions
*/
public void setZ(float z) {
values[2] = z;
}
@Override
public float getW() {
return values[3];
}
/**
* Set the given component of this tuple
*
* @param w The component to set
* @throws IndexOutOfBoundsException If this tuple has less than 4
* dimensions
*/
void setW(float w) {
values[3] = w;
}
@Override
public int getDimensions() {
return values.length;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("(");
for (int i=0; i<getDimensions(); i++) {
sb.append(get(i));
if (i < getDimensions()-1) {
sb.append(",");
}
}
sb.append(")");
return sb.toString();
}
@Override
public int hashCode() {
return Arrays.hashCode(values);
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null) {
return false;
}
if (object instanceof DefaultFloatTuple) {
DefaultFloatTuple other = (DefaultFloatTuple)object;
return Arrays.equals(values, other.values);
}
if (object instanceof FloatTuple) {
FloatTuple other = (FloatTuple)object;
if (other.getDimensions() != getDimensions()) {
return false;
}
for (int i=0; i<getDimensions(); i++) {
if (get(i) != other.get(i)) {
return false;
}
}
return true;
}
return false;
}
}

View File

@ -0,0 +1,594 @@
package net.sf.openrocket.file.wavefrontobj;
/*
* www.javagl.de - Obj
*
* Copyright (c) 2008-2015 Marco Hutter - http://www.javagl.de
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import de.javagl.obj.FloatTuple;
import de.javagl.obj.Obj;
import de.javagl.obj.ObjFace;
import de.javagl.obj.ObjGroup;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Default implementation of an {@link Obj}
*/
public final class DefaultObj implements Obj {
/**
* The vertices in this Obj
*/
private final List<FloatTuple> vertices;
/**
* The texture coordinates in this Obj.
*/
private final List<FloatTuple> texCoords;
/**
* The normals in this Obj
*/
private final List<FloatTuple> normals;
/**
* The faces in this Obj.
*/
private final List<ObjFace> faces;
/**
* The groups in this Obj.
*/
private final List<ObjGroup> groups;
/**
* The material groups in this Obj.
*/
private final List<ObjGroup> materialGroups;
/**
* Maps a group name to a group
*/
private final Map<String, DefaultObjGroup> groupMap;
/**
* Maps a material name to a material group
*/
private final Map<String, DefaultObjGroup> materialGroupMap;
/**
* The names of the MTL files for this Obj.
*/
private List<String> mtlFileNames = Collections.emptyList();
/**
* A map from the faces to the names of the groups that started
* at this face
*/
private final Map<ObjFace, Set<String>> startedGroupNames;
/**
* A map from the faces to the name of the material group that started
* at this face
*/
private final Map<ObjFace, String> startedMaterialGroupNames;
/**
* The names for the groups that should be used for faces that are
* added subsequently
*/
private Set<String> nextActiveGroupNames = null;
/**
* The name for the material group that should be used for faces that are
* added subsequently
*/
private String nextActiveMaterialGroupName = null;
/**
* The groups that are currently active, and to which faces will be
* added
*/
private List<DefaultObjGroup> activeGroups = null;
/**
* The names of the groups that faces are currently added to
*/
private Set<String> activeGroupNames = null;
/**
* The material group that is currently active, and to which faces will be
* added
*/
private DefaultObjGroup activeMaterialGroup = null;
/**
* The name of the material group that is currently active
*/
private String activeMaterialGroupName = null;
private final FloatTupleBounds vertexBounds;
/**
* Creates a new, empty DefaultObj.
*/
public DefaultObj() {
vertices = new ArrayList<>();
normals = new ArrayList<>();
texCoords = new ArrayList<>();
faces = new ArrayList<>();
groups = new ArrayList<>();
materialGroups = new ArrayList<>();
groupMap = new LinkedHashMap<>();
materialGroupMap = new LinkedHashMap<>();
startedGroupNames = new HashMap<>();
startedMaterialGroupNames = new HashMap<>();
vertexBounds = new FloatTupleBounds();
setActiveGroupNames(Arrays.asList("default"));
getGroupInternal("default");
}
@Override
public int getNumVertices() {
return vertices.size();
}
@Override
public FloatTuple getVertex(int index) {
return vertices.get(index);
}
/**
* Return a list of vertices from a list of vertex indices.
* @param indices List of vertex indices
* @return List of vertices
*/
public List<FloatTuple> getVertices(List<Integer> indices) {
List<FloatTuple> vertices = new ArrayList<>();
for (Integer vertexIndex : indices) {
vertices.add(getVertex(vertexIndex));
}
return vertices;
}
@Override
public int getNumTexCoords() {
return texCoords.size();
}
@Override
public FloatTuple getTexCoord(int index) {
return texCoords.get(index);
}
@Override
public int getNumNormals() {
return normals.size();
}
@Override
public FloatTuple getNormal(int index) {
return normals.get(index);
}
@Override
public int getNumFaces() {
return faces.size();
}
@Override
public ObjFace getFace(int index) {
return faces.get(index);
}
@Override
public Set<String> getActivatedGroupNames(ObjFace face) {
return startedGroupNames.get(face);
}
@Override
public String getActivatedMaterialGroupName(ObjFace face) {
return startedMaterialGroupNames.get(face);
}
public List<ObjGroup> getGroups() {
return groups;
}
@Override
public int getNumGroups() {
return groups.size();
}
@Override
public ObjGroup getGroup(int index) {
return groups.get(index);
}
@Override
public ObjGroup getGroup(String name) {
return groupMap.get(name);
}
/**
* Returns the {@link DefaultObjGroup} with the given name. If no such group exists in this object,
* create a new one, add it to this object, and return it.
* @param groupName The group name
* @return The {@link DefaultObjGroup}
*/
public DefaultObjGroup getGroupIfExists(String groupName) {
DefaultObjGroup group = groupMap.get(groupName);
if (group == null) {
group = new DefaultObjGroup(groupName);
groupMap.put(groupName, group);
groups.add(group);
}
return group;
}
public void addGroup(ObjGroup group) {
groups.add(group);
}
@Override
public int getNumMaterialGroups() {
return materialGroups.size();
}
@Override
public ObjGroup getMaterialGroup(int index) {
return materialGroups.get(index);
}
@Override
public ObjGroup getMaterialGroup(String name) {
return materialGroupMap.get(name);
}
@Override
public List<String> getMtlFileNames() {
return mtlFileNames;
}
/**
* Adds a vertex to this object. You can specify whether the added vertex should affect the objects bounds.
* @param vertex The vertex to add
* @param updateBounds Whether the added vertex should affect the objects bounds
*/
public void addVertex(FloatTuple vertex, boolean updateBounds) {
Objects.requireNonNull(vertex, "The vertex is null");
vertices.add(vertex);
if (updateBounds) {
vertexBounds.updateBounds(vertex);
}
}
@Override
public void addVertex(FloatTuple vertex) {
addVertex(vertex, true);
}
/**
* Adds a vertex to this object. You can specify whether the added vertex should affect the objects bounds.
* @param x The x coordinate of the vertex
* @param y The y coordinate of the vertex
* @param z The z coordinate of the vertex
* @param updateBounds Whether the added vertex should affect the objects bounds
*/
public void addVertex(float x, float y, float z, boolean updateBounds) {
addVertex(new DefaultFloatTuple(x, y, z), updateBounds);
}
@Override
public void addVertex(float x, float y, float z) {
addVertex(x, y, z, true);
}
public void setVertex(int index, FloatTuple vertex) {
Objects.requireNonNull(vertex, "The vertex is null");
vertices.set(index, vertex);
// !! It could be that you're replacing the vertex that is the min or max. !!
// So, make sure to add a vertex by properly specifying whether it should affect the bounds.
vertexBounds.updateBounds(vertex);
}
@Override
public void addTexCoord(FloatTuple texCoord) {
Objects.requireNonNull(texCoord, "The texCoord is null");
texCoords.add(texCoord);
}
@Override
public void addTexCoord(float x) {
texCoords.add(new DefaultFloatTuple(x));
}
@Override
public void addTexCoord(float x, float y) {
texCoords.add(new DefaultFloatTuple(x, y));
}
@Override
public void addTexCoord(float x, float y, float z) {
texCoords.add(new DefaultFloatTuple(x, y, z));
}
/**
* Adds a normal to this object. The normal will be normalized.
* @param normal The normal
*/
@Override
public void addNormal(FloatTuple normal) {
Objects.requireNonNull(normal, "The normal is null");
FloatTuple normalized = ObjUtils.normalizeVector(normal);
normals.add(normalized);
}
@Override
public void addNormal(float x, float y, float z) {
normals.add(new DefaultFloatTuple(x, y, z));
}
/**
* Sets the normal at the given index. The normal will be normalized.
* @param index The index to set the normal at
* @param normal The normal to set
*/
public void setNormal(int index, FloatTuple normal) {
Objects.requireNonNull(normal, "The normal is null");
FloatTuple normalized = ObjUtils.normalizeVector(normal);
normals.set(index, normalized);
}
@Override
public void setActiveGroupNames(Collection<? extends String> groupNames) {
if (groupNames == null) {
return;
}
if (groupNames.isEmpty()) {
groupNames = List.of("default");
} else if (groupNames.contains(null)) {
throw new NullPointerException("The groupNames contains null");
}
nextActiveGroupNames =
Collections.unmodifiableSet(new LinkedHashSet<String>(groupNames));
}
public void setActiveGroupNames(String... groupNames) {
setActiveGroupNames(Arrays.asList(groupNames));
}
@Override
public void setActiveMaterialGroupName(String materialGroupName) {
if (materialGroupName == null) {
return;
}
nextActiveMaterialGroupName = materialGroupName;
}
@Override
public void addFace(ObjFace face) {
if (face == null) {
throw new NullPointerException("The face is null");
}
if (nextActiveGroupNames != null) {
activeGroups = getGroupsInternal(nextActiveGroupNames);
if (!nextActiveGroupNames.equals(activeGroupNames)) {
startedGroupNames.put(face, nextActiveGroupNames);
}
activeGroupNames = nextActiveGroupNames;
nextActiveGroupNames = null;
}
if (nextActiveMaterialGroupName != null) {
activeMaterialGroup =
getMaterialGroupInternal(nextActiveMaterialGroupName);
if (!nextActiveMaterialGroupName.equals(activeMaterialGroupName)) {
startedMaterialGroupNames.put(face, nextActiveMaterialGroupName);
}
activeMaterialGroupName = nextActiveMaterialGroupName;
nextActiveMaterialGroupName = null;
}
faces.add(face);
if (activeMaterialGroup != null) {
activeMaterialGroup.addFace(face);
}
for (DefaultObjGroup group : activeGroups) {
group.addFace(face);
}
}
@Override
public void addFace(int ... v) {
addFace(v, null, null);
}
@Override
public void addFaceWithTexCoords(int... v) {
addFace(v, v, null);
}
@Override
public void addFaceWithNormals(int... v) {
addFace(v, null, v);
}
@Override
public void addFaceWithAll(int... v) {
addFace(v, v, v);
}
@Override
public void addFace(int[] v, int[] vt, int[] vn) {
Objects.requireNonNull(v, "The vertex indices are null");
checkIndices(v, getNumVertices(), "Vertex");
checkIndices(vt, getNumTexCoords(), "TexCoord");
checkIndices(vn, getNumNormals(), "Normal");
DefaultObjFace face = new DefaultObjFace(v, vt, vn);
addFace(face);
}
@Override
public void setMtlFileNames(Collection<? extends String> mtlFileNames) {
this.mtlFileNames = Collections.unmodifiableList(
new ArrayList<String>(mtlFileNames));
}
@Override
public String toString() {
return "Obj[" +
"#vertices="+ vertices.size() + "," +
"#texCoords=" + texCoords.size() + "," +
"#normals=" + normals.size() + "," +
"#faces=" + faces.size() + "," +
"#groups=" + groups.size() + "," +
"#materialGroups=" + materialGroups.size() + "," +
"mtlFileNames=" + mtlFileNames + "]";
}
/**
* Returns a set containing all groups with the given names. If the
* groups with the given names do not exist, they are created and
* added to this Obj.
*
* @param groupNames The group names
* @return The groups
*/
private List<DefaultObjGroup> getGroupsInternal(Collection<? extends String> groupNames) {
List<DefaultObjGroup> groups = new ArrayList<>(groupNames.size());
for (String groupName : groupNames) {
DefaultObjGroup group = getGroupInternal(groupName);
groups.add(group);
}
return groups;
}
/**
* Returns the group with the given names. If the group with the given
* name does not exist, it is created and added to this Obj.
*
* @param groupName The group name
* @return The group
*/
private DefaultObjGroup getGroupInternal(String groupName) {
return getGroupIfExists(groupName);
}
/**
* Returns the material group with the given names. If the material group
* with the given name does not exist, it is created and added to this Obj.
*
* @param materialGroupName The material group name
* @return The material group
*/
private DefaultObjGroup getMaterialGroupInternal(String materialGroupName) {
DefaultObjGroup group = materialGroupMap.get(materialGroupName);
if (group == null) {
group = new DefaultObjGroup(materialGroupName);
materialGroupMap.put(materialGroupName, group);
materialGroups.add(group);
}
return group;
}
/**
* Returns the bounds (min and max vertex values) of the vertices of this Obj.
* @return The bounds of this object
*/
public FloatTupleBounds getVertexBounds() {
return vertexBounds;
}
/**
* Resets the bounds of the vertices of this Obj back to its default values.
*/
public void resetVertexBounds() {
vertexBounds.resetBounds();
}
/**
* Recalculates the bounds of all the vertices of this Obj.
* This can be calculation intensive, so it is recommended to not call this too often.
*/
public void recalculateAllVertexBounds() {
resetVertexBounds();
for (FloatTuple vertex : vertices) {
vertexBounds.updateBounds(vertex);
}
}
/**
* If the given indices are <code>null</code>, then this method will
* do nothing. Otherwise, it will check whether the given indices
* are valid, and throw an IllegalArgumentException if not. They
* are valid when they are all not negative, and all smaller than
* the given maximum.
*
* @param indices The indices
* @param max The maximum index, exclusive
* @param name The name of the index set
* @throws IllegalArgumentException If the given indices are not valid
*/
private static void checkIndices(int[] indices, int max, String name) {
if (indices == null) {
return;
}
for (int i=0; i<indices.length; i++) {
if (indices[i] < 0) {
throw new IllegalArgumentException(
name+" index is negative: "+indices[i]);
}
if (indices[i] >= max) {
throw new IllegalArgumentException(
name+" index is "+indices[i]+
", but must be smaller than "+max);
}
}
}
}

View File

@ -0,0 +1,149 @@
package net.sf.openrocket.file.wavefrontobj;
/*
* www.javagl.de - Obj
*
* Copyright (c) 2008-2015 Marco Hutter - http://www.javagl.de
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import de.javagl.obj.ObjFace;
/**
* Default implementation of an ObjFace
*/
public final class DefaultObjFace implements ObjFace {
/**
* The vertex indices of this face
*/
private final int[] vertexIndices;
/**
* The texture coordinate indices of this face
*/
private final int[] texCoordIndices;
/**
* The normal indices of this face
*/
private final int[] normalIndices;
/**
* Creates a face from the given parameters. References to the
* given objects will be stored.
*
* @param vertexIndices The vertex indices
* @param texCoordIndices The texture coordinate indices
* @param normalIndices The normal indices
*/
public DefaultObjFace(int[] vertexIndices, int[] texCoordIndices, int[] normalIndices) {
this.vertexIndices = vertexIndices;
this.texCoordIndices = texCoordIndices;
this.normalIndices = normalIndices;
}
@Override
public boolean containsTexCoordIndices() {
return texCoordIndices != null;
}
@Override
public boolean containsNormalIndices() {
return normalIndices != null;
}
@Override
public int getVertexIndex(int number) {
return this.vertexIndices[number];
}
@Override
public int getTexCoordIndex(int number) {
return this.texCoordIndices[number];
}
@Override
public int getNormalIndex(int number) {
return this.normalIndices[number];
}
/**
* Set the specified index to the given value
*
* @param n The index to set
* @param index The value of the index
*/
public void setVertexIndex(int n, int index) {
vertexIndices[n] = index;
}
/**
* Set the specified index to the given value
*
* @param n The index to set
* @param index The value of the index
*/
public void setNormalIndex(int n, int index) {
normalIndices[n] = index;
}
/**
* Set the specified index to the given value
*
* @param n The index to set
* @param index The value of the index
*/
public void setTexCoordIndex(int n, int index) {
texCoordIndices[n] = index;
}
@Override
public int getNumVertices() {
return this.vertexIndices.length;
}
@Override
public String toString() {
StringBuilder result = new StringBuilder("ObjFace[");
for(int i = 0; i < getNumVertices(); i++) {
result.append(vertexIndices[i]);
if( texCoordIndices != null || normalIndices != null) {
result.append("/");
}
if (texCoordIndices != null) {
result.append(texCoordIndices[i]);
}
if (normalIndices != null) {
result.append("/").append(normalIndices[i]);
}
if (i < getNumVertices() - 1) {
result.append(" ");
}
}
result.append("]");
return result.toString();
}
}

View File

@ -0,0 +1,98 @@
package net.sf.openrocket.file.wavefrontobj;
/*
* www.javagl.de - Obj
*
* Copyright (c) 2008-2015 Marco Hutter - http://www.javagl.de
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
import de.javagl.obj.ObjFace;
import de.javagl.obj.ObjGroup;
import java.util.ArrayList;
import java.util.List;
/**
* Default implementation of an ObjGroup
*/
public final class DefaultObjGroup implements ObjGroup {
/**
* The name of this group.
*/
private String name;
/**
* The faces in this group
*/
private List<ObjFace> faces;
/**
* Creates a new ObjGroup with the given name
*
* @param name The name of this ObjGroup
*/
public DefaultObjGroup(String name) {
this.name = name;
faces = new ArrayList<ObjFace>();
}
@Override
public String getName() {
return name;
}
/**
* Add the given face to this group
*
* @param face The face to add
*/
public void addFace(ObjFace face)
{
faces.add(face);
}
public List<ObjFace> getFaces() {
return faces;
}
@Override
public int getNumFaces()
{
return faces.size();
}
@Override
public ObjFace getFace(int index)
{
return faces.get(index);
}
@Override
public String toString()
{
return "ObjGroup[name=" + name + ",#faces=" + faces.size() + "]";
}
}

View File

@ -0,0 +1,44 @@
package net.sf.openrocket.file.wavefrontobj;
import de.javagl.obj.FloatTuple;
/**
* A class for storing the minimum and maximum float tuple values to keep track of the bounds of a model.
*/
public class FloatTupleBounds {
private FloatTuple min;
private FloatTuple max;
/**
* Default constructor. Initializes the bounds to the maximum and minimum values of a float.
*/
public FloatTupleBounds() {
min = new DefaultFloatTuple(Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE);
max = new DefaultFloatTuple(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE);
}
/**
* Updates the bounds to the values of the given tuple.
* @param tuple The tuple to update the bounds with.
*/
public void updateBounds(FloatTuple tuple) {
min = new DefaultFloatTuple(Math.min(min.getX(), tuple.getX()), Math.min(min.getY(), tuple.getY()), Math.min(min.getZ(), tuple.getZ()));
max = new DefaultFloatTuple(Math.max(max.getX(), tuple.getX()), Math.max(max.getY(), tuple.getY()), Math.max(max.getZ(), tuple.getZ()));
}
/**
* Resets the bounds to the maximum and minimum values of a float.
*/
public void resetBounds() {
min = new DefaultFloatTuple(Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE);
max = new DefaultFloatTuple(Float.MIN_VALUE, Float.MIN_VALUE, Float.MIN_VALUE);
}
public FloatTuple getMin() {
return min;
}
public FloatTuple getMax() {
return max;
}
}

View File

@ -0,0 +1,329 @@
package net.sf.openrocket.file.wavefrontobj;
import de.javagl.obj.FloatTuple;
import de.javagl.obj.Obj;
import de.javagl.obj.ObjFace;
import de.javagl.obj.ObjGroup;
import java.util.List;
/**
* Utility methods for working with {@link Obj} objects.
*
* @author Sibo Van Gool <sibo.vangool@hotmail.com>
*/
public class ObjUtils {
/**
* Level of detail to use for the export.
*/
public enum LevelOfDetail {
LOW_QUALITY(25),
NORMAL(60),
HIGH_QUALITY(100);
private final int value;
LevelOfDetail(int value) {
this.value = value;
}
public int getValue() {
return value;
}
/**
* Get a dynamic estimate of the number of sides (= vertices in a perfect circle) to be used for the given radius.
* @param radius The radius to estimate the number of sides for
* @return The number of sides to use for the given radius
*/
public int getNrOfSides(double radius) {
final int MIN_SIDES = 10;
final double refRadius = 0.05; // Reference radius for the number of sides (the "most common radius") <-- very arbitrary, oh well.
return Math.max(MIN_SIDES, (int) (0.75*value + (radius/refRadius * 0.25*value))); // Adjust if needed
}
}
/**
* Offset the indices by the given offset
* @param indices The indices to offset
* @param offset The offset to apply
*/
public static void offsetIndex(int[] indices, int offset) {
if (indices == null || indices.length == 0) {
return;
}
for (int i = 0; i < indices.length; i++) {
indices[i] += offset;
}
}
/**
* Reverse the winding of the indices if needed.
* @param indices The indices to reverse the winding of
* @param reverse Whether to reverse the winding or not
* @return The indices with the winding reversed if needed
*/
public static int[] reverseIndexWinding(int[] indices, boolean reverse) {
if (!reverse) {
return indices;
}
final int[] reversedIndices = new int[indices.length];
for (int i = 0; i < indices.length; i++) {
reversedIndices[i] = indices[indices.length - 1 - i];
}
return reversedIndices;
}
/**
* Translate the vertices of an object.
* <b>NOTE: this uses the Wavefront OBJ coordinate system</b>
* @param obj The object to translate the vertices of
* @param startIdx The starting vertex index to translate
* @param endIdx The ending vertex index to translate (inclusive)
* @param transX The translation in the x direction
* @param transY The translation in the y direction
* @param transZ The translation in the z direction
*/
public static void translateVertices(DefaultObj obj, int startIdx, int endIdx,
float transX, float transY, float transZ) {
verifyIndexRange(obj, startIdx, endIdx);
if (Float.compare(transX, 0) == 0 && Float.compare(transY, 0) == 0 && Float.compare(transZ, 0) == 0) {
return;
}
for (int i = startIdx; i <= endIdx; i++) {
FloatTuple vertex = obj.getVertex(i);
final float x = vertex.getX();
final float y = vertex.getY();
final float z = vertex.getZ();
FloatTuple translatedVertex = new DefaultFloatTuple(x + transX, y + transY, z + transZ);
obj.setVertex(i, translatedVertex);
}
}
/**
* Rotate the vertices of an object.
* <b>NOTE: this uses the Wavefront OBJ coordinate system</b>
* @param obj The object to rotate the vertices of
* @param verticesStartIdx The starting vertex index to rotate
* @param verticesEndIdx The ending vertex index to rotate (inclusive)
* @param normalsStartIdx The starting normal index to rotate
* @param normalsEndIdx The ending normal index to rotate (inclusive)
* @param rotX The rotation around the x axis in radians
* @param rotY The rotation around the y axis in radians
* @param rotZ The rotation around the z axis in radians
* @param origX The x coordinate of the origin of the rotation
* @param origY The y coordinate of the origin of the rotation
* @param origZ The z coordinate of the origin of the rotation
*/
public static void rotateVertices(DefaultObj obj, int verticesStartIdx, int verticesEndIdx,
int normalsStartIdx, int normalsEndIdx,
float rotX, float rotY, float rotZ,
float origX, float origY, float origZ) {
verifyIndexRange(obj, verticesStartIdx, verticesEndIdx);
if (Float.compare(rotX, 0) == 0 && Float.compare(rotY, 0) == 0 && Float.compare(rotZ, 0) == 0) {
return;
}
final float cosX = (float) Math.cos(rotX);
final float sinX = (float) Math.sin(rotX);
final float cosY = (float) Math.cos(rotY);
final float sinY = (float) Math.sin(rotY);
final float cosZ = (float) Math.cos(rotZ);
final float sinZ = (float) Math.sin(rotZ);
final float Axx = cosY * cosZ;
final float Axy = -cosY * sinZ;
final float Axz = sinY;
final float Ayx = sinX * sinY * cosZ + cosX * sinZ;
final float Ayy = -sinX * sinY * sinZ + cosX * cosZ;
final float Ayz = -sinX * cosY;
final float Azx = -cosX * sinY * cosZ + sinX * sinZ;
final float Azy = cosX * sinY * sinZ + sinX * cosZ;
final float Azz = cosX * cosY;
// Rotate the vertices
for (int i = verticesStartIdx; i <= verticesEndIdx; i++) {
// Get the vertex information
FloatTuple vertex = obj.getVertex(i);
final float x = vertex.getX() - origX;
final float y = vertex.getY() - origY;
final float z = vertex.getZ() - origZ;
// Apply rotation
float rotatedX = Axx * x + Axy * y + Axz * z;
float rotatedY = Ayx * x + Ayy * y + Ayz * z;
float rotatedZ = Azx * x + Azy * y + Azz * z;
// Translate the point back to its original position
rotatedX += origX;
rotatedY += origY;
rotatedZ += origZ;
FloatTuple rotatedVertex = new DefaultFloatTuple(rotatedX, rotatedY, rotatedZ);
obj.setVertex(i, rotatedVertex);
}
// Rotate the normals
for (int i = normalsStartIdx; i <= normalsEndIdx; i++) {
// We don't need to consider the rotation origin for normals, since they are unit vectors
FloatTuple normal = obj.getNormal(i);
final float x = normal.getX();
final float y = normal.getY();
final float z = normal.getZ();
float newX = Axx * x + Axy * y + Axz * z;
float newY = Ayx * x + Ayy * y + Ayz * z;
float newZ = Azx * x + Azy * y + Azz * z;
FloatTuple rotatedNormal = new DefaultFloatTuple(newX, newY, newZ);
rotatedNormal = normalizeVector(rotatedNormal);
obj.setNormal(i, rotatedNormal);
}
}
private static void verifyIndexRange(DefaultObj obj, int startIdx, int endIdx) {
if (startIdx < 0 || startIdx >= obj.getNumVertices()) {
throw new IllegalArgumentException("startIdx must be between 0 and the number of vertices");
}
if (endIdx < 0 || endIdx >= obj.getNumVertices()) {
throw new IllegalArgumentException("endIdx must be between 0 and the number of vertices");
}
if (startIdx > endIdx) {
throw new IllegalArgumentException("startIdx must be less than or equal to endIdx");
}
}
/**
* Normalizes a vector so that its length is 1.
* If all components of the vector are 0, then the vector is returned unchanged.
* @param vector The vector to normalize
* @return The normalized vector
*/
public static FloatTuple normalizeVector(FloatTuple vector) {
final float x = vector.getX();
final float y = vector.getY();
final float z = vector.getZ();
float length = (float)Math.sqrt(x * x + y * y + z * z);
if (length == 0) {
return vector;
}
final float newX = x / length;
final float newY = y / length;
final float newZ = z / length;
return new DefaultFloatTuple(newX, newY, newZ);
}
/**
* Normalizes a vector so that its length is 1.
* @param x The x component of the vector
* @param y The y component of the vector
* @param z The z component of the vector
* @return The normalized vector
*/
public static FloatTuple normalizeVector(float x, float y, float z) {
return normalizeVector(new DefaultFloatTuple(x, y, z));
}
/**
* Calculate the average of a list of vertices.
* @param vertices The list of vertices
* @return The average of the vertices
*/
public static FloatTuple averageVertices(List<FloatTuple> vertices) {
float x = 0;
float y = 0;
float z = 0;
for (FloatTuple vertex : vertices) {
x += vertex.getX();
y += vertex.getY();
z += vertex.getZ();
}
x /= vertices.size();
y /= vertices.size();
z /= vertices.size();
return new DefaultFloatTuple(x, y, z);
}
/**
* Calculate the average of a list of vertices.
* @param obj The obj file to get the vertices from
* @param vertexIndices The indices of the vertices to average
* @return The average of the vertices
*/
public static FloatTuple averageVertices(DefaultObj obj, List<Integer> vertexIndices) {
List<FloatTuple> vertices = obj.getVertices(vertexIndices);
return averageVertices(vertices);
}
/**
* Removes the positional offset of the vertices in the obj file.
* This is useful for making sure that the object is centered at the origin. By default, every component is exported
* in the position that it is in the rocket. This may not be useful for individual components.
* @param obj The obj file to remove the offset from
*/
public static void removeVertexOffset(DefaultObj obj) {
final FloatTupleBounds bounds = obj.getVertexBounds();
final FloatTuple min = bounds.getMin();
final FloatTuple max = bounds.getMax();
final float minX = min.getX();
final float minY = min.getY();
final float minZ = min.getZ();
final float maxX = max.getX();
final float maxZ = max.getZ();
final float offsetX = -(maxX + minX) / 2;
final float offsetY = -minY; // We want the bottom of the object to be at y=0
final float offsetZ = -(maxZ + minZ) / 2;
for (int i = 0; i < obj.getNumVertices(); i++) {
FloatTuple vertex = obj.getVertex(i);
final float x = vertex.getX() + offsetX;
final float y = vertex.getY() + offsetY;
final float z = vertex.getZ() + offsetZ;
obj.setVertex(i, new DefaultFloatTuple(x, y, z));
}
}
/**
* Merge a list of objs into a single obj.
* @param objs The objs to merge
* @return The merged obj
*/
public static Obj merge(List<DefaultObj> objs) {
DefaultObj merged = new DefaultObj();
for (DefaultObj obj : objs) {
for (int i = 0; i < obj.getNumVertices(); i++) {
FloatTuple vertex = obj.getVertex(i);
merged.addVertex(vertex.get(0), vertex.get(1), vertex.get(2));
}
for (ObjGroup group : obj.getGroups()) {
if (!(group instanceof DefaultObjGroup)) {
throw new RuntimeException("Expected DefaultObjGroup");
}
DefaultObjGroup newGroup = new DefaultObjGroup(group.getName());
for (ObjFace face : ((DefaultObjGroup) group).getFaces()) {
newGroup.addFace(face);
}
merged.addGroup(newGroup);
}
}
return merged;
}
}

View File

@ -0,0 +1,146 @@
package net.sf.openrocket.file.wavefrontobj.export;
import de.javagl.obj.ObjWriter;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
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;
import net.sf.openrocket.file.wavefrontobj.export.components.MassObjectExporter;
import net.sf.openrocket.file.wavefrontobj.export.components.RadiusRingComponentExporter;
import net.sf.openrocket.file.wavefrontobj.export.components.RailButtonExporter;
import net.sf.openrocket.file.wavefrontobj.export.components.RocketComponentExporter;
import net.sf.openrocket.file.wavefrontobj.export.components.ThicknessRingComponentExporter;
import net.sf.openrocket.file.wavefrontobj.export.components.TransitionExporter;
import net.sf.openrocket.file.wavefrontobj.export.components.TubeFinSetExporter;
import net.sf.openrocket.rocketcomponent.BodyTube;
import net.sf.openrocket.rocketcomponent.ComponentAssembly;
import net.sf.openrocket.rocketcomponent.FinSet;
import net.sf.openrocket.rocketcomponent.LaunchLug;
import net.sf.openrocket.rocketcomponent.MassObject;
import net.sf.openrocket.rocketcomponent.RadiusRingComponent;
import net.sf.openrocket.rocketcomponent.RailButton;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.rocketcomponent.ThicknessRingComponent;
import net.sf.openrocket.rocketcomponent.Transition;
import net.sf.openrocket.rocketcomponent.TubeFinSet;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class OBJExporterFactory {
private final List<RocketComponent> components;
private final boolean exportChildren;
private final boolean triangulate;
private final boolean removeOffset;
private final ObjUtils.LevelOfDetail LOD; // Level of detailed used for the export
private final String filePath;
/**
* Exports a list of rocket components to a Wavefront OBJ file.
* <b>NOTE: </b> you must call {@link #doExport()} to actually perform the export.
* @param components List of components to export
* @param exportChildren If true, export all children of the components as well
* @param triangulate If true, triangulate all faces
* @param removeOffset If true, remove the offset of the object so it is centered at the origin (but the bottom of the object is at y=0)
* @param LOD Level of detail to use for the export (e.g. '80')
* @param filePath Path to the file to export to
*/
public OBJExporterFactory(List<RocketComponent> components, boolean exportChildren, boolean triangulate,
boolean removeOffset, ObjUtils.LevelOfDetail LOD, String filePath) {
this.components = components;
this.exportChildren = exportChildren;
this.triangulate = triangulate;
this.removeOffset = removeOffset;
this.LOD = LOD;
this.filePath = filePath;
}
/**
* Wavefront OBJ exporter.
* @param components List of components to export
* @param exportChildren If true, export all children of the components as well
* @param triangulate If true, triangulate all faces
* @param removeOffset If true, remove the offset of the object so it is centered at the origin (but the bottom of the object is at y=0)
* @param filePath Path to the file to export to
*/
public OBJExporterFactory(List<RocketComponent> components, boolean exportChildren, boolean triangulate,
boolean removeOffset, String filePath) {
this(components, exportChildren, triangulate, removeOffset, ObjUtils.LevelOfDetail.NORMAL, filePath);
}
/**
* Performs the actual exporting.
*/
public void doExport() {
DefaultObj obj = new DefaultObj();
Set<RocketComponent> componentsToExport = new HashSet<>(this.components);
if (this.exportChildren) {
for (RocketComponent component : this.components) {
componentsToExport.addAll(component.getAllChildren());
}
}
int idx = 1;
for (RocketComponent component : componentsToExport) {
final RocketComponentExporter exporter;
String groupName = component.getName() + "_" + idx; // Add index to make the name unique
if (component instanceof BodyTube) {
exporter = new BodyTubeExporter(obj, (BodyTube) component, groupName, this.LOD);
} else if (component instanceof Transition) {
exporter = new TransitionExporter(obj, (Transition) component, groupName, this.LOD);
}else if (component instanceof LaunchLug) {
exporter = new LaunchLugExporter(obj, (LaunchLug) component, groupName, this.LOD);
} else if (component instanceof TubeFinSet) {
exporter = new TubeFinSetExporter(obj, (TubeFinSet) component, groupName, this.LOD);
} else if (component instanceof FinSet) {
exporter = new FinSetExporter(obj, (FinSet) component, groupName, this.LOD);
} else if (component instanceof ThicknessRingComponent) {
exporter = new ThicknessRingComponentExporter(obj, (ThicknessRingComponent) component, groupName, this.LOD);
} else if (component instanceof RadiusRingComponent) {
exporter = new RadiusRingComponentExporter(obj, (RadiusRingComponent) component, groupName, this.LOD);
} else if (component instanceof MassObject) {
exporter = new MassObjectExporter(obj, (MassObject) component, groupName, this.LOD);
} else if (component instanceof RailButton) {
exporter = new RailButtonExporter(obj, (RailButton) component, groupName, this.LOD);
} else if (component instanceof ComponentAssembly) {
// Do nothing, component assembly instances are handled by the individual rocket component exporters
// by using getComponentLocations()
continue;
} else {
throw new IllegalArgumentException("Unknown component type");
}
exporter.addToObj();
idx++;
}
if (this.triangulate) {
obj = de.javagl.obj.ObjUtils.triangulate(obj, new DefaultObj());
}
if (this.removeOffset) {
// Because of some rotation and translation operations when creating the meshes, the bounds can be inaccurate.
// Therefore, we will recalculate them to be sure.
// Is a bit computationally expensive, but it's the only way to be sure...
obj.recalculateAllVertexBounds();
ObjUtils.removeVertexOffset(obj);
}
try (OutputStream objOutputStream = new FileOutputStream(this.filePath)) {
ObjWriter.write(obj, objOutputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,55 @@
package net.sf.openrocket.file.wavefrontobj.export.components;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import net.sf.openrocket.file.wavefrontobj.export.shapes.CylinderExporter;
import net.sf.openrocket.file.wavefrontobj.export.shapes.TubeExporter;
import net.sf.openrocket.rocketcomponent.BodyTube;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.util.Coordinate;
public class BodyTubeExporter extends RocketComponentExporter {
public BodyTubeExporter(DefaultObj obj, BodyTube component, String groupName, ObjUtils.LevelOfDetail LOD) {
super(obj, component, groupName, LOD);
}
@Override
public void addToObj() {
final BodyTube bodyTube = (BodyTube) component;
final float outerRadius = (float) bodyTube.getOuterRadius();
final float innerRadius = (float) bodyTube.getInnerRadius();
final float length = (float) bodyTube.getLength();
final boolean isFilled = bodyTube.isFilled();
final double rocketLength = bodyTube.getRocket().getLength();
final Coordinate[] locations = bodyTube.getComponentLocations();
// Generate the mesh
for (Coordinate location : locations) {
generateMesh(outerRadius, innerRadius, length, isFilled, rocketLength, location);
}
}
private void generateMesh(float outerRadius, float innerRadius, float length, boolean isFilled,
double rocketLength, Coordinate location) {
int startIdx = obj.getNumVertices();
if (isFilled || Float.compare(innerRadius, 0) == 0) {
CylinderExporter.addCylinderMesh(obj, groupName, outerRadius, length, true, LOD);
} else {
if (Float.compare(innerRadius, outerRadius) == 0) {
CylinderExporter.addCylinderMesh(obj, groupName, outerRadius, length, false, LOD);
} else {
TubeExporter.addTubeMesh(obj, groupName, outerRadius, innerRadius, length, LOD);
}
}
int endIdx = Math.max(obj.getNumVertices() - 1, startIdx); // Clamp in case no vertices were added
// Translate the mesh
final float x = (float) location.y;
final float y = (float) (rocketLength - length - location.x);
final float z = (float) location.z;
ObjUtils.translateVertices(obj, startIdx, endIdx, x, y, z);
}
}

View File

@ -0,0 +1,126 @@
package net.sf.openrocket.file.wavefrontobj.export.components;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import net.sf.openrocket.file.wavefrontobj.export.shapes.PolygonExporter;
import net.sf.openrocket.rocketcomponent.FinSet;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.rocketcomponent.position.AxialMethod;
import net.sf.openrocket.util.ArrayUtils;
import net.sf.openrocket.util.Coordinate;
import java.util.ArrayList;
import java.util.List;
public class FinSetExporter extends RocketComponentExporter {
public FinSetExporter(DefaultObj obj, FinSet component, String groupName, ObjUtils.LevelOfDetail LOD) {
super(obj, component, groupName, LOD);
}
@Override
public void addToObj() {
final FinSet finSet = (FinSet) component;
obj.setActiveGroupNames(groupName);
final Coordinate[] points = finSet.getFinPointsWithRoot();
final Coordinate[] tabPoints = finSet.getTabPoints();
final Coordinate[] tabPointsReversed = new Coordinate[tabPoints.length];
for (int i = 0; i < tabPoints.length; i++) {
tabPointsReversed[i] = tabPoints[tabPoints.length - i - 1];
}
final FloatPoints floatPoints = getPointsAsFloat(points);
final FloatPoints floatTabPoints = getPointsAsFloat(tabPointsReversed);
final float thickness = (float) finSet.getThickness();
final double rocketLength = finSet.getRocket().getLength();
boolean hasTabs = finSet.getTabLength() > 0 && finSet.getTabHeight() > 0;
final Coordinate[] locations = finSet.getComponentLocations();
final double[] angles = finSet.getComponentAngles();
if (locations.length != angles.length) {
throw new IllegalArgumentException("Number of locations and angles must match");
}
// Generate the fin meshes
for (int i = 0; i < locations.length; i++) {
generateMesh(finSet,floatPoints, floatTabPoints, thickness, hasTabs, rocketLength, locations[i], angles[i]);
}
}
private void generateMesh(FinSet finSet, FloatPoints floatPoints, FloatPoints floatTabPoints, float thickness,
boolean hasTabs, double rocketLength, Coordinate location, double angle) {
// Generate the mesh
final int startIdx = obj.getNumVertices();
final int normalsStartIdx = obj.getNumNormals();
// Generate the instance mesh
PolygonExporter.addPolygonMesh(obj, null,
floatPoints.getXCoords(), floatPoints.getYCoords(), thickness);
// Generate the fin tabs
final int tabStartIdx = obj.getNumVertices();
if (hasTabs) {
PolygonExporter.addPolygonMesh(obj, null,
floatTabPoints.getXCoords(), floatTabPoints.getYCoords(), thickness);
}
int endIdx = Math.max(obj.getNumVertices() - 1, startIdx); // Clamp in case no vertices were added
int normalsEndIdx = Math.max(obj.getNumNormals() - 1, normalsStartIdx); // Clamp in case no normals were added
// First rotate the fin for a correct orientation
final float axialRot = (float) angle;
final float cantAngle = (float) finSet.getCantAngle();
ObjUtils.rotateVertices(obj, startIdx, endIdx, normalsStartIdx, normalsEndIdx,
-(float) Math.PI/2, cantAngle, axialRot, 0, 0, 0);
// Translate the mesh
final float x = (float) location.y;
final float y = (float) (rocketLength - location.x);
final float z = (float) location.z;
ObjUtils.translateVertices(obj, startIdx, endIdx, x, y, z);
// Offset the tabs
if (hasTabs) {
float yTab = - (float) finSet.getTabPosition(AxialMethod.TOP);
ObjUtils.translateVertices(obj, tabStartIdx, endIdx, 0, yTab, 0);
}
}
/**
* Converts the double fin points to float fin points and removes any duplicate points (OBJ can't handle this).
* @param points The fin points
* @return The fin points as floats
*/
private FloatPoints getPointsAsFloat(Coordinate[] points) {
// We first want to remove duplicate points, so we'll keep track of indices that are correct
List<Integer> indices = new ArrayList<>();
for (int i = 0; i < points.length; i++) {
final int nextIdx = (i + 1) % points.length;
if (!points[i].equals(points[nextIdx])) {
indices.add(i);
}
}
final int targetLength = indices.size();
final float[] xCoords = new float[targetLength];
final float[] yCoords = new float[targetLength];
// Fill the arrays with the x and y values of each coordinate
for (int i = 0; i < targetLength; i++) {
xCoords[i] = (float) points[indices.get(i)].x;
yCoords[i] = (float) points[indices.get(i)].y;
}
return new FloatPoints(xCoords, yCoords);
}
private record FloatPoints(float[] xCoords, float[] yCoords) {
public float[] getXCoords() {
return xCoords;
}
public float[] getYCoords() {
return yCoords;
}
}
}

View File

@ -0,0 +1,54 @@
package net.sf.openrocket.file.wavefrontobj.export.components;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import net.sf.openrocket.file.wavefrontobj.export.shapes.CylinderExporter;
import net.sf.openrocket.file.wavefrontobj.export.shapes.TubeExporter;
import net.sf.openrocket.rocketcomponent.LaunchLug;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.util.Coordinate;
public class LaunchLugExporter extends RocketComponentExporter {
public LaunchLugExporter(DefaultObj obj, LaunchLug component, String groupName, ObjUtils.LevelOfDetail LOD) {
super(obj, component, groupName, LOD);
}
@Override
public void addToObj() {
final LaunchLug lug = (LaunchLug) component;
final Coordinate[] locations = lug.getComponentLocations();
final double rocketLength = lug.getRocket().getLength();
final float outerRadius = (float) lug.getOuterRadius();
final float innerRadius = (float) lug.getInnerRadius();
final float length = (float) lug.getLength();
// Generate the mesh
for (Coordinate location : locations) {
generateMesh(lug, outerRadius, innerRadius, length, rocketLength, location);
}
}
private void generateMesh(LaunchLug lug, float outerRadius, float innerRadius, float length, double rocketLength, Coordinate location) {
int startIdx2 = obj.getNumVertices();
// Generate the instance mesh
if (Float.compare(innerRadius, 0) == 0) {
CylinderExporter.addCylinderMesh(obj, groupName, outerRadius, length, true, LOD);
} else {
if (Float.compare(innerRadius, outerRadius) == 0) {
CylinderExporter.addCylinderMesh(obj, groupName, outerRadius, length, false, LOD);
} else {
TubeExporter.addTubeMesh(obj, groupName, outerRadius, innerRadius, length, LOD);
}
}
int endIdx2 = Math.max(obj.getNumVertices() - 1, startIdx2); // Clamp in case no vertices were added
// Translate the lug instance
final float x = (float) location.y;
final float y = (float) (rocketLength - lug.getLength() - location.x);
final float z = (float) location.z;
ObjUtils.translateVertices(obj, startIdx2, endIdx2, x, y, z);
}
}

View File

@ -0,0 +1,145 @@
package net.sf.openrocket.file.wavefrontobj.export.components;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.DefaultObjFace;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import net.sf.openrocket.rocketcomponent.MassObject;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.RocketComponentUtils;
public class MassObjectExporter extends RocketComponentExporter {
public MassObjectExporter(DefaultObj obj, MassObject component, String groupName, ObjUtils.LevelOfDetail LOD) {
super(obj, component, groupName, LOD);
}
@Override
public void addToObj() {
final MassObject massObject = (MassObject) component;
obj.setActiveGroupNames(groupName);
final Coordinate[] locations = massObject.getComponentLocations();
final double rocketLength = massObject.getRocket().getLength();
final int numSides = LOD.getValue() / 2;
final int numStacks = LOD.getValue() / 2;
// Generate the mesh
for (Coordinate location : locations) {
generateMesh(massObject, numSides, numStacks, rocketLength, location);
}
}
private void generateMesh(MassObject massObject, int numSides, int numStacks, double rocketLength, Coordinate location) {
// Other meshes may have been added to the obj, so we need to keep track of the starting indices
int verticesStartIdx = obj.getNumVertices();
int normalsStartIdx = obj.getNumNormals();
double dy = massObject.getLength() / numStacks;
double da = 2.0f * Math.PI / numSides;
// Generate vertices and normals
for (int j = 0; j <= numStacks; j++) {
double y = j * dy;
if (j == 0 || j == numStacks) {
// Add a center vertex
obj.addVertex(0, (float) y, 0);
obj.addNormal(0, j == 0 ? -1 : 1, 0);
} else {
// Add a vertex for each side
for (int i = 0; i < numSides; i++) {
double angle = i * da;
double r = RocketComponentUtils.getMassObjectRadius(massObject, y);
float x = (float) (r * Math.cos(angle));
float z = (float) (r * Math.sin(angle));
obj.addVertex(x, (float) y, z);
// Add normals
if (Double.compare(r, massObject.getRadius()) == 0) {
obj.addNormal(x, 0, z);
} else {
final double yCenter;
if (j <= numStacks/2) {
yCenter = RocketComponentUtils.getMassObjectArcHeight(massObject);
} else {
yCenter = massObject.getLength() - RocketComponentUtils.getMassObjectArcHeight(massObject);
}
obj.addNormal(x, (float) (y - yCenter), z); // For smooth shading
}
}
}
}
int endIdx = Math.max(obj.getNumVertices() - 1, verticesStartIdx); // Clamp in case no vertices were added
// Create bottom tip faces
for (int i = 0; i < numSides; i++) {
int nextIdx = (i + 1) % numSides;
int[] vertexIndices = new int[] {
0, // Center vertex
1 + i,
1 + nextIdx,
};
int[] normalIndices = vertexIndices.clone(); // For a smooth surface, the vertex and normal indices are the same
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx); // Only do this after normals are added, since the vertex indices are used for normals
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
// Create normal side faces
for (int j = 0; j < numStacks-2; j++) {
for (int i = 0; i < numSides; i++) {
int nextIdx = (i + 1) % numSides;
int[] vertexIndices = new int[] {
1 + j * numSides + i,
1 + (j + 1) * numSides + i,
1 + (j + 1) * numSides + nextIdx,
1 + j * numSides + nextIdx
};
int[] normalIndices = vertexIndices.clone(); // For a smooth surface, the vertex and normal indices are the same
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx); // Only do this after normals are added, since the vertex indices are used for normals
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
}
// Create top tip faces
final int normalEndIdx = obj.getNumNormals() - 1;
for (int i = 0; i < numSides; i++) {
int nextIdx = (i + 1) % numSides;
int[] vertexIndices = new int[] {
endIdx, // Center vertex
endIdx - numSides + nextIdx,
endIdx - numSides + i,
};
int[] normalIndices = new int[] {
normalEndIdx, // Center vertex
normalEndIdx - numSides + nextIdx,
normalEndIdx - numSides + i,
};
// Don't offset! We reference from the last index
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
// Translate the mesh
final double radialPosition = massObject.getRadialPosition();
final double radialDirection = massObject.getRadialDirection();
final float x = (float) (location.y + radialPosition * Math.cos(radialDirection));
final float y = (float) (rocketLength - massObject.getLength() - location.x);
final float z = (float) (location.z + radialPosition * Math.sin(radialDirection));
ObjUtils.translateVertices(obj, verticesStartIdx, endIdx, x, y, z);
}
}

View File

@ -0,0 +1,53 @@
package net.sf.openrocket.file.wavefrontobj.export.components;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import net.sf.openrocket.file.wavefrontobj.export.shapes.CylinderExporter;
import net.sf.openrocket.file.wavefrontobj.export.shapes.TubeExporter;
import net.sf.openrocket.rocketcomponent.RadiusRingComponent;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.util.Coordinate;
public class RadiusRingComponentExporter extends RocketComponentExporter {
public RadiusRingComponentExporter(DefaultObj obj, RadiusRingComponent component, String groupName, ObjUtils.LevelOfDetail LOD) {
super(obj, component, groupName, LOD);
}
@Override
public void addToObj() {
final RadiusRingComponent radiusRing = (RadiusRingComponent) component;
float outerRadius = (float) radiusRing.getOuterRadius();
float innerRadius = (float) radiusRing.getInnerRadius();
float thickness = (float) radiusRing.getThickness();
final double rocketLength = radiusRing.getRocket().getLength();
final Coordinate[] locations = radiusRing.getComponentLocations();
// Generate the mesh
for (Coordinate location : locations) {
generateMesh(outerRadius, innerRadius, thickness, rocketLength, location);
}
}
private void generateMesh(float outerRadius, float innerRadius, float thickness, double rocketLength, Coordinate location) {
int startIdx = obj.getNumVertices();
if (Float.compare(innerRadius, 0) == 0) {
CylinderExporter.addCylinderMesh(obj, groupName, outerRadius, thickness, true, LOD);
} else {
if (Float.compare(innerRadius, outerRadius) == 0) {
CylinderExporter.addCylinderMesh(obj, groupName, outerRadius, thickness, false, LOD);
} else {
TubeExporter.addTubeMesh(obj, groupName, outerRadius, innerRadius, thickness, LOD);
}
}
int endIdx = Math.max(obj.getNumVertices() - 1, startIdx); // Clamp in case no vertices were added
// Translate the mesh
final float x = (float) location.y;
final float y = (float) (rocketLength - thickness - location.x);
final float z = (float) location.z;
ObjUtils.translateVertices(obj, startIdx, endIdx, x, y, z);
}
}

View File

@ -0,0 +1,214 @@
package net.sf.openrocket.file.wavefrontobj.export.components;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.DefaultObjFace;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import net.sf.openrocket.file.wavefrontobj.export.shapes.CylinderExporter;
import net.sf.openrocket.file.wavefrontobj.export.shapes.DiskExporter;
import net.sf.openrocket.rocketcomponent.RailButton;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.util.Coordinate;
import java.util.ArrayList;
import java.util.List;
public class RailButtonExporter extends RocketComponentExporter {
/**
* Wavefront OBJ exporter for a rail button.
*
* @param obj The OBJ to export to
* @param component The component to export
* @param groupName The name of the group to export to
* @param LOD Level of detail to use for the export (e.g. '80')
*/
public RailButtonExporter(DefaultObj obj, RailButton component, String groupName, ObjUtils.LevelOfDetail LOD) {
super(obj, component, groupName, LOD);
}
@Override
public void addToObj() {
final RailButton railButton = (RailButton) component;
obj.setActiveGroupNames(groupName);
final float outerRadius = (float) railButton.getOuterDiameter() / 2;
final float innerRadius = (float) railButton.getInnerDiameter() / 2;
final float baseHeight = (float) railButton.getBaseHeight();
final float innerHeight = (float) railButton.getInnerHeight();
final float flangeHeight = (float) railButton.getFlangeHeight();
final float screwHeight = (float) railButton.getScrewHeight();
final Coordinate[] locations = railButton.getComponentLocations();
final double[] angles = railButton.getInstanceAngles();
final double rocketLength = railButton.getRocket().getLength();
// Generate the mesh
for (int i = 0; i < locations.length; i++) {
generateMesh(outerRadius, innerRadius, baseHeight, innerHeight, flangeHeight, screwHeight,
rocketLength, locations[i], angles[i]);
}
}
private void generateMesh(float outerRadius, float innerRadius, float baseHeight, float innerHeight, float flangeHeight,
float screwHeight, double rocketLength, Coordinate location, double angle) {
final int vertexStartIdx = obj.getNumVertices();
final int normalStartIdx = obj.getNumNormals();
// Generate base cylinder
List<Integer> baseCylinderBottomVertices = new ArrayList<>();
List<Integer> baseCylinderTopVertices = new ArrayList<>();
CylinderExporter.addCylinderMesh(obj, null, outerRadius, baseHeight, false, LOD,
baseCylinderBottomVertices, baseCylinderTopVertices);
// Generate inner cylinder
int tmpStartIdx = obj.getNumVertices();
List<Integer> innerCylinderBottomVertices = new ArrayList<>();
List<Integer> innerCylinderTopVertices = new ArrayList<>();
CylinderExporter.addCylinderMesh(obj, null, innerRadius, innerHeight, false, LOD,
innerCylinderBottomVertices, innerCylinderTopVertices);
int tmpEndIdx = Math.max(obj.getNumVertices() - 1, tmpStartIdx);
ObjUtils.translateVertices(obj, tmpStartIdx, tmpEndIdx, 0, baseHeight, 0);
// Generate flange cylinder
tmpStartIdx = obj.getNumVertices();
List<Integer> flangeCylinderBottomVertices = new ArrayList<>();
List<Integer> flangeCylinderTopVertices = new ArrayList<>();
CylinderExporter.addCylinderMesh(obj, null, outerRadius, flangeHeight, false, LOD,
flangeCylinderBottomVertices, flangeCylinderTopVertices);
tmpEndIdx = Math.max(obj.getNumVertices() - 1, tmpStartIdx);
ObjUtils.translateVertices(obj, tmpStartIdx, tmpEndIdx, 0, baseHeight + innerHeight, 0);
// Generate base disk
DiskExporter.closeDiskMesh(obj, null, baseCylinderBottomVertices, true); // Not a top face, but otherwise the culling isn't right...
// Generate base inner disk
DiskExporter.closeDiskMesh(obj, null, baseCylinderTopVertices, innerCylinderBottomVertices, true);
// Generate flange inner disk
DiskExporter.closeDiskMesh(obj, null, innerCylinderTopVertices, flangeCylinderBottomVertices, true); // Not a top face, but otherwise the culling isn't right...
// Generate flange disk/screw
if (Float.compare(screwHeight, 0) == 0) {
DiskExporter.closeDiskMesh(obj, null, flangeCylinderTopVertices, false); // Not a bottom face, but otherwise the culling isn't right...
} else {
addScrew(obj, baseHeight, innerHeight, flangeHeight, outerRadius, screwHeight, LOD, flangeCylinderTopVertices);
}
final int vertexEndIdx = Math.max(obj.getNumVertices() - 1, vertexStartIdx);
final int normalEndIdx = Math.max(obj.getNumNormals() - 1, normalStartIdx);
// Rotate the mesh (also PI/2!)
final float rX = - (float) Math.PI / 2;
final float rY = (float) angle;
final float rZ = 0;
ObjUtils.rotateVertices(obj, vertexStartIdx, vertexEndIdx, normalStartIdx, normalEndIdx,
rX, rY, rZ, 0, 0, 0);
// Translate the mesh
final float x = (float) location.y;
final float y = (float) location.x;
final float z = (float) location.z;
ObjUtils.translateVertices(obj, vertexStartIdx, vertexEndIdx, x, y, z);
}
private void addScrew(DefaultObj obj, float baseHeight, float innerHeight, float flangeHeight, float outerRadius,
float screwHeight, ObjUtils.LevelOfDetail LOD, List<Integer> flangeCylinderTopVertices) {
final int nrOfStacks = LOD.getValue() / 10;
final int nrOfSlices = flangeCylinderTopVertices.size();
final int startIdx = obj.getNumVertices();
final int normalStartIdx = obj.getNumNormals();
final float buttonHeight = baseHeight + innerHeight + flangeHeight;
final float centerX = 0;
final float centerY = buttonHeight;
final float centerZ = 0;
// Generate the mesh vertices (no tip)
for (int i = 1; i <= nrOfStacks-1; i++) {
for (int j = 0; j < nrOfSlices; j++) {
final float theta = (float) (Math.PI / 2) - (i / (float) nrOfStacks) * (float) (Math.PI / 2); // Adjusted to start from π/2 and decrease
final float phi = (float) j / nrOfSlices * (float) (2.0 * Math.PI);
final float x = outerRadius * (float) Math.sin(theta) * (float) Math.cos(phi);
final float y = buttonHeight + (screwHeight * (float) Math.cos(theta)); // Now the y coordinate increases as theta decreases
final float z = outerRadius * (float) Math.sin(theta) * (float) Math.sin(phi);
obj.addVertex(x, y, z);
// Calculate the optimal normal
float nx = x - centerX;
float ny = y - centerY;
float nz = z - centerZ;
obj.addNormal(nx, ny, nz);
}
}
// Generate the tip vertex
obj.addVertex(0, buttonHeight+screwHeight, 0);
obj.addNormal(0, 1, 0);
// Generate the faces between the flange cylinder and the quad faces
for (int i = 0; i < nrOfSlices; i++) {
int nextIdx = (i+1) % nrOfSlices;
int[] vertexIndices = new int[]{
flangeCylinderTopVertices.get(i), // Bottom-left of quad
startIdx + i, // Top-left of quad
startIdx + nextIdx, // Top-right of quad
flangeCylinderTopVertices.get(nextIdx), // Bottom-right of quad
};
int[] normalIndices = new int[]{
flangeCylinderTopVertices.get(i), // Bottom-left of quad
normalStartIdx + i, // Top-left of quad
normalStartIdx + nextIdx, // Top-right of quad
flangeCylinderTopVertices.get(nextIdx), // Bottom-right of quad
};
// No need to offset! We already directly reference the indices
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
// Generate the quad mesh faces (no tip)
for (int i = 0; i <= nrOfStacks-2; i++) {
for (int j = 0; j < nrOfSlices; j++) {
int nextIdx = (j+1) % nrOfSlices;
int[] vertexIndices = new int[]{
i * nrOfSlices + j, // Bottom-left of quad outside vertex
(i+1) * nrOfSlices + j, // Top-left of quad outside vertex
(i+1) * nrOfSlices + nextIdx, // Top-right of quad inside vertex
i * nrOfSlices + nextIdx // Bottom-right of quad inside vertex
};
int[] normalIndices = vertexIndices.clone();
ObjUtils.offsetIndex(normalIndices, normalStartIdx);
ObjUtils.offsetIndex(vertexIndices, startIdx);
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
}
// Generate the tip faces
final int endIdx = Math.max(obj.getNumVertices() - 1, startIdx);
final int normalEndIdx = Math.max(obj.getNumNormals() - 1, normalStartIdx);
for (int i = 0; i < nrOfSlices; i++) {
int nextIdx = (i+1) % nrOfSlices;
int[] vertexIndices = new int[]{
endIdx, // Tip vertex
endIdx - nrOfSlices + nextIdx,
endIdx - nrOfSlices + i,
};
int[] normalIndices = new int[]{
normalEndIdx, // Tip normal
normalEndIdx - nrOfSlices + nextIdx,
normalEndIdx - nrOfSlices + i,
};
// Don't offset! We already reference from the end index
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
}
}

View File

@ -0,0 +1,35 @@
package net.sf.openrocket.file.wavefrontobj.export.components;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import net.sf.openrocket.file.wavefrontobj.export.OBJExporterFactory;
import net.sf.openrocket.rocketcomponent.RocketComponent;
/**
* Base class for a rocket component Wavefront OBJ exporter.
* This class generates OBJ data for a rocket component and adds it to the given OBJ.
*
* @author Sibo Van Gool <sibo.vangool@hotmail.com>
*/
public abstract class RocketComponentExporter {
protected final DefaultObj obj;
protected final RocketComponent component;
protected final String groupName;
protected final ObjUtils.LevelOfDetail LOD;
/**
* Wavefront OBJ exporter for a rocket component.
* @param obj The OBJ to export to
* @param component The component to export
* @param groupName The name of the group to export to
* @param LOD Level of detail to use for the export (e.g. '80')
*/
public RocketComponentExporter(DefaultObj obj, RocketComponent component, String groupName, ObjUtils.LevelOfDetail LOD) {
this.obj = obj;
this.component = component;
this.groupName = groupName;
this.LOD = LOD;
}
public abstract void addToObj();
}

View File

@ -0,0 +1,53 @@
package net.sf.openrocket.file.wavefrontobj.export.components;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import net.sf.openrocket.file.wavefrontobj.export.shapes.CylinderExporter;
import net.sf.openrocket.file.wavefrontobj.export.shapes.TubeExporter;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.rocketcomponent.ThicknessRingComponent;
import net.sf.openrocket.util.Coordinate;
public class ThicknessRingComponentExporter extends RocketComponentExporter {
public ThicknessRingComponentExporter(DefaultObj obj, ThicknessRingComponent component, String groupName, ObjUtils.LevelOfDetail LOD) {
super(obj, component, groupName, LOD);
}
@Override
public void addToObj() {
final ThicknessRingComponent thicknessRing = (ThicknessRingComponent) component;
final float outerRadius = (float) thicknessRing.getOuterRadius();
final float innerRadius = (float) thicknessRing.getInnerRadius();
final float length = (float) thicknessRing.getLength();
final double rocketLength = thicknessRing.getRocket().getLength();
final Coordinate[] locations = thicknessRing.getComponentLocations();
// Generate the mesh
for (Coordinate location : locations) {
generateMesh(outerRadius, innerRadius, length, rocketLength, location);
}
}
private void generateMesh(float outerRadius, float innerRadius, float length, double rocketLength, Coordinate location) {
int startIdx = obj.getNumVertices();
if (Float.compare(innerRadius, 0) == 0) {
CylinderExporter.addCylinderMesh(obj, groupName, outerRadius, length, true, LOD);
} else {
if (Float.compare(innerRadius, outerRadius) == 0) {
CylinderExporter.addCylinderMesh(obj, groupName, outerRadius, length, false, LOD);
} else {
TubeExporter.addTubeMesh(obj, groupName, outerRadius, innerRadius, length, LOD);
}
}
int endIdx = Math.max(obj.getNumVertices() - 1, startIdx); // Clamp in case no vertices were added
// Translate the mesh
final float x = (float) location.y;
final float y = (float) (rocketLength - length - location.x);
final float z = (float) location.z;
ObjUtils.translateVertices(obj, startIdx, endIdx, x, y, z);
}
}

View File

@ -0,0 +1,533 @@
package net.sf.openrocket.file.wavefrontobj.export.components;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.DefaultObjFace;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import net.sf.openrocket.file.wavefrontobj.export.shapes.CylinderExporter;
import net.sf.openrocket.file.wavefrontobj.export.shapes.DiskExporter;
import net.sf.openrocket.file.wavefrontobj.export.shapes.TubeExporter;
import net.sf.openrocket.rocketcomponent.Transition;
import net.sf.openrocket.util.Coordinate;
import java.util.ArrayList;
import java.util.List;
public class TransitionExporter extends RocketComponentExporter {
private static final double RADIUS_EPSILON = 1e-4;
private final int nrOfSides;
/**
* Exporter for transition components
* @param obj Wavefront OBJ file to export to
* @param component Component to export
* @param groupName Name of the group to export to
* @param LOD Level of detail to use for the export
*/
public TransitionExporter(DefaultObj obj, Transition component, String groupName, ObjUtils.LevelOfDetail LOD) {
super(obj, component, groupName, LOD);
this.nrOfSides = LOD.getNrOfSides(Math.max(component.getForeRadius(), component.getAftRadius()));
}
@Override
public void addToObj() {
final Transition transition = (Transition) component;
obj.setActiveGroupNames(groupName);
final Coordinate[] locations = transition.getComponentLocations();
final double rocketLength = transition.getRocket().getLength();
// Generate the mesh
for (Coordinate location : locations) {
generateMesh(transition, rocketLength, location);
}
}
private void generateMesh(Transition transition, double rocketLength, Coordinate location) {
int startIdx = obj.getNumVertices();
final boolean hasForeShoulder = Double.compare(transition.getForeShoulderRadius(), 0) > 0
&& Double.compare(transition.getForeShoulderLength(), 0) > 0
&& transition.getForeRadius() > 0;
final boolean hasAftShoulder = Double.compare(transition.getAftShoulderRadius(), 0) > 0
&& Double.compare(transition.getAftShoulderLength(), 0) > 0
&& transition.getAftRadius() > 0;
final List<Integer> outsideForeRingVertices = new ArrayList<>();
final List<Integer> outsideAftRingVertices = new ArrayList<>();
final List<Integer> insideForeRingVertices = new ArrayList<>();
final List<Integer> insideAftRingVertices = new ArrayList<>();
// Check if geometry is a simple cylinder
if (Double.compare(transition.getAftRadius(), transition.getForeRadius()) == 0 ||
transition.getShapeType() == Transition.Shape.CONICAL ||
(transition.getShapeType() == Transition.Shape.OGIVE && transition.getShapeParameter() == 0) ||
(transition.getShapeType() == Transition.Shape.POWER && transition.getShapeParameter() == 1) ||
(transition.getShapeType() == Transition.Shape.PARABOLIC && transition.getShapeParameter() == 0)) {
float outerAft = (float) transition.getAftRadius();
float innerAft = (float) (transition.getAftRadius() - transition.getThickness());
float outerFore = (float) transition.getForeRadius();
float innerFore = (float) (transition.getForeRadius() - transition.getThickness());
TubeExporter.addTubeMesh(obj, null, outerAft, outerFore, innerAft, innerFore,
(float) transition.getLength(), this.nrOfSides,
outsideAftRingVertices, outsideForeRingVertices, insideAftRingVertices, insideForeRingVertices);
}
// Otherwise, use complex geometry
else {
int numStacks = transition.getShapeType() == Transition.Shape.CONICAL ? 4 : this.nrOfSides / 2;
// Draw outside
addTransitionMesh(obj, transition, this.nrOfSides, numStacks, 0, true,
outsideForeRingVertices, outsideAftRingVertices, hasForeShoulder, hasAftShoulder);
// Draw inside
if (!transition.isFilled()) {
addTransitionMesh(obj, transition, this.nrOfSides, numStacks, -transition.getThickness(), false,
insideForeRingVertices, insideAftRingVertices, hasForeShoulder, hasAftShoulder);
}
// Draw bottom and top face
if (!hasForeShoulder) {
closeFace(obj, transition, outsideForeRingVertices, insideForeRingVertices, true);
}
if (!hasAftShoulder) {
closeFace(obj, transition, outsideAftRingVertices, insideAftRingVertices, false);
}
}
// Add shoulders
addShoulders(obj, transition, this.nrOfSides, outsideForeRingVertices, outsideAftRingVertices,
insideForeRingVertices, insideAftRingVertices, hasForeShoulder, hasAftShoulder);
int endIdx = Math.max(obj.getNumVertices() - 1, startIdx); // Clamp in case no vertices were added
// Translate the mesh
final float x = (float) location.y;
final float y = (float) (rocketLength - transition.getLength() - location.x);
final float z = (float) location.z;
ObjUtils.translateVertices(obj, startIdx, endIdx, x, y, z);
}
/**
* Add a transition mesh to the obj.
* @param obj the obj to add the mesh to
* @param transition the transition to draw
* @param numSlices the number of slices to use (= number of vertices in the circumferential direction)
* @param numStacks the number of stacks to use (= number of vertices in the longitudinal direction)
* @param offsetRadius offset radius from the transition radius
* @param isOutside true if the mesh is on the outside of the rocket, false if it is on the inside
* @param foreRingVertices list of vertices of the fore ring
* @param aftRingVertices list of vertices of the aft ring
*/
private static void addTransitionMesh(DefaultObj obj, Transition transition,
int numSlices, int numStacks, double offsetRadius, boolean isOutside,
List<Integer> foreRingVertices, List<Integer> aftRingVertices,
boolean hasForeShoulder, boolean hasAftShoulder) {
// Other meshes may have been added to the obj, so we need to keep track of the starting indices
final int verticesStartIdx = obj.getNumVertices();
final int normalsStartIdx = obj.getNumNormals();
final double dyBase = transition.getLength() / numStacks; // Base step size in the longitudinal direction
final double actualLength = estimateActualLength(transition, offsetRadius, dyBase);
// Generate vertices and normals
float y = 0; // Distance from the aft end
double r; // Current radius at height y
boolean isForeTip = false; // True if the fore end of the transition is a tip (radius = 0)
boolean isAftTip = false; // True if the aft end of the transition is a tip (radius = 0)
int actualNumStacks = 0; // Number of stacks added, deviates from set point due to reduced step size near the tip (does not include aft/fore tip rings)
while (y <= (float) transition.getLength()) {
// When closer to the tip, decrease the step size
double t = Math.min(1, y / actualLength);
if (transition.getForeRadius() < transition.getAftRadius()) {
t = 1 - t;
}
float dy = t < 0.2 ? (float) (dyBase / (5.0 - 20*t)) : (float) dyBase;
float yNext = y + dy;
yNext = Math.min(yNext, (float) transition.getLength());
// Calculate the radius at this height
r = Math.max(0, transition.getRadius(transition.getLength()-y) + offsetRadius); // y = 0 is aft and, but this would return the fore radius, so subtract it from the length
double rNext = Math.max(0, transition.getRadius(transition.getLength()-yNext) + offsetRadius);
/*
This is where things get a bit complex. Since transitions can have a fore and aft tip, we need to handle
this separately from the rest of the mesh. Normal stack rings will eventually be drawn using quads, but
transition tips will use an aft/fore tip vertex and then use triangles to connect the tip to the stack rings.
Additionally, we need to store the fore ring and aft ring vertices to eventually close the outside and inside
mesh. For this, we need to set isAftRing and isForeRing.
The following rules apply:
1. If actualNumStacks == 0, r == 0 and rNext == 0, skip this ring (you're at the aft end, before the aft tip)
2. If actualNumStacks == 0, r == 0 and rNext > 0, add a single vertex at the center (= aft tip)
3. If r > 0 and rNext == 0, set isForeRing = true & add a single vertex at the center at the next y position (= fore tip)
4. If actualNumStacks > 0, r == 0 and rNext == 0, break the loop (you're at the fore tip)
5. If r > 0, add normal vertices
*/
if (actualNumStacks == 0) {
if (Double.compare(r, 0) == 0) {
if (Double.compare(rNext, 0) == 0) {
// Case 1: Skip this ring (you're at the aft end, before the aft tip)
y = yNext;
continue;
} else {
// Case 2: Add a single vertex at the center (= aft tip)
float epsilon = dy / 20;
float yTip = getTipLocation(transition, y, yNext, offsetRadius, epsilon);
obj.addVertex(0, yTip, 0);
obj.addNormal(0, isOutside ? -1 : 1, 0);
isAftTip = true;
// The aft tip is the first aft "ring"
aftRingVertices.add(obj.getNumVertices() - 1);
}
}
} else if (actualNumStacks > 0 && Double.compare(r, 0) == 0 && Double.compare(rNext, 0) == 0) {
// Case 4: Break the loop (you're at the fore tip)
break;
}
if (Double.compare(r, RADIUS_EPSILON) > 0) { // Don't just compare to 0, otherwise you could still have really small rings that could be better replaced by a tip
// For the inside transition shape if we're in the shoulder base region, we need to skip adding rings,
// because this is where the shoulder base will be
float yClamped = y;
if (!isOutside) {
final double yForeShoulder = transition.getLength() - transition.getForeShoulderThickness();
final double yAftShoulder = transition.getAftShoulderThickness();
if (hasForeShoulder) {
if (y < yForeShoulder) {
// If the current ring is before the fore shoulder ring and the next ring is after, clamp the
// next ring to the fore shoulder ring
if (yNext > yForeShoulder) {
yNext = (float) yForeShoulder;
}
}
// Skip the ring
else if (y > yForeShoulder) {
continue;
}
} else if (hasAftShoulder && y < yAftShoulder) {
// The aft shoulder point is between this and the next ring, so clamp the y value to the shoulder height
// and add the ring
if (yNext > yAftShoulder) {
yClamped = (float) yAftShoulder;
}
// Skip the ring
else {
continue;
}
}
}
if (Double.compare(rNext, RADIUS_EPSILON) <= 0 && Double.compare(yClamped, transition.getLength()) != 0) {
// Case 3: Add a single vertex at the center at the next y position (= fore tip)
float epsilon = dy / 20;
float yTip = getTipLocation(transition, yClamped, yNext, offsetRadius, epsilon);
obj.addVertex(0, yTip, 0);
obj.addNormal(0, isOutside ? 1 : -1, 0);
isForeTip = true;
// The fore tip is the first fore "ring"
foreRingVertices.add(obj.getNumVertices() - 1);
break;
} else {
// Check on which ring we are
boolean isAftRing = actualNumStacks == 0 && aftRingVertices.isEmpty();
boolean isForeRing = Double.compare(yClamped, (float) transition.getLength()) == 0;
// Case 5: Add normal vertices
addQuadVertices(obj, numSlices, foreRingVertices, aftRingVertices, r, rNext, yClamped, isAftRing, isForeRing, isOutside);
actualNumStacks++;
}
}
// If we're at the fore end, stop
if (Float.compare(y, (float) transition.getLength()) == 0) {
break;
}
y = yNext;
}
// Create aft/fore tip faces
if (isAftTip || isForeTip) {
addTipFaces(obj, numSlices, isOutside, isAftTip, normalsStartIdx, verticesStartIdx);
}
// Create regular faces
int corrVStartIdx = isAftTip ? verticesStartIdx + 1 : verticesStartIdx;
int corrNStartIdx = isAftTip ? normalsStartIdx + 1 : normalsStartIdx;
addQuadFaces(obj, numSlices, actualNumStacks, corrVStartIdx, corrNStartIdx, isOutside);
}
private static void addTipFaces(DefaultObj obj, int numSlices, boolean isOutside, boolean isAftTip, int normalsStartIdx, int verticesStartIdx) {
final int lastIdx = obj.getNumVertices() - 1;
for (int i = 0; i < numSlices; i++) {
int nextIdx = (i + 1) % numSlices;
int[] vertexIndices;
int[] normalIndices;
// Aft tip
if (isAftTip) {
vertexIndices = new int[] {
0, // Aft tip vertex
1 + i,
1 + nextIdx
};
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, !isOutside);
normalIndices = vertexIndices.clone(); // No need to reverse, already done by vertices
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx); // Do this last, otherwise the normal indices will be wrong
}
// Fore tip
else {
vertexIndices = new int[] {
lastIdx, // Fore tip vertex
lastIdx - numSlices + nextIdx,
lastIdx - numSlices + i,
};
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, !isOutside);
int lastNormalIdx = obj.getNumNormals() - 1;
normalIndices = new int[] {
lastNormalIdx, // Fore tip vertex
lastNormalIdx - numSlices + nextIdx,
lastNormalIdx - numSlices + i,
};
normalIndices = ObjUtils.reverseIndexWinding(normalIndices, !isOutside);
// No need to offset the indices, because we reference the last vertex (this caused a lot of debugging frustration hmfmwl)
}
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
}
private static void addQuadVertices(DefaultObj obj, int numSlices, List<Integer> foreRingVertices, List<Integer> aftRingVertices,
double r, double rNext, float y, boolean isAftRing, boolean isForeRing, boolean isOutside) {
for (int i = 0; i < numSlices; i++) {
double angle = 2 * Math.PI * i / numSlices;
float x = (float) (r * Math.cos(angle));
float z = (float) (r * Math.sin(angle));
obj.addVertex(x, y, z);
// Add the ring vertices to the lists
if (isAftRing) {
aftRingVertices.add(obj.getNumVertices()-1);
}
if (isForeRing) {
foreRingVertices.add(obj.getNumVertices()-1);
}
// Calculate the normal
final float nx = isOutside ? x : -x;
final float ny = isOutside ? (float) (r - rNext) : (float) (rNext -r);
final float nz = isOutside ? z : -z;
obj.addNormal(nx, ny, nz);
}
}
private static void addQuadFaces(DefaultObj obj, int numSlices, int numStacks, int verticesStartIdx, int normalsStartIdx, boolean isOutside) {
for (int i = 0; i < numStacks - 1; i++) {
for (int j = 0; j < numSlices; j++) {
final int nextIdx = (j + 1) % numSlices;
int[] vertexIndices = new int[] {
i * numSlices + j, // Bottom-left of quad
(i + 1) * numSlices + j, // Top-left of quad
(i + 1) * numSlices + nextIdx, // Top-right of quad
i * numSlices + nextIdx, // Bottom-right of quad
};
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, !isOutside);
int[] normalIndices = vertexIndices.clone(); // No reversing needed, already done by vertices
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx); // Do this last, otherwise the normal indices will be wrong
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
}
}
private static void addShoulders(DefaultObj obj, Transition transition, int nrOfSides,
List<Integer> outsideForeRingVertices, List<Integer> outsideAftRingVertices,
List<Integer> insideForeRingVertices, List<Integer> insideAftRingVertices,
boolean hasForeShoulder, boolean hasAftShoulder) {
final float foreShoulderRadius = (float) transition.getForeShoulderRadius();
final float aftShoulderRadius = (float) transition.getAftShoulderRadius();
final float foreShoulderLength = (float) transition.getForeShoulderLength();
final float aftShoulderLength = (float) transition.getAftShoulderLength();
final float foreShoulderThickness = (float) transition.getForeShoulderThickness();
final float aftShoulderThickness = (float) transition.getAftShoulderThickness();
final boolean foreShoulderCapped = transition.isForeShoulderCapped();
final boolean aftShoulderCapped = transition.isAftShoulderCapped();
if (hasForeShoulder) {
addShoulder(obj, transition, foreShoulderRadius, foreShoulderLength, foreShoulderThickness, foreShoulderCapped,
true, nrOfSides, outsideForeRingVertices, insideForeRingVertices);
}
if (hasAftShoulder) {
addShoulder(obj, transition, aftShoulderRadius, aftShoulderLength, aftShoulderThickness, aftShoulderCapped,
false, nrOfSides, outsideAftRingVertices, insideAftRingVertices);
}
}
private static void addShoulder(DefaultObj obj, Transition transition, float shoulderRadius, float shoulderLength,
float shoulderThickness, boolean isCapped, boolean isForeSide, int nrOfSides,
List<Integer> outerRingVertices, List<Integer> innerRingVertices) {
final float innerCylinderRadius = isCapped ? 0 : shoulderRadius - shoulderThickness;
final List<Integer> outerCylinderBottomVertices = new ArrayList<>();
final List<Integer> outerCylinderTopVertices = new ArrayList<>();
final List<Integer> innerCylinderBottomVertices = isCapped ? null : new ArrayList<>();
final List<Integer> innerCylinderTopVertices = isCapped ? null : new ArrayList<>();
int startIdx;
int endIdx;
/*
Cross-section of a transition with aft shoulder:
UNCAPPED CAPPED
| | 2| | | | 2| |
| | | |1 | | | |1
| | | | | | | |
| |______ ____6_| | | |______________6_| |
|_______ |7 | _______| |_______ _______|
| | | | 5 | | 5
3|_| |_| 3|______|
4 4
1: transition inside
2: transition outside
3: shoulder outer open cylinder
4: shoulder top disk
5: transition outer disk
6: transition inner disk
7: shoulder inner open cylinder (only if uncapped)
*/
// Generate outer cylinder (no. 3)
startIdx = obj.getNumVertices();
CylinderExporter.addCylinderMesh(obj, null, shoulderRadius, shoulderLength,
false, nrOfSides, outerCylinderBottomVertices, outerCylinderTopVertices);
endIdx = Math.max(obj.getNumVertices() - 1, startIdx);
// Translate the outer cylinder to the correct position
float dy = isForeSide ? (float) transition.getLength() : -shoulderLength;
ObjUtils.translateVertices(obj, startIdx, endIdx, 0, dy, 0);
// Generate inner cylinder (no. 7)
if (!isCapped) {
startIdx = obj.getNumVertices();
CylinderExporter.addCylinderMesh(obj, null, innerCylinderRadius, shoulderLength + shoulderThickness,
false, false, nrOfSides, innerCylinderBottomVertices, innerCylinderTopVertices);
endIdx = Math.max(obj.getNumVertices() - 1, startIdx);
// Translate the outer cylinder to the correct position
dy = isForeSide ? (float) transition.getLength() - shoulderThickness : -shoulderLength;
ObjUtils.translateVertices(obj, startIdx, endIdx, 0, dy, 0);
}
// Generate shoulder top disk (no. 4)
if (isForeSide) {
DiskExporter.closeDiskMesh(obj, null, outerCylinderTopVertices, innerCylinderTopVertices, true);
} else {
DiskExporter.closeDiskMesh(obj, null, outerCylinderBottomVertices, innerCylinderBottomVertices, false);
}
// Generate transition outer disk (no. 5)
if (isForeSide) {
DiskExporter.closeDiskMesh(obj, null, outerRingVertices, outerCylinderBottomVertices, true);
} else {
DiskExporter.closeDiskMesh(obj, null, outerRingVertices, outerCylinderTopVertices, false);
}
// Generate transition inner disk (no. 6)
if (isForeSide) {
DiskExporter.closeDiskMesh(obj, null, innerRingVertices, innerCylinderBottomVertices, false);
} else {
DiskExporter.closeDiskMesh(obj, null, innerRingVertices, innerCylinderTopVertices, true);
}
}
private static void closeFace(DefaultObj obj, Transition transition, List<Integer> outerVertices, List<Integer> innerVertices,
boolean isTopFace) {
boolean filledCap = transition.isFilled() || innerVertices.size() <= 1;
DiskExporter.closeDiskMesh(obj, null, outerVertices, filledCap ? null : innerVertices, isTopFace);
}
/**
* Due to the offsetRadius, the length of the transition to be drawn can be smaller than the actual length of the transition,
* because the offsetRadius causes the mesh to "shrink". This method estimates the length of the transition to be drawn.
* @param transition the transition to estimate the length for
* @param offsetRadius the offset radius to the radius
* @param dyBase the base of the dy
* @return the estimated length of the transition to be drawn
*/
private static float estimateActualLength(Transition transition, double offsetRadius, double dyBase) {
if (Double.compare(offsetRadius, 0) >= 0) {
return (float) transition.getLength();
}
double y = 0;
final float increment = (float) dyBase / 4;
float actualLength = 0;
while (y < transition.getLength()) {
final double r = transition.getRadius(transition.getLength()-y) + offsetRadius;
if (Double.compare(r, 0) > 0) {
actualLength += increment;
}
y += increment;
}
return actualLength;
}
/**
* Locate the best location for the tip of a transition.
* @param transition the transition to look the tip for
* @param yStart the start position to look for
* @param yEnd the end position to look for
* @param offsetRadius the offset radius to the radius
* @param epsilon the increment to parse the next y location
* @return the best location for the tip
*/
private static float getTipLocation(Transition transition, float yStart, float yEnd, double offsetRadius, float epsilon) {
if (Float.compare(yStart, yEnd) == 0 || Float.compare(epsilon, 0) == 0) {
throw new IllegalArgumentException("Invalid parameters");
}
boolean isStartSmaller = transition.getRadius(transition.getLength()-yStart) < transition.getRadius(transition.getLength()-yEnd);
if (isStartSmaller) {
for (float y = yEnd; y >= yStart; y -= epsilon) {
double r = Math.max(0, transition.getRadius(transition.getLength() - y) + offsetRadius);
if (Double.compare(r, 0) == 0) {
return y;
}
}
return yStart;
} else {
for (float y = yStart; y <= yEnd; y += epsilon) {
double r = Math.max(0, transition.getRadius(transition.getLength() - y) + offsetRadius);
if (Double.compare(r, 0) == 0) {
return y;
}
}
return yEnd;
}
}
}

View File

@ -0,0 +1,56 @@
package net.sf.openrocket.file.wavefrontobj.export.components;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import net.sf.openrocket.file.wavefrontobj.export.shapes.PolygonExporter;
import net.sf.openrocket.file.wavefrontobj.export.shapes.TubeExporter;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.rocketcomponent.TubeFinSet;
import net.sf.openrocket.util.Coordinate;
public class TubeFinSetExporter extends RocketComponentExporter {
public TubeFinSetExporter(DefaultObj obj, TubeFinSet component, String groupName, ObjUtils.LevelOfDetail LOD) {
super(obj, component, groupName, LOD);
}
@Override
public void addToObj() {
final TubeFinSet tubeFinSet = (TubeFinSet) component;
obj.setActiveGroupNames(groupName);
final float outerRadius = (float) tubeFinSet.getOuterRadius();
final float innerRadius = (float) tubeFinSet.getInnerRadius();
final float length = (float) tubeFinSet.getLength();
final Coordinate[] locations = tubeFinSet.getComponentLocations();
final double[] angles = tubeFinSet.getInstanceAngles();
final double rocketLength = tubeFinSet.getRocket().getLength();
if (locations.length != angles.length) {
throw new IllegalArgumentException("Number of locations and angles must match");
}
// Generate the fin meshes
for (int i = 0; i < locations.length; i++) {
generateMesh(outerRadius, innerRadius, length, rocketLength, locations[i], angles[i]);
}
}
private void generateMesh(float outerRadius, float innerRadius, float length, double rocketLength, Coordinate location, double angle) {
// Create the fin meshes
final int startIdx = obj.getNumVertices();
// Generate the instance mesh
TubeExporter.addTubeMesh(obj, null, outerRadius, innerRadius, length, LOD.getValue());
int endIdx = Math.max(obj.getNumVertices() - 1, startIdx); // Clamp in case no vertices were added
// Translate the mesh
final float dx = outerRadius * (float) Math.cos(angle);
final float dz = outerRadius * (float) Math.sin(angle);
final float x = (float) location.y + dx;
final float y = (float) (rocketLength - length - location.x);
final float z = (float) location.z + dz;
ObjUtils.translateVertices(obj, startIdx, endIdx, x, y, z);
}
}

View File

@ -0,0 +1,182 @@
package net.sf.openrocket.file.wavefrontobj.export.shapes;
import de.javagl.obj.ObjWriter;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.DefaultObjFace;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.List;
public class CylinderExporter {
/**
* Adds a cylinder mesh to the given obj
* @param obj The obj to add the mesh to
* @param groupName The name of the group to add the mesh to, or null if no group should be added (use the active group)
* @param radius The radius of the cylinder
* @param height The height of the cylinder
* @param numSides The number of sides of the cylinder
* @param solid Whether the cylinder should be solid (true) or hollow (false)
* NOTE: Culling is not really thought of for the hollow cylinder; this mode is really meant to be
* combined with other objects
* @param isOutside Whether the cylinder is an outside face (true) or inside face (false)
* @param bottomRingVertices A list to add the bottom ring vertex indices to
* @param topRingVertices A list to add the top ring vertex indices to
*/
public static void addCylinderMesh(DefaultObj obj, String groupName,
float radius, float height, int numSides, boolean solid, boolean isOutside,
List<Integer> bottomRingVertices, List<Integer> topRingVertices) {
// Set the new group
if (groupName != null) {
obj.setActiveGroupNames(groupName);
}
// Other meshes may have been added to the obj, so we need to keep track of the starting indices
int verticesStartIdx = obj.getNumVertices();
int normalsStartIdx = obj.getNumNormals();
if (solid) {
// Bottom center vertex
obj.addVertex(0, 0, 0);
obj.addNormal(0, isOutside ? -1 : 1, 0);
// Top center vertex
obj.addVertex(0, height, 0);
obj.addNormal(0, isOutside ? 1 : -1, 0);
}
// Generate side bottom vertices
int tmpStartIdx = obj.getNumVertices();
for (int i = 0; i < numSides; i++) {
double angle = 2 * Math.PI * i / numSides;
float x = radius * (float) Math.cos(angle);
float z = radius * (float) Math.sin(angle);
obj.addVertex(x, 0, z);
final float nx = isOutside ? x : -x;
final float nz = isOutside ? z : -z;
obj.addNormal(nx, 0, nz); // This kind of normal ensures the object is smoothly rendered (like the 'Shade Smooth' option in Blender)
if (bottomRingVertices != null) {
bottomRingVertices.add(tmpStartIdx + i);
}
}
// Generate side top vertices
tmpStartIdx = obj.getNumVertices();
for (int i = 0; i < numSides; i++) {
double angle = 2 * Math.PI * i / numSides;
float x = radius * (float) Math.cos(angle);
float z = radius * (float) Math.sin(angle);
obj.addVertex(x, height, z);
final float nx = isOutside ? x : -x;
final float nz = isOutside ? z : -z;
obj.addNormal(nx, height, nz); // For smooth shading
if (topRingVertices != null) {
topRingVertices.add(tmpStartIdx + i);
}
}
// Create faces for the bottom and top
if (solid) {
for (int i = 0; i < numSides; i++) {
int nextIdx = (i + 1) % numSides;
// Bottom face
int[] vertexIndices = new int[] {
0, // Bottom center vertex
2 + i,
2 + nextIdx
};
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, !isOutside);
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx);
int[] normalIndices = new int[]{0, 0, 0};
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
// Top face
vertexIndices = new int[] {
1, // Top center vertex
2 + numSides + ((i + 1) % numSides),
2 + numSides + i
};
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, !isOutside);
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx);
normalIndices = new int[] {1, 1, 1};
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
}
// Create faces for the sides
for (int i = 0; i < numSides; i++) {
final int nextIdx = (i + 1) % numSides;
final int offset = solid ? 2 : 0; // Offset by 2 to skip the bottom and top center vertices
int[] vertexIndices = new int[]{
i, // Bottom-left of quad
numSides + i, // Top-left of quad
numSides + nextIdx, // Top-right of quad
nextIdx, // Bottom-right of quad
};
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, !isOutside);
int[] normalIndices = vertexIndices.clone(); // No need to reverse winding, already done by vertices
ObjUtils.offsetIndex(normalIndices, normalsStartIdx + offset);
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx + offset); // ! Only add offset here, otherwise you mess up the indices for the normals
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
}
public static void addCylinderMesh(DefaultObj obj, String groupName,
float radius, float height, int numSides, boolean solid,
List<Integer> bottomRingVertices, List<Integer> topRingVertices) {
addCylinderMesh(obj, groupName, radius, height, numSides, solid, true, bottomRingVertices, topRingVertices);
}
public static void addCylinderMesh(DefaultObj obj, String groupName, float radius, float height, boolean solid,
ObjUtils.LevelOfDetail LOD,
List<Integer> bottomRingVertices, List<Integer> topRingVertices) {
addCylinderMesh(obj, groupName, radius, height, LOD.getNrOfSides(radius), solid, bottomRingVertices, topRingVertices);
}
public static void addCylinderMesh(DefaultObj obj, String groupName, float radius, float height, boolean solid,
boolean isOutside, int nrOfSlices,
List<Integer> bottomRingVertices, List<Integer> topRingVertices) {
addCylinderMesh(obj, groupName, radius, height, nrOfSlices, solid, isOutside, bottomRingVertices, topRingVertices);
}
public static void addCylinderMesh(DefaultObj obj, String groupName, float radius, float height, boolean solid,
int nrOfSlices,
List<Integer> bottomRingVertices, List<Integer> topRingVertices) {
addCylinderMesh(obj, groupName, radius, height, nrOfSlices, solid, bottomRingVertices, topRingVertices);
}
public static void addCylinderMesh(DefaultObj obj, String groupName, float radius, float height, boolean solid,
ObjUtils.LevelOfDetail LOD) {
addCylinderMesh(obj, groupName, radius, height, LOD.getNrOfSides(radius), solid, null, null);
}
public static void addCylinderMesh(DefaultObj obj, String groupName, float radius, float height, boolean solid) {
addCylinderMesh(obj, groupName, radius, height, solid, ObjUtils.LevelOfDetail.NORMAL);
}
public static void main(String[] args) throws Exception {
DefaultObj obj = new DefaultObj();
addCylinderMesh(obj, "cylinder", 1, 2, 15, true, null, null);
try (OutputStream objOutputStream = new FileOutputStream("/Users/SiboVanGool/Downloads/cylinder.obj")) {
ObjWriter.write(obj, objOutputStream);
}
}
}

View File

@ -0,0 +1,99 @@
package net.sf.openrocket.file.wavefrontobj.export.shapes;
import de.javagl.obj.FloatTuple;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.DefaultObjFace;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import java.util.List;
public class DiskExporter {
/**
* Adds a disk mesh to the obj by using existing outer and inner vertices
* @param obj The obj to add the mesh to
* @param groupName The name of the group to add the mesh to
* @param outerVertices The indices of the outer vertices
* @param innerVertices The indices of the inner vertices, or null if the disk is solid
* @param isTopFace Whether the disk is a top face (true) or bottom face (false)
*/
public static void closeDiskMesh(DefaultObj obj, String groupName, List<Integer> outerVertices, List<Integer> innerVertices,
boolean isTopFace) {
if (outerVertices.isEmpty()) {
throw new IllegalArgumentException("Outer vertices cannot be empty");
}
// If there is only one vertex, this is a tip of an object (0 radius)
if (outerVertices.size() == 1) {
return;
}
boolean isSolid = innerVertices == null || innerVertices.size() == 1;
boolean useInnerVertexAsCenter = innerVertices != null && innerVertices.size() == 1; // Whether you should use the inner vertex as the center of the solid disk
if (!isSolid && innerVertices.size() > 1 && outerVertices.size() != innerVertices.size()) {
throw new IllegalArgumentException("Outer and inner vertices must have the same size");
}
// Set the new group
if (groupName != null) {
obj.setActiveGroupNames(groupName);
}
// Flat disk, so all vertices have the same normal
obj.addNormal(0, isTopFace ? 1 : -1, 0); // TODO: hm, what if the object is rotated? If the disk is not drawn in the y direction?
final int normalIndex = obj.getNumNormals() - 1;
if (isSolid) {
// Add the center vertex
final int centerVertexIdx;
if (useInnerVertexAsCenter) {
centerVertexIdx = innerVertices.get(0);
} else {
final FloatTuple centerVertex = ObjUtils.averageVertices(obj, outerVertices);
obj.addVertex(centerVertex);
centerVertexIdx = obj.getNumVertices() - 1;
}
// Add the faces
for (int i = 0; i < outerVertices.size(); i++) {
int nextIdx = (i + 1) % outerVertices.size();
int[] vertexIndices = new int[] {
centerVertexIdx,
outerVertices.get(nextIdx),
outerVertices.get(i),
};
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, isTopFace);
int[] normalIndices = new int[] { normalIndex, normalIndex, normalIndex };
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
} else {
// Add the faces
for (int i = 0; i < outerVertices.size(); i++) {
int nextIdx = (i + 1) % outerVertices.size();
int[] vertexIndices = new int[] {
outerVertices.get(i), // Bottom-left of quad
outerVertices.get(nextIdx), // Bottom-right of quad
innerVertices.get(nextIdx), // Top-right of quad
innerVertices.get(i), // Top-left of quad
};
vertexIndices = ObjUtils.reverseIndexWinding(vertexIndices, isTopFace);
int[] normalIndices = new int[] { normalIndex, normalIndex, normalIndex, normalIndex };
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
}
}
/**
* Adds a (closed) disk mesh to the obj by using existing outer and inner vertices
* @param obj The obj to add the mesh to
* @param groupName The name of the group to add the mesh to
* @param outerVertices The indices of the outer vertices
* @param isTopFace Whether the disk is a top face (true) or bottom face (false)
*/
public static void closeDiskMesh(DefaultObj obj, String groupName, List<Integer> outerVertices,
boolean isTopFace) {
closeDiskMesh(obj, groupName, outerVertices, null, isTopFace);
}
}

View File

@ -0,0 +1,124 @@
package net.sf.openrocket.file.wavefrontobj.export.shapes;
import de.javagl.obj.ObjWriter;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.DefaultObjFace;
import net.sf.openrocket.file.wavefrontobj.DefaultObjGroup;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import java.io.FileOutputStream;
import java.io.OutputStream;
public class PolygonExporter {
/**
* Add a polygon mesh to the obj. It is drawn in the XY plane with the bottom left corner at the origin.
* @param obj The obj to add the mesh to
* @param groupName The name of the group to add the mesh to, or null if no group should be added (use the active group)
* @param pointLocationsX The x locations of the points --> NOTE: points should follow a clockwise direction
* @param pointLocationsY The y locations of the points --> NOTE: points should follow a clockwise direction
* @param thickness The thickness of the polygon
*/
public static void addPolygonMesh(DefaultObj obj, String groupName,
float[] pointLocationsX, float[] pointLocationsY, float thickness) {
verifyPoints(pointLocationsX, pointLocationsY);
// Set the new group
if (groupName != null) {
obj.setActiveGroupNames(groupName);
}
// Other meshes may have been added to the group, so we need to keep track of the starting indices
int verticesStartIdx = obj.getNumVertices();
int normalsStartIdx = obj.getNumNormals();
obj.addNormal(0, 0, 1); // Front faces normal
obj.addNormal(0, 0, -1); // Back faces normal
// Generate front face vertices
for (int i = 0; i < pointLocationsX.length; i++) {
obj.addVertex(pointLocationsX[i], pointLocationsY[i], thickness/2);
}
// Generate back face vertices
for (int i = 0; i < pointLocationsX.length; i++) {
obj.addVertex(pointLocationsX[i], pointLocationsY[i], -thickness/2);
}
// Create front face
int[] vertexIndices = new int[pointLocationsX.length];
int[] normalIndices = new int[pointLocationsX.length];
for (int i = 0; i < pointLocationsX.length; i++) {
vertexIndices[i] = pointLocationsX.length - i -1;
normalIndices[i] = normalsStartIdx;
}
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx);
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
// Create back face
vertexIndices = new int[pointLocationsX.length];
normalIndices = new int[pointLocationsX.length];
for (int i = 0; i < pointLocationsX.length; i++) {
vertexIndices[i] = pointLocationsX.length + i;
normalIndices[i] = normalsStartIdx + 1;
}
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx);
face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
// Create side faces
for (int i = 0; i < pointLocationsX.length; i++) {
int nextIdx = (i + 1) % pointLocationsX.length;
vertexIndices = new int[]{
i, // Bottom-left of quad
nextIdx, // Top-left of quad
pointLocationsX.length + nextIdx, // Top-right of quad
pointLocationsX.length + i // Bottom-right of quad
};
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx);
// Calculate normals for side faces
final float dx = pointLocationsX[nextIdx] - pointLocationsX[i];
final float dy = pointLocationsY[nextIdx] - pointLocationsY[i];
// Perpendicular vector in 2D (for clockwise vertices)
final float nx = -dy;
final float ny = dx;
// Add the normal to the object
obj.addNormal(nx, ny, 0);
normalIndices = new int[]{i, i, i, i};
ObjUtils.offsetIndex(normalIndices, normalsStartIdx + 2); // Offset by 2 to skip the bottom and top faces normals
face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
}
private static void verifyPoints(float[] pointLocationsX, float[] pointLocationsY) {
if (pointLocationsX.length != pointLocationsY.length) {
throw new IllegalArgumentException("pointLocationsX and pointLocationsY must be the same length");
}
if (pointLocationsX.length < 3) {
throw new IllegalArgumentException("At least 3 points are required to create a polygon");
}
if (Float.compare(pointLocationsX[pointLocationsX.length-1], pointLocationsX[0]) == 0 &&
Float.compare(pointLocationsY[pointLocationsY.length-1], pointLocationsY[0]) == 0) {
throw new IllegalArgumentException("The first and last points must be different");
}
}
public static void main(String[] args) throws Exception {
DefaultObj obj = new DefaultObj();
float[] x = new float[]{0, 0.3f, 1, 0.7f};
float[] y = new float[]{0, 0.5f, 0.5f, 0};
addPolygonMesh(obj, "polygon", x, y, 0.025f);
try (OutputStream objOutputStream = new FileOutputStream("/Users/SiboVanGool/Downloads/poly.obj")) {
ObjWriter.write(obj, objOutputStream);
}
}
}

View File

@ -0,0 +1,249 @@
package net.sf.openrocket.file.wavefrontobj.export.shapes;
import de.javagl.obj.ObjWriter;
import net.sf.openrocket.file.wavefrontobj.DefaultObj;
import net.sf.openrocket.file.wavefrontobj.DefaultObjFace;
import net.sf.openrocket.file.wavefrontobj.ObjUtils;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.List;
public class TubeExporter {
/**
* Add a tube mesh to the obj. The longitudinal axis is the y axis (in OBJ coordinate system).
* @param obj The obj to add the mesh to
* @param groupName The name of the group to add the mesh to, or null if no group should be added (use the active group)
* @param bottomOuterRadius The outer radius of the bottom of the tube
* @param topOuterRadius The outer radius of the top of the tube
* @param bottomInnerRadius The inner radius of the bottom of the tube
* @param topInnerRadius The inner radius of the top of the tube
* @param height The height of the tube
* @param numSides The number of sides of the tube
* @param bottomOuterVertices A list to add the indices of the bottom outer vertices to, or null if the indices are not needed
* @param topOuterVertices A list to add the indices of the top outer vertices to, or null if the indices are not needed
* @param bottomInnerVertices A list to add the indices of the bottom inner vertices to, or null if the indices are not needed
* @param topInnerVertices A list to add the indices of the top inner vertices to, or null if the indices are not needed
*/
public static void addTubeMesh(DefaultObj obj, String groupName,
float bottomOuterRadius, float topOuterRadius,
float bottomInnerRadius, float topInnerRadius, float height, int numSides,
List<Integer> bottomOuterVertices, List<Integer> topOuterVertices,
List<Integer> bottomInnerVertices, List<Integer> topInnerVertices) {
if (bottomInnerRadius > bottomOuterRadius || topInnerRadius > topOuterRadius) {
throw new IllegalArgumentException("Inner radius must be less than outer radius");
}
// Set the new group
if (groupName != null) {
obj.setActiveGroupNames(groupName);
}
// Other meshes may have been added to the obj, so we need to keep track of the starting indices
int verticesStartIdx = obj.getNumVertices();
int normalsStartIdx = obj.getNumNormals();
obj.addNormal(0, -1, 0); // Bottom faces normal
obj.addNormal(0, 1, 0); // Top faces normal
// Generate bottom outside vertices
for (int i = 0; i < numSides; i++) {
double angle = 2 * Math.PI * i / numSides;
float x = bottomOuterRadius * (float) Math.cos(angle);
float z = bottomOuterRadius * (float) Math.sin(angle);
obj.addVertex(x, 0, z);
obj.addNormal(x, 0, z); // This kind of normal ensures the object is smoothly rendered (like the 'Shade Smooth' option in Blender)
if (bottomOuterVertices != null) {
bottomOuterVertices.add(verticesStartIdx + i);
}
}
// Generate bottom inside vertices
for (int i = 0; i < numSides; i++) {
double angle = 2 * Math.PI * i / numSides;
float x = bottomInnerRadius * (float) Math.cos(angle);
float z = bottomInnerRadius * (float) Math.sin(angle);
obj.addVertex(x, 0, z);
obj.addNormal(x, 0, z); // For smooth shading
if (bottomInnerVertices != null) {
bottomInnerVertices.add(verticesStartIdx + numSides + i);
}
}
// Generate top outside vertices
for (int i = 0; i < numSides; i++) {
double angle = 2 * Math.PI * i / numSides;
float x = topOuterRadius * (float) Math.cos(angle);
float z = topOuterRadius * (float) Math.sin(angle);
// Side top vertices
obj.addVertex(x, height, z);
obj.addNormal(x, 0, z);
if (topOuterVertices != null) {
topOuterVertices.add(verticesStartIdx + 2*numSides + i);
}
}
// Generate top inside vertices
for (int i = 0; i < numSides; i++) {
double angle = 2 * Math.PI * i / numSides;
float x = topInnerRadius * (float) Math.cos(angle);
float z = topInnerRadius * (float) Math.sin(angle);
// Side top vertices
obj.addVertex(x, height, z);
obj.addNormal(x, 0, z);
if (topInnerVertices != null) {
topInnerVertices.add(verticesStartIdx + 3*numSides + i);
}
}
// Create bottom faces
for (int i = 0; i < numSides; i++) {
int[] vertexIndices = new int[]{
i, // Bottom-left of quad outside vertex
((i + 1) % numSides), // Bottom-right of quad outside vertex
numSides + ((i + 1) % numSides), // Top-right of quad inside vertex
numSides + i // Top-left of quad inside vertex
};
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx);
int[] normalIndices = new int[]{0, 0, 0, 0};
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
// Create top faces
for (int i = 0; i < numSides; i++) {
int[] vertexIndices = new int[]{
2*numSides + i, // Bottom-left of quad outside vertex
3*numSides + i, // Top-left of quad inside vertex
3*numSides + ((i + 1) % numSides), // Top-right of quad inside vertex
2*numSides + ((i + 1) % numSides) // Bottom-right of quad outside vertex
};
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx);
int[] normalIndices = new int[]{1, 1, 1, 1};
ObjUtils.offsetIndex(normalIndices, normalsStartIdx);
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
// Create outside side faces
for (int i = 0; i < numSides; i++) {
final int nextIdx = (i + 1) % numSides;
int[] vertexIndices = new int[]{
i, // Bottom-left of quad outside vertex
2*numSides + i, // Top-left of quad outside vertex
2*numSides + nextIdx, // Top-right of quad outside vertex
nextIdx, // Bottom-right of quad outside vertex
};
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx);
int[] normalIndices = new int[]{
i, // Bottom-left of quad outside vertex
2*numSides + i, // Top-left of quad outside vertex
2*numSides + nextIdx, // Top-right of quad outside vertex
nextIdx, // Bottom-right of quad outside vertex
};
ObjUtils.offsetIndex(normalIndices, normalsStartIdx + 2); // Extra 2 offset for bottom and top normals
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
// Create inside side faces
for (int i = 0; i < numSides; i++) {
final int nextIdx = (i + 1) % numSides;
int[] vertexIndices = new int[]{
numSides + i, // Bottom-left of quad inside vertex
numSides + nextIdx, // Bottom-right of quad inside vertex
3*numSides + nextIdx, // Top-right of quad inside vertex
3*numSides + i, // Top-left of quad inside vertex
};
ObjUtils.offsetIndex(vertexIndices, verticesStartIdx);
int[] normalIndices = new int[]{
numSides + i, // Bottom-left of quad inside vertex
numSides + nextIdx, // Bottom-right of quad inside vertex
3*numSides + nextIdx, // Top-right of quad inside vertex
3*numSides + i, // Top-left of quad inside vertex
};
ObjUtils.offsetIndex(normalIndices, normalsStartIdx + 2); // Extra 2 offset for bottom and top normals
DefaultObjFace face = new DefaultObjFace(vertexIndices, null, normalIndices);
obj.addFace(face);
}
}
public static void addTubeMesh(DefaultObj obj, String groupName,
float bottomOuterRadius, float topOuterRadius,
float bottomInnerRadius, float topInnerRadius, float height, ObjUtils.LevelOfDetail LOD,
List<Integer> bottomOuterVertices, List<Integer> topOuterVertices,
List<Integer> bottomInnerVertices, List<Integer> topInnerVertices) {
addTubeMesh(obj, groupName, bottomOuterRadius, topOuterRadius, bottomInnerRadius, topInnerRadius, height,
LOD.getNrOfSides(Math.max(bottomOuterRadius, topOuterRadius)),
bottomOuterVertices, topOuterVertices, bottomInnerVertices, topInnerVertices);
}
public static void addTubeMesh(DefaultObj obj, String groupName,
float bottomOuterRadius, float topOuterRadius,
float bottomInnerRadius, float topInnerRadius, float height, ObjUtils.LevelOfDetail LOD) {
addTubeMesh(obj, groupName, bottomOuterRadius, topOuterRadius, bottomInnerRadius, topInnerRadius, height,
LOD.getNrOfSides(Math.max(bottomOuterRadius, topOuterRadius)),
null, null, null, null);
}
public static void addTubeMesh(DefaultObj obj, String groupName, float outerRadius, float innerRadius, float height, int numSides) {
addTubeMesh(obj, groupName, outerRadius, outerRadius, innerRadius, innerRadius, height, numSides,
null, null, null, null);
}
public static void addTubeMesh(DefaultObj obj, String groupName, float bottomOuterRadius, float topOuterRadius,
float bottomInnerRadius, float topInnerRadius, float height) {
addTubeMesh(obj, groupName, bottomOuterRadius, topOuterRadius, bottomInnerRadius, topInnerRadius, height, ObjUtils.LevelOfDetail.NORMAL);
}
public static void addTubeMesh(DefaultObj obj, String groupName, float outerRadius, float innerRadius, float height,
ObjUtils.LevelOfDetail LOD,
List<Integer> bottomOuterVertices, List<Integer> topOuterVertices,
List<Integer> bottomInnerVertices, List<Integer> topInnerVertices) {
addTubeMesh(obj, groupName, outerRadius, outerRadius, innerRadius, innerRadius, height, LOD.getNrOfSides(outerRadius),
bottomOuterVertices, topOuterVertices, bottomInnerVertices, topInnerVertices);
}
public static void addTubeMesh(DefaultObj obj, String groupName, float outerRadius, float innerRadius, float height,
int nrOfSlices,
List<Integer> bottomOuterVertices, List<Integer> topOuterVertices,
List<Integer> bottomInnerVertices, List<Integer> topInnerVertices) {
addTubeMesh(obj, groupName, outerRadius, outerRadius, innerRadius, innerRadius, height, nrOfSlices,
bottomOuterVertices, topOuterVertices, bottomInnerVertices, topInnerVertices);
}
public static void addTubeMesh(DefaultObj obj, String groupName, float outerRadius, float innerRadius, float height,
ObjUtils.LevelOfDetail LOD) {
addTubeMesh(obj, groupName, outerRadius, outerRadius, innerRadius, innerRadius, height, LOD.getNrOfSides(outerRadius),
null, null, null, null);
}
public static void addTubeMesh(DefaultObj obj, String groupName, float outerRadius, float innerRadius, float height) {
addTubeMesh(obj, groupName, outerRadius, outerRadius, innerRadius, innerRadius, height);
}
public static void main(String[] args) throws Exception {
DefaultObj obj = new DefaultObj();
//addTubeMesh(obj, "tube", 0.1f, 0.085f, 0.3f);
addTubeMesh(obj, "tube", 0.14f, 0.06f, 0.13f, 0.05f, 0.3f);
try (OutputStream objOutputStream = new FileOutputStream("/Users/SiboVanGool/Downloads/tube.obj")) {
ObjWriter.write(obj, objOutputStream);
}
}
}

View File

@ -404,6 +404,10 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona
return getTabOffset(this.tabOffsetMethod);
}
public double getTabPosition(AxialMethod method) {
return method.getAsPosition(tabOffset, tabLength, length);
}
/**
* Return the tab trailing edge position *from the front of the fin*.
*/

View File

@ -0,0 +1,32 @@
package net.sf.openrocket.util;
import net.sf.openrocket.rocketcomponent.MassObject;
public class RocketComponentUtils {
/**
* Returns the radius of a mass object at a given z coordinate to be used for 3D rendering.
* This has no real physical meaning.
* @param o the mass object
* @param z the z coordinate
* @return the radius of the mass object at the given z coordinate
*/
public static double getMassObjectRadius(MassObject o, double z) {
double arc = getMassObjectArcHeight(o);
double r = o.getRadius();
if (z == 0 || z == o.getLength())
return 0;
if (z < arc) {
double zz = z - arc;
return (r - arc) + Math.sqrt(arc * arc - zz * zz);
}
if (z > o.getLength() - arc) {
double zz = (z - o.getLength() + arc);
return (r - arc) + Math.sqrt(arc * arc - zz * zz);
}
return o.getRadius();
}
public static double getMassObjectArcHeight(MassObject o) {
return Math.min(o.getLength(), 2 * o.getRadius()) * 0.35f;
}
}

View File

@ -0,0 +1,193 @@
package net.sf.openrocket.file.wavefrontobj;
import de.javagl.obj.FloatTuple;
import net.sf.openrocket.util.BaseTestCase.BaseTestCase;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class OBJUtilsTest extends BaseTestCase {
public static final float EPSILON = 0.0001f;
@Test
public void testTranslateSingleVertex() {
final DefaultObj obj = new DefaultObj();
int startIdx = obj.getNumVertices();
obj.addVertex(0.0f, 1.0f, 2.3f);
int endIdx = obj.getNumVertices() - 1;
ObjUtils.translateVertices(obj, startIdx, endIdx, 1.0f, 1.0f, 1.0f);
FloatTuple vertex = obj.getVertex(startIdx);
assertEquals(1.0f, vertex.getX(), EPSILON);
assertEquals(2.0f, vertex.getY(), EPSILON);
assertEquals(3.3f, vertex.getZ(), EPSILON);
}
@Test
public void testTranslateMultipleVertices() {
final DefaultObj obj = new DefaultObj();
int startIdx = obj.getNumVertices();
obj.addVertex(0.0f, 1.0f, 2.3f);
obj.addVertex(2.0f, 2.0f, 4.6f);
int endIdx = obj.getNumVertices() - 1;
ObjUtils.translateVertices(obj, startIdx, endIdx, 0.0f, -2.0f, 0.0f);
FloatTuple vertex0 = obj.getVertex(startIdx);
assertEquals(0.0f, vertex0.getX(), EPSILON);
assertEquals(-1.0f, vertex0.getY(), EPSILON);
assertEquals(2.3f, vertex0.getZ(), EPSILON);
FloatTuple vertex1 = obj.getVertex(endIdx);
assertEquals(2.0f, vertex1.getX(), EPSILON);
assertEquals(0.0f, vertex1.getY(), EPSILON);
assertEquals(4.6f, vertex1.getZ(), EPSILON);
}
@Test
public void testTranslateWithNoTranslation() {
final DefaultObj obj = new DefaultObj();
int startIdx = obj.getNumVertices();
obj.addVertex(0.0f, 1.0f, 2.3f);
int endIdx = obj.getNumVertices() - 1;
ObjUtils.translateVertices(obj, startIdx, endIdx, 0.0f, 0.0f, 0.0f);
FloatTuple vertex = obj.getVertex(startIdx);
assertEquals(0.0f, vertex.getX(), EPSILON);
assertEquals(1.0f, vertex.getY(), EPSILON);
assertEquals(2.3f, vertex.getZ(), EPSILON);
}
@Test
public void testTranslateIndexBoundaries() {
final DefaultObj obj = new DefaultObj();
obj.addVertex(0.0f, 1.0f, 2.3f);
obj.addVertex(2.0f, 2.0f, 4.6f);
obj.addVertex(4.0f, 3.0f, 6.9f);
// Translate only the first vertex
ObjUtils.translateVertices(obj, 0, 0, 1.0f, 1.0f, 1.0f);
FloatTuple vertex0 = obj.getVertex(0);
assertEquals(1.0f, vertex0.getX(), EPSILON);
assertEquals(2.0f, vertex0.getY(), EPSILON);
assertEquals(3.3f, vertex0.getZ(), EPSILON);
FloatTuple vertex1 = obj.getVertex(1);
assertEquals(2.0f, vertex1.getX(), EPSILON);
assertEquals(2.0f, vertex1.getY(), EPSILON);
assertEquals(4.6f, vertex1.getZ(), EPSILON);
FloatTuple vertex2 = obj.getVertex(2);
assertEquals(4.0f, vertex2.getX(), EPSILON);
assertEquals(3.0f, vertex2.getY(), EPSILON);
assertEquals(6.9f, vertex2.getZ(), EPSILON);
// Reset and translate only the last vertex
obj.setVertex(0, new DefaultFloatTuple(0.0f, 1.0f, 2.3f));
ObjUtils.translateVertices(obj, 2, 2, 1.0f, 1.0f, 1.0f);
vertex0 = obj.getVertex(0);
assertEquals(0.0f, vertex0.getX(), EPSILON);
assertEquals(1.0f, vertex0.getY(), EPSILON);
assertEquals(2.3f, vertex0.getZ(), EPSILON);
vertex1 = obj.getVertex(1);
assertEquals(2.0f, vertex1.getX(), EPSILON);
assertEquals(2.0f, vertex1.getY(), EPSILON);
assertEquals(4.6f, vertex1.getZ(), EPSILON);
vertex2 = obj.getVertex(2);
assertEquals(5.0f, vertex2.getX(), EPSILON);
assertEquals(4.0f, vertex2.getY(), EPSILON);
assertEquals(7.9f, vertex2.getZ(), EPSILON);
}
@Test
public void testTranslateWithNegativeValues() {
final DefaultObj obj = new DefaultObj();
int startIdx = obj.getNumVertices();
obj.addVertex(0.0f, 1.0f, 2.3f);
int endIdx = obj.getNumVertices() - 1;
ObjUtils.translateVertices(obj, startIdx, endIdx, -1.0f, -1.0f, -1.0f);
FloatTuple vertex = obj.getVertex(startIdx);
assertEquals(-1.0f, vertex.getX(), EPSILON);
assertEquals(0.0f, vertex.getY(), EPSILON);
assertEquals(1.3f, vertex.getZ(), EPSILON);
}
@Test
public void testRotateWithNoChange() {
final DefaultObj obj = new DefaultObj();
int verticesStartIdx = obj.getNumVertices();
obj.addVertex(1.0f, 1.0f, 1.0f);
int normalsStartIdx = obj.getNumNormals();
obj.addNormal(1.0f, 0.0f, 0.0f);
ObjUtils.rotateVertices(obj, verticesStartIdx, verticesStartIdx, normalsStartIdx, normalsStartIdx,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
FloatTuple vertex = obj.getVertex(verticesStartIdx);
assertEquals(1.0f, vertex.getX(), EPSILON);
assertEquals(1.0f, vertex.getY(), EPSILON);
assertEquals(1.0f, vertex.getZ(), EPSILON);
FloatTuple normal = obj.getNormal(normalsStartIdx);
assertEquals(1.0f, normal.getX(), EPSILON);
assertEquals(0.0f, normal.getY(), EPSILON);
assertEquals(0.0f, normal.getZ(), EPSILON);
}
@Test
public void testRotationAroundXAxis() {
final DefaultObj obj = new DefaultObj();
obj.addVertex(1.0f, 1.0f, 1.0f);
obj.addNormal(1.0f, 0.0f, 0.0f);
ObjUtils.rotateVertices(obj, 0, 0, 0, 0,
(float) Math.PI/2, 0, 0, 0.0f, 0.0f, 0.0f);
FloatTuple vertex = obj.getVertex(0);
assertEquals(1.0f, vertex.getX(), EPSILON);
assertEquals(-1.0f, vertex.getY(), EPSILON);
assertEquals(1.0f, vertex.getZ(), EPSILON);
FloatTuple normal = obj.getNormal(0);
assertEquals(1.0f, normal.getX(), EPSILON);
assertEquals(0.0f, normal.getY(), EPSILON);
assertEquals(0.0f, normal.getZ(), EPSILON);
}
@Test
public void testRotationAroundYAxis() {
final DefaultObj obj = new DefaultObj();
int verticesStartIdx = obj.getNumVertices();
obj.addVertex(1.0f, 1.0f, 1.0f);
int normalsStartIdx = obj.getNumNormals();
obj.addNormal(1.0f, 0.0f, 0.0f);
ObjUtils.rotateVertices(obj, verticesStartIdx, verticesStartIdx, normalsStartIdx, normalsStartIdx,
0.0f, (float) Math.PI/2, 0.0f, 0.0f, 0.0f, 0.0f);
FloatTuple vertex = obj.getVertex(verticesStartIdx);
assertEquals(1.0f, vertex.getX(), EPSILON);
assertEquals(1.0f, vertex.getY(), EPSILON);
assertEquals(-1.0f, vertex.getZ(), EPSILON);
FloatTuple normal = obj.getNormal(normalsStartIdx);
assertEquals(0.0f, normal.getX(), EPSILON);
assertEquals(0.0f, normal.getY(), EPSILON);
assertEquals(-1.0f, normal.getZ(), EPSILON);
}
}

View File

@ -0,0 +1,93 @@
package net.sf.openrocket.file.wavefrontobj.export;
import net.sf.openrocket.document.OpenRocketDocumentFactory;
import net.sf.openrocket.rocketcomponent.AxialStage;
import net.sf.openrocket.rocketcomponent.BodyTube;
import net.sf.openrocket.rocketcomponent.FinSet;
import net.sf.openrocket.rocketcomponent.LaunchLug;
import net.sf.openrocket.rocketcomponent.NoseCone;
import net.sf.openrocket.rocketcomponent.Parachute;
import net.sf.openrocket.rocketcomponent.RailButton;
import net.sf.openrocket.rocketcomponent.Rocket;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.rocketcomponent.Transition;
import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
import net.sf.openrocket.rocketcomponent.TubeFinSet;
import net.sf.openrocket.util.BaseTestCase.BaseTestCase;
import net.sf.openrocket.util.TestRockets;
import org.junit.Test;
import java.util.List;
public class OBJExporterFactoryTest extends BaseTestCase {
@Test
public void testExport() {
Rocket rocket = OpenRocketDocumentFactory.createNewRocket().getRocket();
AxialStage sustainer = rocket.getStage(0);
NoseCone noseCone = new NoseCone();
noseCone.setBaseRadius(0.05);
noseCone.setLength(0.1);
noseCone.setShoulderLength(0.01);
noseCone.setShoulderRadius(0.03);
noseCone.setShoulderThickness(0.002);
noseCone.setShoulderCapped(false);
sustainer.addChild(noseCone);
BodyTube bodyTube = new BodyTube();
bodyTube.setOuterRadius(0.05);
bodyTube.setThickness(0.005);
bodyTube.setLength(0.3);
sustainer.addChild(bodyTube);
LaunchLug launchLug = new LaunchLug();
launchLug.setLength(0.05);
launchLug.setOuterRadius(0.02);
launchLug.setThickness(0.005);
launchLug.setInstanceCount(2);
launchLug.setInstanceSeparation(0.1);
bodyTube.addChild(launchLug);
TrapezoidFinSet finSet = new TrapezoidFinSet();
finSet.setRootChord(0.05);
finSet.setTabLength(0.03);
finSet.setTabHeight(0.01);
finSet.setTabOffset(0);
bodyTube.addChild(finSet);
TubeFinSet tubeFinSet = new TubeFinSet();
tubeFinSet.setFinCount(4);
tubeFinSet.setOuterRadius(0.01);
tubeFinSet.setLength(0.05);
tubeFinSet.setBaseRotation(Math.PI / 8);
tubeFinSet.setAxialOffset(-0.1);
bodyTube.addChild(tubeFinSet);
Transition transition = new Transition();
transition.setLength(0.1);
transition.setForeRadius(0.05);
transition.setAftRadius(0.025);
transition.setThickness(0.003);
transition.setShapeType(Transition.Shape.PARABOLIC);
transition.setShapeParameter(0.7);
sustainer.addChild(transition);
Parachute parachute = new Parachute();
parachute.setRadiusAutomatic(false);
parachute.setRadius(0.05);
parachute.setLength(0.075);
parachute.setRadialPosition(0.02);
parachute.setRadialDirection(Math.PI / 3);
bodyTube.addChild(parachute);
RailButton railButton = new RailButton();
railButton.setScrewHeight(0.0025);
bodyTube.addChild(railButton);
List<RocketComponent> components = List.of(noseCone);
OBJExporterFactory exporterFactory = new OBJExporterFactory(components, false, false, true,
"/Users/SiboVanGool/Downloads/testExport.obj");
exporterFactory.doExport();
}
}

View File

@ -121,6 +121,7 @@
<zipfileset src="${core.dir}/lib/slf4j-api-1.7.30.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/obj-0.4.0.jar"/>
<zipfileset src="${lib.dir}/rsyntaxtextarea-3.2.0.jar"/>
<!-- JOGL libraries need to be jar-in-jar -->

View File

@ -43,7 +43,7 @@ public class AboutDialog extends JDialog {
"Daniel Williams (pod support, maintainer)<br>" +
"Joe Pfeiffer (maintainer)<br>" +
"Billy Olsen (maintainer)<br>" +
"Sibo Van Gool (RASAero file format, maintainer)<br>" +
"Sibo Van Gool (RASAero file format, 3D OBJ export, maintainer)<br>" +
"Justin Hanney (maintainer)<br>" +
"Neil Weinstock (tester, icons, forum support)<br>" +
"H. Craig Miller (tester)<br><br>" +
@ -72,6 +72,7 @@ public class AboutDialog extends JDialog {
"Simple Logging Facade for Java" + href("http://www.slf4j.org", true, true) + "<br>" +
"Java library for parsing and rendering CommonMark" + href("https://github.com/commonmark/commonmark-java", true, true) + "<br>" +
"RSyntaxTextArea" + href("http://bobbylight.github.io/RSyntaxTextArea", true, true) + "<br>" +
"Obj" + href("https://github.com/javagl/Obj", true, true) + "<br>" +
"<br>" +
"<b>OpenRocket gratefully acknowledges our use of the following databases:</b><br>" +
"<br>" +

View File

@ -37,7 +37,7 @@ public class ComponentRenderer {
@SuppressWarnings("unused")
private static final Logger log = LoggerFactory.getLogger(ComponentRenderer.class);
private int LOD = 80;
private int LOD = 80; // Level of detail for rendering
GLU glu;
GLUquadric q;

View File

@ -118,6 +118,7 @@ import com.jogamp.opengl.GL;
import com.jogamp.opengl.GL2;
import net.sf.openrocket.rocketcomponent.MassObject;
import net.sf.openrocket.util.RocketComponentUtils;
final class MassObjectRenderer {
private static final boolean textureFlag = true;
@ -133,7 +134,7 @@ final class MassObjectRenderer {
* @param slices number of slices for the 3D object (kind of like subdivision surface)
* @param stacks number of stacks for the 3D object (kind of like subdivision surface)
*/
static final void drawMassObject(final GL2 gl, final MassObject o,
static void drawMassObject(final GL2 gl, final MassObject o,
final int slices, final int stacks) {
double da, r, dz; // Axial length per slice, radius & length per stack
@ -148,8 +149,8 @@ final class MassObjectRenderer {
double t = 0.0f;
z = 0.0f;
for (j = 0; j < stacks; j++) {
r = getRadius(o, z);
double rNext = getRadius(o, z + dz);
r = RocketComponentUtils.getMassObjectRadius(o, z);
double rNext = RocketComponentUtils.getMassObjectRadius(o, z + dz);
if (j == stacks - 1)
rNext = 0;
@ -192,22 +193,6 @@ final class MassObjectRenderer {
} // for stacks
}
private static final double getRadius(MassObject o, double z) {
double arc = Math.min(o.getLength(), 2 * o.getRadius()) * 0.35f;
double r = o.getRadius();
if (z == 0 || z == o.getLength())
return 0;
if (z < arc) {
double zz = z - arc;
return (r - arc) + Math.sqrt(arc * arc - zz * zz);
}
if (z > o.getLength() - arc) {
double zz = (z - o.getLength() + arc);
return (r - arc) + Math.sqrt(arc * arc - zz * zz);
}
return o.getRadius();
}
// ----------------------------------------------------------------------
// Internals only below this point
//

View File

@ -49,6 +49,7 @@ import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import net.miginfocom.swing.MigLayout;
import net.sf.openrocket.file.wavefrontobj.export.OBJExporterFactory;
import net.sf.openrocket.gui.configdialog.SaveDesignInfoPanel;
import net.sf.openrocket.gui.dialogs.ErrorWarningDialog;
import net.sf.openrocket.logging.ErrorSet;
@ -432,6 +433,19 @@ public class BasicFrame extends JFrame {
});
exportSubMenu.add(exportRockSim);
exportSubMenu.addSeparator();
////// Export Wavefront OBJ
JMenuItem exportOBJ = new JMenuItem(trans.get("main.menu.file.exportAs.WavefrontOBJ"));
exportOBJ.setIcon(Icons.EXPORT_3D);
exportOBJ.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.file.exportAs.WavefrontOBJ.desc"));
exportOBJ.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
exportWavefrontOBJAction();}
});
exportSubMenu.add(exportOBJ);
fileMenu.add(exportSubMenu);
fileMenu.addSeparator();
@ -1591,6 +1605,15 @@ public class BasicFrame extends JFrame {
//// END ROCKSIM Save/Export Action
//// BEGIN WAVEFRONT OBJ Save/Export Action
private void exportWavefrontOBJAction() {
// TODO: popup dialog for extra options (quality, whether to triangulate, whether to export materials, whether to save all subcomponents of the selected ones, whether to offset the object position to zero or to the location in the rocket, whether to scale the rocket etc.)
String filePath = "/Users/SiboVanGool/Downloads/test.obj";
OBJExporterFactory exporter = new OBJExporterFactory(getSelectedComponents(), false, false, true, filePath);
exporter.doExport();
}
/**
* "Save As" action.
*

View File

@ -102,7 +102,7 @@ public class Icons {
public static final Icon MASS_OVERRIDE = loadImageIcon("pix/icons/mass-override.png", "Mass Override");
public static final Icon MASS_OVERRIDE_SUBCOMPONENT = loadImageIcon("pix/icons/mass-override-subcomponent.png", "Mass Override Subcomponent");
// MANUFACTURERS ICONS
// MANUFACTURERS ICONS
public static final Icon RASAERO = loadImageIcon("pix/icons/RASAero_16.png", "RASAero Icon");
public static final Icon ROCKSIM = loadImageIcon("pix/icons/Rocksim_16.png", "Rocksim Icon");