diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 3e5148004..9942047e7 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -1505,6 +1505,12 @@ OBJOptionChooser.lbl.Scaling = Scaling: OBJOptionChooser.lbl.Scaling.ttip = Scale the exported geometry by the given factor.
The default dimensions are in SI units (meters), but e.g. 3D printing slicer software often uses mm.
In that scenario, you can set the scale to '1000'. OBJOptionChooser.lbl.LevelOfDetail = Level of detail: OBJOptionChooser.lbl.LevelOfDetail.ttip = Select the desired level of detail of the geometry export. +OBJOptionChooser.lbl.CoordinateTransform = Coordinate transform: +OBJOptionChooser.lbl.CoordinateTransform.ttip = Define how to OpenRocket axes are mapped to the OBJ axes. +OBJOptionChooser.lbl.CoordinateTransform.Axial = Axial (up) axis: +OBJOptionChooser.lbl.CoordinateTransform.Axial.ttip = Select the OBJ axis that should correspond to the axial (up) axis of the OpenRocket model. +OBJOptionChooser.lbl.CoordinateTransform.Forward = Forward axis: +OBJOptionChooser.lbl.CoordinateTransform.Forward.ttip = Select the OBJ axis that should correspond to the forward axis of the OpenRocket model. ! LevelOfDetail LevelOfDetail.LOW_QUALITY = Low quality diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/Axis.java b/core/src/net/sf/openrocket/file/wavefrontobj/Axis.java index 4a57431da..ab3166715 100644 --- a/core/src/net/sf/openrocket/file/wavefrontobj/Axis.java +++ b/core/src/net/sf/openrocket/file/wavefrontobj/Axis.java @@ -46,4 +46,34 @@ public enum Axis { default -> throw new IllegalArgumentException("Unknown axis: " + axis); }; } + + private static boolean isXAxis(Axis axis) { + return axis == X || axis == X_MIN; + } + + private static boolean isYAxis(Axis axis) { + return axis == Y || axis == Y_MIN; + } + + private static boolean isZAxis(Axis axis) { + return axis == Z || axis == Z_MIN; + } + + /** + * Get the third axis given two axes, using a right-handed coordinate system. + * @param firstAxis The first axis. + * @param thirdAxis The third axis. + * @return The third axis. + */ + public static Axis getThirdAxis(Axis firstAxis, Axis thirdAxis) { + if (isXAxis(firstAxis) && isYAxis(thirdAxis) || isYAxis(firstAxis) && isXAxis(thirdAxis)) { + return Z; + } else if (isXAxis(firstAxis) && isZAxis(thirdAxis) || isZAxis(firstAxis) && isXAxis(thirdAxis)) { + return Y; + } else if (isYAxis(firstAxis) && isZAxis(thirdAxis) || isZAxis(firstAxis) && isYAxis(thirdAxis)) { + return X; + } else { + throw new IllegalArgumentException("Unknown axis: " + firstAxis + ", " + thirdAxis); + } + } } diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/CoordTransform.java b/core/src/net/sf/openrocket/file/wavefrontobj/CoordTransform.java index 6551a1135..547b76384 100644 --- a/core/src/net/sf/openrocket/file/wavefrontobj/CoordTransform.java +++ b/core/src/net/sf/openrocket/file/wavefrontobj/CoordTransform.java @@ -7,8 +7,8 @@ import net.sf.openrocket.util.Coordinate; /** * Interface for classes that can convert location and rotation coordinates from the OpenRocket coordinate system * to a custom OBJ coordinate system. - * OpenRocket uses a left-handed coordinate system with the y-axis pointing up, the z-axis pointing away from the viewer, - * and the x-axis pointing to the right (in the side view). Its origin is also at the tip of the rocket. + * OpenRocket uses a left-handed coordinate system with the forward (y-)axis pointing up, the depth (z-)axis pointing away from the viewer, + * and the longitudinal (x-)axis pointing to the right (in the side view). Its origin is also at the tip of the rocket. * * @author Sibo Van Gool */ @@ -17,7 +17,6 @@ public class CoordTransform { protected final Axis xAxis; protected final Axis yAxis; protected final Axis zAxis; - protected final Axis axialAxis; // Origin offsets protected final double origXOffs; @@ -34,16 +33,13 @@ public class CoordTransform { * of the OpenRocket x-axis), then you must pass Axis.X_MIN as the xAxis parameter. * You must also add an offset to the origin of the transformed coordinate system, so that it starts * at the bottom of the rocket => set origZOffs to the length of the rocket. - * @param axialAxis the axial/longitudinal axis in the transformed coordinate system, with the direction - * relative to the OpenRocket x-axis !! - * From the previous example, the longitudinal axis would be Axis.Z_MIN. * @param origXOffs the x-offset of the origin of the OBJ coordinate system, in the OpenRocket coordinate system * @param origYOffs the y-offset of the origin of the OBJ coordinate system, in the OpenRocket coordinate system * @param origZOffs the z-offset of the origin of the OBJ coordinate system, in the OpenRocket coordinate system */ - public CoordTransform(@NotNull Axis xAxis, @NotNull Axis yAxis, @NotNull Axis zAxis, @NotNull Axis axialAxis, + public CoordTransform(@NotNull Axis xAxis, @NotNull Axis yAxis, @NotNull Axis zAxis, double origXOffs, double origYOffs, double origZOffs) { - if (xAxis == null || yAxis == null || zAxis == null || axialAxis == null) { + if (xAxis == null || yAxis == null || zAxis == null) { throw new IllegalArgumentException("Axes cannot be null"); } @@ -54,13 +50,72 @@ public class CoordTransform { this.xAxis = xAxis; this.yAxis = yAxis; this.zAxis = zAxis; - this.axialAxis = axialAxis; this.origXOffs = origXOffs; this.origYOffs = origYOffs; this.origZOffs = origZOffs; } + /** + * Create a new coordinate system converter. + * @param axialAxis the OBJ axis that corresponds to the OpenRocket axial (x-)axis + * @param forwardAxis the OBJ axis that corresponds to the OpenRocket forward (y-)axis + * @param origXOffs the x-offset of the origin of the OBJ coordinate system, in the OBJ coordinate system + * @param origYOffs the y-offset of the origin of the OBJ coordinate system, in the OBJ coordinate system + * @param origZOffs the z-offset of the origin of the OBJ coordinate system, in the OBJ coordinate system + */ + public static CoordTransform generateUsingLongitudinalAndForwardAxes(Axis axialAxis, Axis forwardAxis, + double origXOffs, double origYOffs, double origZOffs) { + if (axialAxis == null || forwardAxis == null) { + throw new IllegalArgumentException("Axes cannot be null"); + } + + if (axialAxis.isSameAxis(forwardAxis)) { + throw new IllegalArgumentException("Axes must be different"); + } + + Axis xAxis = null; + Axis yAxis = null; + Axis zAxis = null; + + switch (axialAxis) { + case X -> xAxis = Axis.X; + case X_MIN -> xAxis = Axis.X_MIN; + case Y -> yAxis = Axis.X; + case Y_MIN -> yAxis = Axis.X_MIN; + case Z -> zAxis = Axis.X; + case Z_MIN -> zAxis = Axis.X_MIN; + } + switch (forwardAxis) { + case X -> xAxis = Axis.Y; + case X_MIN -> xAxis = Axis.Y_MIN; + case Y -> yAxis = Axis.Y; + case Y_MIN -> yAxis = Axis.Y_MIN; + case Z -> zAxis = Axis.Y; + case Z_MIN -> zAxis = Axis.Y_MIN; + } + + Axis depthAxis = Axis.getThirdAxis(axialAxis, forwardAxis); + switch (depthAxis) { + case X -> xAxis = Axis.Z; + case X_MIN -> xAxis = Axis.Z_MIN; + case Y -> yAxis = Axis.Z; + case Y_MIN -> yAxis = Axis.Z_MIN; + case Z -> zAxis = Axis.Z; + case Z_MIN -> zAxis = Axis.Z_MIN; + } + + if (xAxis == null || yAxis == null || zAxis == null) { + throw new IllegalStateException("Axes should not be null"); + } + + final double origXTrans = getTransformedOriginOffset(xAxis, origXOffs, origYOffs, origZOffs); + final double origYTrans = getTransformedOriginOffset(yAxis, origXOffs, origYOffs, origZOffs); + final double origZTrans = getTransformedOriginOffset(zAxis, origXOffs, origYOffs, origZOffs); + + return new CoordTransform(xAxis, yAxis, zAxis, origXTrans, origYTrans, origZTrans); + } + private FloatTuple convertLoc(double x, double y, double z, double origXOffs, double origYOffs, double origZOffs) { @@ -147,7 +202,7 @@ public class CoordTransform { * @param z the z-coordinate in the OpenRocket coordinate system * @return the coordinate in the transformed OBJ coordinate system */ - private double getTransformedCoordinate(Axis axis, double x, double y, double z) { + private static double getTransformedCoordinate(Axis axis, double x, double y, double z) { return switch (axis) { case X -> x; case X_MIN -> -x; @@ -164,7 +219,7 @@ public class CoordTransform { * @param axis the axis to get the offset for * @return the offset of the origin of the OBJ coordinate system for the given axis */ - private double getTransformedOriginOffset(Axis axis, double origXOffs, double origYOffs, double origZOffs) { + private static double getTransformedOriginOffset(Axis axis, double origXOffs, double origYOffs, double origZOffs) { return switch (axis) { case X, X_MIN -> origXOffs; case Y, Y_MIN -> origYOffs; @@ -181,7 +236,7 @@ public class CoordTransform { * @param rotZ the rotation in radians around the OpenRocket z-axis * @return the rotation in radians around the transformed OBJ axis */ - private double getTransformedRotation(Axis axis, double rotX, double rotY, double rotZ) { + private static double getTransformedRotation(Axis axis, double rotX, double rotY, double rotZ) { // OpenRocket uses left-handed coordinate system, we'll use right-handed return switch (axis) { case X -> -rotX; @@ -214,7 +269,61 @@ public class CoordTransform { * @return the equivalent axis for the x-axis in the OpenRocket coordinate system (axial axis) */ public Axis getAxialAxis() { - return axialAxis; + switch (xAxis) { + case X -> { + return Axis.X; + } + case X_MIN -> { + return Axis.X_MIN; + } + } + switch (yAxis) { + case X -> { + return Axis.Y; + } + case X_MIN -> { + return Axis.Y_MIN; + } + } + switch (zAxis) { + case X -> { + return Axis.Z; + } + case X_MIN -> { + return Axis.Z_MIN; + } + } + + throw new IllegalStateException("No axial axis found"); + } + + public Axis getForwardAxis() { + switch (xAxis) { + case Y -> { + return Axis.X; + } + case Y_MIN -> { + return Axis.X_MIN; + } + } + switch (yAxis) { + case Y -> { + return Axis.Y; + } + case Y_MIN -> { + return Axis.Y_MIN; + } + } + switch (zAxis) { + case Y -> { + return Axis.Z; + } + case Y_MIN -> { + return Axis.Z_MIN; + } + } + + throw new IllegalStateException("No forward axis found"); } public double getOrigXOffs() { diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/DefaultCoordTransform.java b/core/src/net/sf/openrocket/file/wavefrontobj/DefaultCoordTransform.java index 8055b1836..9ab625751 100644 --- a/core/src/net/sf/openrocket/file/wavefrontobj/DefaultCoordTransform.java +++ b/core/src/net/sf/openrocket/file/wavefrontobj/DefaultCoordTransform.java @@ -11,6 +11,6 @@ package net.sf.openrocket.file.wavefrontobj; */ public class DefaultCoordTransform extends CoordTransform { public DefaultCoordTransform(double rocketLength) { - super(Axis.Y, Axis.Z, Axis.X_MIN, Axis.Z_MIN, 0, 0, rocketLength); + super(Axis.Y, Axis.Z, Axis.X_MIN, 0, 0, rocketLength); } } diff --git a/core/src/net/sf/openrocket/startup/Preferences.java b/core/src/net/sf/openrocket/startup/Preferences.java index 32080941e..ce1951c56 100644 --- a/core/src/net/sf/openrocket/startup/Preferences.java +++ b/core/src/net/sf/openrocket/startup/Preferences.java @@ -133,7 +133,6 @@ public abstract class Preferences implements ChangeSource { private static final String OBJ_X_AXIS = "xAxis"; private static final String OBJ_Y_AXIS = "yAxis"; private static final String OBJ_Z_AXIS = "zAxis"; - private static final String OBJ_AXIAL_AXIS = "AxialAxis"; private static final String OBJ_ORIG_X_OFFS = "OrigXOffs"; private static final String OBJ_ORIG_Y_OFFS = "OrigYOffs"; private static final String OBJ_ORIG_Z_OFFS = "OrigZOffs"; @@ -1056,7 +1055,6 @@ public abstract class Preferences implements ChangeSource { coordTransformNode.put(OBJ_X_AXIS, transform.getXAxis().toString()); coordTransformNode.put(OBJ_Y_AXIS, transform.getYAxis().toString()); coordTransformNode.put(OBJ_Z_AXIS, transform.getZAxis().toString()); - coordTransformNode.put(OBJ_AXIAL_AXIS, transform.getAxialAxis().toString()); coordTransformNode.putDouble(OBJ_ORIG_X_OFFS, transform.getOrigXOffs()); coordTransformNode.putDouble(OBJ_ORIG_Y_OFFS, transform.getOrigYOffs()); coordTransformNode.putDouble(OBJ_ORIG_Z_OFFS, transform.getOrigZOffs()); @@ -1084,12 +1082,11 @@ public abstract class Preferences implements ChangeSource { Axis xAxis = Axis.fromString(coordTransformNode.get(OBJ_X_AXIS, Axis.Y.toString())); Axis yAxis = Axis.fromString(coordTransformNode.get(OBJ_Y_AXIS, Axis.Z.toString())); Axis zAxis = Axis.fromString(coordTransformNode.get(OBJ_Z_AXIS, Axis.X_MIN.toString())); - Axis axialAxis = Axis.fromString(coordTransformNode.get(OBJ_AXIAL_AXIS, Axis.Z_MIN.toString())); double origXOffs = coordTransformNode.getDouble(OBJ_ORIG_X_OFFS, 0.0); double origYOffs = coordTransformNode.getDouble(OBJ_ORIG_Y_OFFS, 0.0); double origZOffs = coordTransformNode.getDouble(OBJ_ORIG_Z_OFFS, rocket.getLength()); - CoordTransform transform = new CoordTransform(xAxis, yAxis, zAxis, axialAxis, origXOffs, origYOffs, origZOffs); + CoordTransform transform = new CoordTransform(xAxis, yAxis, zAxis, origXOffs, origYOffs, origZOffs); options.setTransformer(transform); return options; diff --git a/swing/src/net/sf/openrocket/file/wavefrontobj/OBJOptionChooser.java b/swing/src/net/sf/openrocket/file/wavefrontobj/OBJOptionChooser.java index aa6f9e45b..14717d736 100644 --- a/swing/src/net/sf/openrocket/file/wavefrontobj/OBJOptionChooser.java +++ b/swing/src/net/sf/openrocket/file/wavefrontobj/OBJOptionChooser.java @@ -7,6 +7,7 @@ import net.sf.openrocket.gui.adaptors.DoubleModel; import net.sf.openrocket.gui.util.GUIUtil; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.rocketcomponent.ComponentAssembly; +import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.UnitGroup; @@ -35,13 +36,19 @@ public class OBJOptionChooser extends JPanel { private final JCheckBox sRGB; private final JComboBox LOD; private final DoubleModel scalingModel; + private final JComboBox axialCombo; + private final JComboBox forwardCombo; private final List selectedComponents; + private final Rocket rocket; - public OBJOptionChooser(OBJExportOptions opts, List selectedComponents) { - super(new MigLayout()); + private boolean isProgrammaticallyChanging = false; + + public OBJOptionChooser(OBJExportOptions opts, List selectedComponents, Rocket rocket) { + super(new MigLayout("hidemode 3")); this.selectedComponents = selectedComponents; + this.rocket = rocket; // ------------ Basic options ------------ //// Export children @@ -102,11 +109,48 @@ public class OBJOptionChooser extends JPanel { advancedOptionsPanel.add(LODLabel, "spanx, split 2"); this.LOD = new JComboBox<>(ObjUtils.LevelOfDetail.values()); this.LOD.setToolTipText(trans.get("OBJOptionChooser.lbl.LevelOfDetail.ttip")); - advancedOptionsPanel.add(LOD, "growx, wrap para"); + advancedOptionsPanel.add(LOD, "growx, wrap unrel"); //// Coordinate transformer - // TODO + JLabel coordTransLabel = new JLabel(trans.get("OBJOptionChooser.lbl.CoordinateTransform")); + coordTransLabel.setToolTipText(trans.get("OBJOptionChooser.lbl.CoordinateTransform.ttip")); + advancedOptionsPanel.add(coordTransLabel, "spanx, wrap"); + + ////// Axial (up) axis + JLabel axialLabel = new JLabel(trans.get("OBJOptionChooser.lbl.CoordinateTransform.Axial")); + axialLabel.setToolTipText(trans.get("OBJOptionChooser.lbl.CoordinateTransform.Axial.ttip")); + advancedOptionsPanel.add(axialLabel, "gapleft 10lp"); + this.axialCombo = new JComboBox<>(Axis.values()); + this.axialCombo.setToolTipText(trans.get("OBJOptionChooser.lbl.CoordinateTransform.Axial.ttip")); + advancedOptionsPanel.add(axialCombo, "wrap"); + + ////// Forward axis + JLabel forwardLabel = new JLabel(trans.get("OBJOptionChooser.lbl.CoordinateTransform.Forward")); + forwardLabel.setToolTipText(trans.get("OBJOptionChooser.lbl.CoordinateTransform.Forward.ttip")); + advancedOptionsPanel.add(forwardLabel, "gapleft 10lp"); + this.forwardCombo = new JComboBox<>(Axis.values()); + this.forwardCombo.setToolTipText(trans.get("OBJOptionChooser.lbl.CoordinateTransform.Forward.ttip")); + advancedOptionsPanel.add(forwardCombo, "wrap"); + + //// Set up the listeners for the coordinate transformer combo boxes + this.axialCombo.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (!isProgrammaticallyChanging) { + coordTransComboAction(e, forwardCombo); + } + } + }); + // Let's just keep all the options for the axial axis, and only remove the forward axis options + /*this.forwardCombo.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (!isProgrammaticallyChanging) { + coordTransComboAction(e, axialCombo); + } + } + });*/ // Add action listener to the toggle button @@ -153,6 +197,10 @@ public class OBJOptionChooser extends JPanel { this.scalingModel.setValue(opts.getScaling()); this.LOD.setSelectedItem(opts.getLOD()); + + CoordTransform transformer = opts.getTransformer(); + this.axialCombo.setSelectedItem(transformer.getAxialAxis()); + this.forwardCombo.setSelectedItem(transformer.getForwardAxis()); } /** @@ -174,6 +222,11 @@ public class OBJOptionChooser extends JPanel { opts.setUseSRGB(sRGB.isSelected()); opts.setScaling((float) scalingModel.getValue()); opts.setLOD((ObjUtils.LevelOfDetail) LOD.getSelectedItem()); + + CoordTransform transformer = CoordTransform.generateUsingLongitudinalAndForwardAxes( + (Axis) axialCombo.getSelectedItem(), (Axis) forwardCombo.getSelectedItem(), + rocket.getLength(), 0, 0); + opts.setTransformer(transformer); } private static boolean isOnlyComponentAssembliesSelected(List selectedComponents) { @@ -186,4 +239,37 @@ public class OBJOptionChooser extends JPanel { } return onlyComponentAssemblies; } + + private void coordTransComboAction(ItemEvent e, JComboBox otherCombo) { + if (e.getStateChange() != ItemEvent.SELECTED) { + return; + } + + Axis selected = (Axis) e.getItem(); + Object otherAxis = otherCombo.getSelectedItem(); + if (!(otherAxis instanceof Axis)) { + return; + } + + // Set the flag to denote we're changing combo box items programmatically + this.isProgrammaticallyChanging = true; + + // Change the combobox items to axes that don't conflict with the selected axis + otherCombo.removeAllItems(); + for (Axis axis : Axis.values()) { + if (!axis.isSameAxis(selected)) { + otherCombo.addItem(axis); + } + } + + // Select the first item in the combobox + if (!((Axis) otherAxis).isSameAxis(selected)) { + otherCombo.setSelectedItem(otherAxis); + } else { + otherCombo.setSelectedIndex(0); + } + + // Reset the flag after changes are done + this.isProgrammaticallyChanging = false; + } } diff --git a/swing/src/net/sf/openrocket/gui/main/DesignFileSaveAsFileChooser.java b/swing/src/net/sf/openrocket/gui/main/DesignFileSaveAsFileChooser.java index cb17cb959..d064bd237 100644 --- a/swing/src/net/sf/openrocket/gui/main/DesignFileSaveAsFileChooser.java +++ b/swing/src/net/sf/openrocket/gui/main/DesignFileSaveAsFileChooser.java @@ -72,7 +72,7 @@ public class DesignFileSaveAsFileChooser extends SaveFileChooser { defaultFilename = FileHelper.forceExtension(defaultFilename,"obj"); this.setDialogTitle(trans.get("saveAs.wavefront.title")); OBJExportOptions initialOptions = prefs.loadOBJExportOptions(document.getRocket()); - OBJOptionChooser objChooser = new OBJOptionChooser(initialOptions, selectedComponents); + OBJOptionChooser objChooser = new OBJOptionChooser(initialOptions, selectedComponents, document.getRocket()); this.setAccessory(objChooser); this.addChoosableFileFilter(FileHelper.WAVEFRONT_OBJ_FILTER); this.setFileFilter(FileHelper.WAVEFRONT_OBJ_FILTER);