diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties
index 711e7e119..aef08a6e0 100644
--- a/core/resources/l10n/messages.properties
+++ b/core/resources/l10n/messages.properties
@@ -122,6 +122,7 @@ FileHelper.ALL_DESIGNS_FILTER = All rocket designs (*.ork; *.rkt, *.CDX1)
FileHelper.OPENROCKET_DESIGN_FILTER = OpenRocket designs (*.ork)
FileHelper.ROCKSIM_DESIGN_FILTER = RockSim designs (*.rkt)
FileHelper.RASAERO_DESIGN_FILTER = RASAero designs (*.CDX1)
+FileHelper.WAVEFRONT_OBJ_FILTER = Wavefront OBJ 3D file (*.obj)
FileHelper.OPEN_ROCKET_COMPONENT_FILTER = OpenRocket presets (*.orc)
FileHelper.PNG_FILTER = PNG image (*.png)
FileHelper.IMAGES = Image files
@@ -1413,6 +1414,7 @@ SaveRktWarningDialog.donotshow=Do not show this dialog again
saveAs.openrocket.title = Save as OpenRocket ork file
saveAs.rocksim.title = Export as RockSim rkt file
saveAs.rasaero.title = Export as RASAero CDX1 file
+saveAs.wavefront.title = Export as Wavefront OBJ file
! RASAero exporting
RASAeroExport.warning1 = Nose cone may not be flipped.
@@ -1481,6 +1483,27 @@ StorageOptChooser.lbl.info3 = Smallest file size
StorageOptChooser.ttip.Saveopt = Save options
StorageOptChooser.lbl.Saveopt = Save options
+! OBJOptionChooser
+OBJOptionChooser.checkbox.exportChildren = Export children
+OBJOptionChooser.checkbox.exportChildren.ttip = If true, export children of the selected components as well.
+OBJOptionChooser.checkbox.exportChildren.assemblies.ttip = Component assemblies always export their children
+OBJOptionChooser.checkbox.exportAppearance = Export appearance
+OBJOptionChooser.checkbox.exportAppearance.ttip = If true, export the component appearances to an MTL file.
+OBJOptionChooser.checkbox.exportAsSeparateFiles = Export as separate files
+OBJOptionChooser.checkbox.exportAsSeparateFiles.ttip = If true, export each component as a separate OBJ file.
+OBJOptionChooser.checkbox.removeOffset = Remove offset
+OBJOptionChooser.checkbox.removeOffset.ttip = If true, remove the offset of the component from the origin.
If false, the component is exported at its original location in the rocket.
+OBJOptionChooser.btn.showAdvanced = Show Advanced options
+OBJOptionChooser.checkbox.triangulate = Triangulate mesh
+OBJOptionChooser.checkbox.triangulate.ttip = If true, triangulate the mesh before exporting (convert all quads or high-order polygons to a triangle).
+OBJOptionChooser.lbl.LevelOfDetail = Level of detail:
+OBJOptionChooser.lbl.LevelOfDetail.ttip = Select the desired level of detail of the geometry export.
+
+! LevelOfDetail
+LevelOfDetail.LOW_QUALITY = Low quality
+LevelOfDetail.NORMAL = Normal quality
+LevelOfDetail.HIGH_QUALITY = High quality
+
! ThrustCurveMotorSelectionPanel
TCMotorSelPan.lbl.Selrocketmotor = Select rocket motor:
TCMotorSelPan.checkbox.hideSimilar = Hide very similar thrust curves
diff --git a/core/src/net/sf/openrocket/document/OpenRocketDocument.java b/core/src/net/sf/openrocket/document/OpenRocketDocument.java
index 47160f751..614e8cec7 100644
--- a/core/src/net/sf/openrocket/document/OpenRocketDocument.java
+++ b/core/src/net/sf/openrocket/document/OpenRocketDocument.java
@@ -3,6 +3,7 @@ package net.sf.openrocket.document;
import java.io.File;
import java.util.*;
+import net.sf.openrocket.file.wavefrontobj.export.OBJExportOptions;
import net.sf.openrocket.rocketcomponent.*;
import net.sf.openrocket.util.StateChangeListener;
import org.slf4j.Logger;
@@ -91,6 +92,7 @@ public class OpenRocketDocument implements ComponentChangeListener, StateChangeL
private int savedID = -1;
private final StorageOptions storageOptions = new StorageOptions();
+ private final OBJExportOptions objOptions;
private final DecalRegistry decalRegistry = new DecalRegistry();
@@ -103,6 +105,7 @@ public class OpenRocketDocument implements ComponentChangeListener, StateChangeL
*/
OpenRocketDocument(Rocket rocket) {
this.rocket = rocket;
+ this.objOptions = new OBJExportOptions(rocket);
rocket.enableEvents();
init();
}
@@ -248,6 +251,10 @@ public class OpenRocketDocument implements ComponentChangeListener, StateChangeL
public StorageOptions getDefaultStorageOptions() {
return storageOptions;
}
+
+ public OBJExportOptions getDefaultOBJOptions() {
+ return objOptions;
+ }
/**
diff --git a/core/src/net/sf/openrocket/document/StorageOptions.java b/core/src/net/sf/openrocket/document/StorageOptions.java
index a1d8f2769..0c33fc725 100644
--- a/core/src/net/sf/openrocket/document/StorageOptions.java
+++ b/core/src/net/sf/openrocket/document/StorageOptions.java
@@ -7,7 +7,8 @@ public class StorageOptions implements Cloneable {
public enum FileType {
OPENROCKET,
ROCKSIM,
- RASAERO
+ RASAERO,
+ WAVEFRONT_OBJ
}
private FileType fileType = FileType.OPENROCKET;
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/ObjUtils.java b/core/src/net/sf/openrocket/file/wavefrontobj/ObjUtils.java
index fd53a6d52..d1963aa6d 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/ObjUtils.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/ObjUtils.java
@@ -4,7 +4,9 @@ import de.javagl.obj.FloatTuple;
import de.javagl.obj.Obj;
import de.javagl.obj.ObjFace;
import de.javagl.obj.ObjGroup;
+import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.startup.Application;
import net.sf.openrocket.util.Coordinate;
import java.util.List;
@@ -15,19 +17,22 @@ import java.util.List;
* @author Sibo Van Gool
*/
public class ObjUtils {
+ private static final Translator trans = Application.getTranslator();
/**
* Level of detail to use for the export.
*/
public enum LevelOfDetail {
- LOW_QUALITY(25),
- NORMAL(60),
- HIGH_QUALITY(100);
+ LOW_QUALITY(25, trans.get("LevelOfDetail.LOW_QUALITY")),
+ NORMAL(60, trans.get("LevelOfDetail.NORMAL")),
+ HIGH_QUALITY(100, trans.get("LevelOfDetail.HIGH_QUALITY"));
private final int value;
+ private final String label;
- LevelOfDetail(int value) {
+ LevelOfDetail(int value, String label) {
this.value = value;
+ this.label = label;
}
public int getValue() {
@@ -44,6 +49,11 @@ public class ObjUtils {
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
}
+
+ @Override
+ public String toString() {
+ return label;
+ }
}
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExportOptions.java b/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExportOptions.java
new file mode 100644
index 000000000..283cd8868
--- /dev/null
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExportOptions.java
@@ -0,0 +1,107 @@
+package net.sf.openrocket.file.wavefrontobj.export;
+
+import net.sf.openrocket.file.wavefrontobj.CoordTransform;
+import net.sf.openrocket.file.wavefrontobj.DefaultCoordTransform;
+import net.sf.openrocket.file.wavefrontobj.ObjUtils;
+import net.sf.openrocket.rocketcomponent.Rocket;
+
+public class OBJExportOptions {
+ /**
+ * If true, export all children of the components as well
+ */
+ private boolean exportChildren;
+ /**
+ * If true, export the appearance of the components to an MTL file.
+ */
+ private boolean exportAppearance;
+ /**
+ * If true, export each component as a separate OBJ file.
+ */
+ private boolean exportAsSeparateFiles;
+ /**
+ * If true, remove the offset of the object so it is centered at the origin (but the bottom of the object is at x=0).
+ * 'x' being the longitudinal axis (depends on the used {@link CoordTransform}).
+ */
+ private boolean removeOffset;
+ /**
+ * If true, triangulate all faces (convert quads and higher-order polygons to triangles)
+ */
+ private boolean triangulate;
+ /**
+ * The level of detail to use for the export (e.g. low-quality, normal quality...).
+ */
+ private ObjUtils.LevelOfDetail LOD;
+ /**
+ * The coordinate transformer to use for the export.
+ * This is used to convert the coordinates from the rocket's coordinate system to the OBJ coordinate system (which is arbitrary).
+ */
+ private CoordTransform transformer;
+
+ // TODO: scaling (to mm = x1000, or SI units)
+
+ public OBJExportOptions(Rocket rocket) {
+ this.exportChildren = false;
+ this.exportAppearance = false;
+ this.exportAsSeparateFiles = false;
+ this.removeOffset = true;
+ this.triangulate = false;
+ this.LOD = ObjUtils.LevelOfDetail.NORMAL;
+ this.transformer = new DefaultCoordTransform(rocket.getLength());
+ }
+
+ public boolean isExportChildren() {
+ return exportChildren;
+ }
+
+ public void setExportChildren(boolean exportChildren) {
+ this.exportChildren = exportChildren;
+ }
+
+ public boolean isRemoveOffset() {
+ return removeOffset;
+ }
+
+ public void setRemoveOffset(boolean removeOffset) {
+ this.removeOffset = removeOffset;
+ }
+
+ public boolean isTriangulate() {
+ return triangulate;
+ }
+
+ public void setTriangulate(boolean triangulate) {
+ this.triangulate = triangulate;
+ }
+
+ public boolean isExportAppearance() {
+ return exportAppearance;
+ }
+
+ public void setExportAppearance(boolean exportAppearance) {
+ this.exportAppearance = exportAppearance;
+ }
+
+ public ObjUtils.LevelOfDetail getLOD() {
+ return LOD;
+ }
+
+ public void setLOD(ObjUtils.LevelOfDetail LOD) {
+ this.LOD = LOD;
+ }
+
+ public CoordTransform getTransformer() {
+ return transformer;
+ }
+
+ public void setTransformer(CoordTransform transformer) {
+ this.transformer = transformer;
+ }
+
+ public boolean isExportAsSeparateFiles() {
+ return exportAsSeparateFiles;
+ }
+
+ public void setExportAsSeparateFiles(boolean exportAsSeparateFiles) {
+ this.exportAsSeparateFiles = exportAsSeparateFiles;
+ }
+}
diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactory.java b/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactory.java
index dd7a9f134..8c35f3e7f 100644
--- a/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactory.java
+++ b/core/src/net/sf/openrocket/file/wavefrontobj/export/OBJExporterFactory.java
@@ -28,6 +28,7 @@ import net.sf.openrocket.rocketcomponent.RingComponent;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.rocketcomponent.Transition;
import net.sf.openrocket.rocketcomponent.TubeFinSet;
+import net.sf.openrocket.util.FileUtils;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -55,12 +56,8 @@ import java.util.Set;
public class OBJExporterFactory {
private final List components;
private final FlightConfiguration configuration;
- 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 OBJExportOptions options;
private final String filePath;
- private final CoordTransform transformer;
// The different exporters for each component
private static final Map, ExporterFactory>> EXPORTER_MAP = Map.of(
@@ -79,38 +76,15 @@ public class OBJExporterFactory {
* NOTE: you must call {@link #doExport()} to actually perform the export.
* @param components List of components to export
* @param configuration Flight configuration to use for the 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 transformer Coordinate system transformer to use to switch from the OpenRocket coordinate system to a custom OBJ coordinate system
+ * @param options Options to use for the export
* @param filePath Path to the file to export to
*/
- public OBJExporterFactory(List components, FlightConfiguration configuration, boolean exportChildren, boolean triangulate,
- boolean removeOffset, ObjUtils.LevelOfDetail LOD, CoordTransform transformer, String filePath) {
+ public OBJExporterFactory(List components, FlightConfiguration configuration, String filePath,
+ OBJExportOptions options) {
this.components = components;
this.configuration = configuration;
- this.exportChildren = exportChildren;
- this.triangulate = triangulate;
- this.removeOffset = removeOffset;
- this.LOD = LOD;
- this.transformer = transformer;
this.filePath = filePath;
- }
-
- /**
- * Wavefront OBJ exporter.
- * @param components List of components to export
- * @param configuration Flight configuration to use for the 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 transformer Coordinate system transformer to use to switch from the OpenRocket coordinate system to a custom OBJ coordinate system
- * @param filePath Path to the file to export to
- */
- public OBJExporterFactory(List components, FlightConfiguration configuration, boolean exportChildren, boolean triangulate,
- boolean removeOffset, CoordTransform transformer, String filePath) {
- this(components, configuration, exportChildren, triangulate, removeOffset, ObjUtils.LevelOfDetail.NORMAL, transformer, filePath);
+ this.options = options;
}
/**
@@ -118,9 +92,11 @@ public class OBJExporterFactory {
*/
public void doExport() {
DefaultObj obj = new DefaultObj();
+ boolean exportAsSeparateFiles = this.options.isExportAsSeparateFiles();
+ // Get all the components to export
Set componentsToExport = new HashSet<>(this.components);
- if (this.exportChildren) {
+ if (this.options.isExportChildren()) {
for (RocketComponent component : this.components) {
componentsToExport.addAll(component.getAllChildren());
}
@@ -142,27 +118,44 @@ public class OBJExporterFactory {
ArrayList contexts = map.get(component);
contexts.get(0).transform.getXrotation();
+ // If separate export, create a new OBJ for each component
+ if (exportAsSeparateFiles) {
+ obj = new DefaultObj();
+ }
+
// Component exporting
- String groupName = component.getName() + "_" + idx;
- handleComponent(obj, this.configuration, this.transformer, component, groupName, this.LOD);
+ String groupName = idx + "_" + component.getName();
+ handleComponent(obj, this.configuration, this.options.getTransformer(), component, groupName, this.options.getLOD());
+
+ // If separate export, already need to write the OBJ here
+ if (exportAsSeparateFiles) {
+ String path = FileUtils.removeExtension(this.filePath) + "_" + groupName + ".obj";
+ writeObj(obj, path);
+ }
idx++;
}
- if (this.triangulate) {
+ if (this.options.isTriangulate()) {
obj = de.javagl.obj.ObjUtils.triangulate(obj, new DefaultObj());
}
- if (this.removeOffset) {
+ if (this.options.isRemoveOffset()) {
// 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, transformer);
+ ObjUtils.removeVertexOffset(obj, this.options.getTransformer());
}
- try (OutputStream objOutputStream = new FileOutputStream(this.filePath)) {
+ if (!exportAsSeparateFiles) {
+ writeObj(obj, this.filePath);
+ }
+ }
+
+ private static void writeObj(DefaultObj obj, String filePath) {
+ try (OutputStream objOutputStream = new FileOutputStream(filePath)) {
ObjWriter.write(obj, objOutputStream);
} catch (IOException e) {
throw new RuntimeException(e);
diff --git a/core/src/net/sf/openrocket/util/FileUtils.java b/core/src/net/sf/openrocket/util/FileUtils.java
index 3956f6b2d..5cfeb3321 100644
--- a/core/src/net/sf/openrocket/util/FileUtils.java
+++ b/core/src/net/sf/openrocket/util/FileUtils.java
@@ -38,4 +38,17 @@ public abstract class FileUtils {
}
+ /**
+ * Remove the extension from a file name.
+ * @param fileName the file name
+ * @return the file name without extension
+ */
+ public static String removeExtension(String fileName) {
+ String[] splitResults = fileName.split("\\.");
+ if (splitResults.length > 0) {
+ return splitResults[0];
+ }
+ return fileName;
+ }
+
}
diff --git a/swing/src/net/sf/openrocket/file/wavefrontobj/OBJOptionChooser.java b/swing/src/net/sf/openrocket/file/wavefrontobj/OBJOptionChooser.java
new file mode 100644
index 000000000..336d8cecd
--- /dev/null
+++ b/swing/src/net/sf/openrocket/file/wavefrontobj/OBJOptionChooser.java
@@ -0,0 +1,141 @@
+package net.sf.openrocket.file.wavefrontobj;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.file.wavefrontobj.export.OBJExportOptions;
+import net.sf.openrocket.l10n.Translator;
+import net.sf.openrocket.rocketcomponent.ComponentAssembly;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.startup.Application;
+
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+import javax.swing.JToggleButton;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.util.List;
+
+public class OBJOptionChooser extends JPanel {
+ private static final Translator trans = Application.getTranslator();
+
+ // Widgets
+ private final JCheckBox exportChildren;
+ private final JCheckBox exportAppearance;
+ private final JCheckBox exportAsSeparateFiles;
+ private final JCheckBox removeOffset;
+ private final JCheckBox triangulate;
+ private final JComboBox LOD;
+
+ public OBJOptionChooser(OBJExportOptions opts, List selectedComponents) {
+ super(new MigLayout());
+
+ // ------------ Basic options ------------
+ //// Export children
+ this.exportChildren = new JCheckBox(trans.get("OBJOptionChooser.checkbox.exportChildren"));
+ this.add(exportChildren, "spanx, wrap");
+
+ //// Export appearance
+ this.exportAppearance = new JCheckBox(trans.get("OBJOptionChooser.checkbox.exportAppearance"));
+ this.exportAppearance.setToolTipText(trans.get("OBJOptionChooser.checkbox.exportAppearance.ttip"));
+ this.add(exportAppearance, "spanx, wrap");
+
+ //// Export as separate files
+ this.exportAsSeparateFiles = new JCheckBox(trans.get("OBJOptionChooser.checkbox.exportAsSeparateFiles"));
+ this.exportAsSeparateFiles.setToolTipText(trans.get("OBJOptionChooser.checkbox.exportAsSeparateFiles.ttip"));
+ this.add(exportAsSeparateFiles, "spanx, wrap");
+
+ //// Remove offsets
+ this.removeOffset = new JCheckBox(trans.get("OBJOptionChooser.checkbox.removeOffset"));
+ this.removeOffset.setToolTipText(trans.get("OBJOptionChooser.checkbox.removeOffset.ttip"));
+ this.add(removeOffset, "spanx, wrap para");
+
+
+ // ------------ Advanced options ------------
+ this.add(new JSeparator(JSeparator.HORIZONTAL), "spanx, growx, wrap");
+
+ // Show advanced options toggle
+ JToggleButton advancedToggle = new JToggleButton(trans.get("OBJOptionChooser.btn.showAdvanced"));
+ this.add(advancedToggle, "spanx, wrap para");
+
+ // Panel for advanced options
+ JPanel advancedOptionsPanel = new JPanel();
+ advancedOptionsPanel.setLayout(new MigLayout("ins 0"));
+ advancedOptionsPanel.setVisible(false);
+
+ //// Triangulate
+ this.triangulate = new JCheckBox(trans.get("OBJOptionChooser.checkbox.triangulate"));
+ this.triangulate.setToolTipText(trans.get("OBJOptionChooser.checkbox.triangulate.ttip"));
+ advancedOptionsPanel.add(triangulate, "spanx, wrap");
+
+
+ //// Level of detail
+ JLabel LODLabel = new JLabel(trans.get("OBJOptionChooser.lbl.LevelOfDetail"));
+ LODLabel.setToolTipText(trans.get("OBJOptionChooser.lbl.LevelOfDetail.ttip"));
+ advancedOptionsPanel.add(LODLabel);
+ this.LOD = new JComboBox<>(ObjUtils.LevelOfDetail.values());
+ this.LOD.setToolTipText(trans.get("OBJOptionChooser.lbl.LevelOfDetail.ttip"));
+ advancedOptionsPanel.add(LOD, "spanx, wrap para");
+
+
+ //// Scale
+ // TODO: + (add tooltip text that mm is useful for 3D printing)
+
+
+ //// Coordinate transformer
+ // TODO
+
+
+ // Add action listener to the toggle button
+ advancedToggle.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ // Toggle the visibility of the advanced options panel
+ advancedOptionsPanel.setVisible(e.getStateChange() == ItemEvent.SELECTED);
+
+ // Refresh the UI after changing visibility
+ OBJOptionChooser.this.revalidate();
+ OBJOptionChooser.this.repaint();
+ }
+ });
+ this.add(advancedOptionsPanel);
+
+ loadOptions(opts, selectedComponents);
+ }
+
+ public void loadOptions(OBJExportOptions opts, List selectedComponents) {
+ boolean onlyComponentAssemblies = true;
+ for (RocketComponent component : selectedComponents) {
+ if (!(component instanceof ComponentAssembly)) {
+ onlyComponentAssemblies = false;
+ break;
+ }
+ }
+ if (onlyComponentAssemblies) {
+ exportChildren.setEnabled(false);
+ exportChildren.setSelected(true);
+ exportChildren.setToolTipText(trans.get("OBJOptionChooser.checkbox.exportChildren.assemblies.ttip"));
+ } else {
+ exportChildren.setEnabled(true);
+ exportChildren.setSelected(opts.isExportChildren());
+ exportChildren.setToolTipText(trans.get("OBJOptionChooser.checkbox.exportChildren.ttip"));
+ }
+
+ this.exportAppearance.setSelected(opts.isExportAppearance());
+ this.exportAsSeparateFiles.setSelected(opts.isExportAsSeparateFiles());
+ this.removeOffset.setSelected(opts.isRemoveOffset());
+ this.triangulate.setSelected(opts.isTriangulate());
+
+ this.LOD.setSelectedItem(opts.getLOD());
+ }
+
+ public void storeOptions(OBJExportOptions opts) {
+ opts.setExportChildren(exportChildren.isSelected());
+ opts.setExportAppearance(exportAppearance.isSelected());
+ opts.setExportAsSeparateFiles(exportAsSeparateFiles.isSelected());
+ opts.setRemoveOffset(removeOffset.isSelected());
+ opts.setTriangulate(triangulate.isSelected());
+ opts.setLOD((ObjUtils.LevelOfDetail) LOD.getSelectedItem());
+ }
+}
diff --git a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
index b5ad0b89e..04ad5e366 100644
--- a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
+++ b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java
@@ -51,6 +51,8 @@ import javax.swing.tree.TreeSelectionModel;
import net.miginfocom.swing.MigLayout;
import net.sf.openrocket.file.wavefrontobj.CoordTransform;
import net.sf.openrocket.file.wavefrontobj.DefaultCoordTransform;
+import net.sf.openrocket.file.wavefrontobj.OBJOptionChooser;
+import net.sf.openrocket.file.wavefrontobj.export.OBJExportOptions;
import net.sf.openrocket.file.wavefrontobj.export.OBJExporterFactory;
import net.sf.openrocket.gui.configdialog.SaveDesignInfoPanel;
import net.sf.openrocket.gui.dialogs.ErrorWarningDialog;
@@ -1387,10 +1389,15 @@ public class BasicFrame extends JFrame {
/**
* Opens a file chooser dialog for saving a new file, and returns the selected file.
* @param fileType file type to use (e.g. RASAero)
+ * @param selectedComponents list of selected components in the design
* @return the file selected from the dialog, or null if no file was selected.
*/
- private File openFileSaveAsDialog(FileType fileType) {
- final DesignFileSaveAsFileChooser chooser = DesignFileSaveAsFileChooser.build(document, fileType);
+ private File openFileSaveAsDialog(FileType fileType, List selectedComponents) {
+ final DesignFileSaveAsFileChooser chooser = DesignFileSaveAsFileChooser.build(document, fileType, selectedComponents);
+ OBJOptionChooser objChooser = null;
+ if (chooser.getAccessory() instanceof OBJOptionChooser) {
+ objChooser = (OBJOptionChooser) chooser.getAccessory();
+ }
int option = chooser.showSaveDialog(BasicFrame.this);
if (option != JFileChooser.APPROVE_OPTION) {
@@ -1398,6 +1405,10 @@ public class BasicFrame extends JFrame {
return null;
}
+ if (objChooser != null) {
+ objChooser.storeOptions(document.getDefaultOBJOptions());
+ }
+
File file = chooser.getSelectedFile();
if (file == null) {
log.info(Markers.USER_MARKER, "User did not select a file");
@@ -1409,6 +1420,15 @@ public class BasicFrame extends JFrame {
return file;
}
+ /**
+ * Opens a file chooser dialog for saving a new file, and returns the selected file.
+ * @param fileType file type to use (e.g. RASAero)
+ * @return the file selected from the dialog, or null if no file was selected.
+ */
+ private File openFileSaveAsDialog(FileType fileType) {
+ return openFileSaveAsDialog(fileType, null);
+ }
+
//// BEGIN RASAero Save/Export Action
/**
@@ -1608,13 +1628,42 @@ public class BasicFrame extends JFrame {
//// 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 save the rocket dimensions in SI units or in mm (add tooltip text that mm is useful for 3D printing) etc.)
- String filePath = "/Users/SiboVanGool/Downloads/test.obj";
- CoordTransform transformer = new DefaultCoordTransform(rocket.getLength());
+ /**
+ * MODEL "Export as" Wavefront OBJ file format
+ *
+ * @return true if the file was saved, false otherwise
+ */
+ private boolean exportWavefrontOBJAction() {
+ File file = openFileSaveAsDialog(FileType.WAVEFRONT_OBJ, getSelectedComponents());
+ if (file == null) {
+ return false;
+ }
+
+ file = FileHelper.forceExtension(file, "obj");
+ boolean isExportAsSeparateFiles = document.getDefaultOBJOptions().isExportAsSeparateFiles();
+ if (isExportAsSeparateFiles || FileHelper.confirmWrite(file, BasicFrame.this)) { // No overwrite warning for separate files
+ return saveAsWavefrontOBJ(file);
+ }
+ return false;
+ }
+
+ private boolean saveAsWavefrontOBJ(File file) {
+ OBJExportOptions options = document.getDefaultOBJOptions();
+ return saveWavefrontOBJFile(file, options);
+ }
+
+ /**
+ * Perform the actual saving of the Wavefront OBJ file
+ * @param file file to be stored
+ * @param options OBJ export options to use
+ * @return true if the file was written
+ */
+ private boolean saveWavefrontOBJFile(File file, OBJExportOptions options) {
OBJExporterFactory exporter = new OBJExporterFactory(getSelectedComponents(), rocket.getSelectedConfiguration(),
- false, false, true, transformer, filePath);
+ file.getAbsolutePath(), options);
exporter.doExport();
+
+ return true;
}
diff --git a/swing/src/net/sf/openrocket/gui/main/DesignFileSaveAsFileChooser.java b/swing/src/net/sf/openrocket/gui/main/DesignFileSaveAsFileChooser.java
index d4545a7d6..4bf4d8832 100644
--- a/swing/src/net/sf/openrocket/gui/main/DesignFileSaveAsFileChooser.java
+++ b/swing/src/net/sf/openrocket/gui/main/DesignFileSaveAsFileChooser.java
@@ -3,6 +3,7 @@ package net.sf.openrocket.gui.main;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
+import java.util.List;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileFilter;
@@ -10,12 +11,15 @@ import javax.swing.filechooser.FileFilter;
import net.sf.openrocket.document.OpenRocketDocument;
import net.sf.openrocket.document.StorageOptions;
import net.sf.openrocket.document.StorageOptions.FileType;
+import net.sf.openrocket.file.wavefrontobj.OBJOptionChooser;
import net.sf.openrocket.gui.util.FileHelper;
import net.sf.openrocket.gui.util.SimpleFileFilter;
import net.sf.openrocket.gui.util.SwingPreferences;
import net.sf.openrocket.gui.widgets.SaveFileChooser;
import net.sf.openrocket.l10n.Translator;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.FileUtils;
public class DesignFileSaveAsFileChooser extends SaveFileChooser {
@@ -24,11 +28,15 @@ public class DesignFileSaveAsFileChooser extends SaveFileChooser {
private static final Translator trans = Application.getTranslator();
- public static DesignFileSaveAsFileChooser build(OpenRocketDocument document, FileType type ) {
- return new DesignFileSaveAsFileChooser(document,type);
+ public static DesignFileSaveAsFileChooser build(OpenRocketDocument document, FileType type) {
+ return new DesignFileSaveAsFileChooser(document, type, null);
}
- private DesignFileSaveAsFileChooser(OpenRocketDocument document, FileType type ) {
+ public static DesignFileSaveAsFileChooser build(OpenRocketDocument document, FileType type, List selectedComponents) {
+ return new DesignFileSaveAsFileChooser(document, type, selectedComponents);
+ }
+
+ private DesignFileSaveAsFileChooser(OpenRocketDocument document, FileType type, List selectedComponents) {
this.document = document;
this.type = type;
@@ -58,6 +66,14 @@ public class DesignFileSaveAsFileChooser extends SaveFileChooser {
this.addChoosableFileFilter(FileHelper.RASAERO_DESIGN_FILTER);
this.setFileFilter(FileHelper.RASAERO_DESIGN_FILTER);
break;
+ case WAVEFRONT_OBJ:
+ defaultFilename = FileHelper.forceExtension(defaultFilename,"obj");
+ this.setDialogTitle(trans.get("saveAs.wavefront.title"));
+ OBJOptionChooser objChooser = new OBJOptionChooser(document.getDefaultOBJOptions(), selectedComponents);
+ this.setAccessory(objChooser);
+ this.addChoosableFileFilter(FileHelper.WAVEFRONT_OBJ_FILTER);
+ this.setFileFilter(FileHelper.WAVEFRONT_OBJ_FILTER);
+ break;
}
final RememberFilenamePropertyListener listener = new RememberFilenamePropertyListener();
@@ -108,19 +124,11 @@ class RememberFilenamePropertyListener implements PropertyChangeListener {
String currentFileName = oldFileName;
if (!filter.accept(new File(currentFileName))) {
- currentFileName = removeExtension(currentFileName);
+ currentFileName = FileUtils.removeExtension(currentFileName);
chooser.setSelectedFile(new File(currentFileName + desiredExtension));
}
}
}
-
- private String removeExtension(String fileName) {
- String[] splitResults = fileName.split("\\.");
- if (splitResults.length > 0) {
- return splitResults[0];
- }
- return fileName;
- }
}
diff --git a/swing/src/net/sf/openrocket/gui/util/FileHelper.java b/swing/src/net/sf/openrocket/gui/util/FileHelper.java
index c2bc914d6..1450def77 100644
--- a/swing/src/net/sf/openrocket/gui/util/FileHelper.java
+++ b/swing/src/net/sf/openrocket/gui/util/FileHelper.java
@@ -48,6 +48,10 @@ public final class FileHelper {
public static final FileFilter RASAERO_DESIGN_FILTER =
new SimpleFileFilter(trans.get("FileHelper.RASAERO_DESIGN_FILTER"), ".CDX1", ".CDX1.gz");
+ /** File filter for RASAero II designs (*.CDX1) */
+ public static final FileFilter WAVEFRONT_OBJ_FILTER =
+ new SimpleFileFilter(trans.get("FileHelper.WAVEFRONT_OBJ_FILTER"), ".obj");
+
/** File filter for OpenRocket components and presets (*.orc) */
public static final FileFilter OPEN_ROCKET_COMPONENT_FILTER =
new SimpleFileFilter(trans.get("FileHelper.OPEN_ROCKET_COMPONENT_FILTER"), ".orc", ".orc.gz");