[#604] Implement Wavefront OBJ 3D export (PHEW!)
This commit is contained in:
parent
d2bb552807
commit
c4315e83aa
@ -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)
|
||||
|
||||
|
@ -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
BIN
core/lib/obj-0.4.0.jar
Normal file
Binary file not shown.
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
594
core/src/net/sf/openrocket/file/wavefrontobj/DefaultObj.java
Normal file
594
core/src/net/sf/openrocket/file/wavefrontobj/DefaultObj.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
149
core/src/net/sf/openrocket/file/wavefrontobj/DefaultObjFace.java
Normal file
149
core/src/net/sf/openrocket/file/wavefrontobj/DefaultObjFace.java
Normal 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();
|
||||
}
|
||||
}
|
||||
|
@ -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() + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
329
core/src/net/sf/openrocket/file/wavefrontobj/ObjUtils.java
Normal file
329
core/src/net/sf/openrocket/file/wavefrontobj/ObjUtils.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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*.
|
||||
*/
|
||||
|
32
core/src/net/sf/openrocket/util/RocketComponentUtils.java
Normal file
32
core/src/net/sf/openrocket/util/RocketComponentUtils.java
Normal 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;
|
||||
}
|
||||
}
|
193
core/test/net/sf/openrocket/file/wavefrontobj/OBJUtilsTest.java
Normal file
193
core/test/net/sf/openrocket/file/wavefrontobj/OBJUtilsTest.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 -->
|
||||
|
@ -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>" +
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
//
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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");
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user