From 8b150cbd7461b46ec90ea9fbe91892c6c23755b0 Mon Sep 17 00:00:00 2001 From: SiboVG Date: Mon, 29 Jan 2024 22:25:50 +0100 Subject: [PATCH 1/4] Clean up these horrendous castings --- .../net/sf/openrocket/gui/main/SimulationPanel.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java index 0159d3cfa..2aa9c4448 100644 --- a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java @@ -5,7 +5,6 @@ import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; -import java.awt.Font; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; @@ -501,10 +500,11 @@ public class SimulationPanel extends JPanel { return; } - String separator = ((CsvOptionPanel) chooser.getAccessory()).getFieldSeparator(); - int precision = ((CsvOptionPanel) chooser.getAccessory()).getDecimalPlaces(); - boolean isExponentialNotation = ((CsvOptionPanel) chooser.getAccessory()).isExponentialNotation(); - ((CsvOptionPanel) chooser.getAccessory()).storePreferences(); + CsvOptionPanel csvOptions = (CsvOptionPanel) chooser.getAccessory(); + String separator = csvOptions.getFieldSeparator(); + int precision = csvOptions.getDecimalPlaces(); + boolean isExponentialNotation = csvOptions.isExponentialNotation(); + csvOptions.storePreferences(); // Handle some special separator options from CsvOptionPanel if (separator.equals(trans.get("CsvOptionPanel.separator.space"))) { From 74cfa4dde54c68d92f33a058412aac62c3bd612b Mon Sep 17 00:00:00 2001 From: SiboVG Date: Tue, 30 Jan 2024 20:47:27 +0100 Subject: [PATCH 2/4] Allow fins to be exported to SVG --- core/resources/l10n/messages.properties | 9 + .../file/svg/export/SVGBuilder.java | 177 ++++++++++++++++++ .../sf/openrocket/startup/Preferences.java | 41 ++++ .../src/net/sf/openrocket/unit/UnitGroup.java | 12 ++ .../gui/components/SVGOptionPanel.java | 58 ++++++ .../gui/configdialog/FinSetConfig.java | 58 +++++- .../sf/openrocket/gui/util/FileHelper.java | 4 + 7 files changed, 356 insertions(+), 3 deletions(-) create mode 100644 core/src/net/sf/openrocket/file/svg/export/SVGBuilder.java create mode 100644 swing/src/net/sf/openrocket/gui/components/SVGOptionPanel.java diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 5d605f0ce..163318edd 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -127,6 +127,7 @@ 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.SVG_FILTER = SVG files (*.svg) FileHelper.IMAGES = Image files FileHelper.XML_FILTER = XML files (*.xml) @@ -708,6 +709,12 @@ SimExpPan.Col.Unit = Unit CsvOptionPanel.separator.space = SPACE CsvOptionPanel.separator.tab = TAB +!SVGOptionPanel +SVGOptionPanel.lbl.strokeColor = Stroke color +SVGOptionPanel.lbl.strokeColor.ttip = The color of the lines in the SVG image. +SVGOptionPanel.lbl.strokeWidth = Stroke width (mm) +SVGOptionPanel.lbl.strokeWidth.ttip = The width of the lines in the SVG image in millimeters. + ! Custom expression general stuff customExpression.Name = Name @@ -959,6 +966,8 @@ FinSetConfig.but.Converttofreeform.ttip = Convert this fin set into a freeform f FinSetConfig.Convertfinset = Convert fin set FinSetConfig.but.Splitfins = Split fins FinSetConfig.but.Splitfins.ttip = Split the fin set into separate fins. +FinSetConfig.lbl.exportSVG = Export to SVG +FinSetConfig.lbl.exportSVG.ttip = Export the fin profile to an SVG file. FinSetConfig.but.AutoCalc = Calculate automatically FinSetConfig.but.AutoCalc.ttip = Calculates the height and length of the fin tabs, based on
inner tube and centering ring components of the fin's parent component. FinSetConfig.lbl.Through-the-wall = Through-the-wall fin tabs: diff --git a/core/src/net/sf/openrocket/file/svg/export/SVGBuilder.java b/core/src/net/sf/openrocket/file/svg/export/SVGBuilder.java new file mode 100644 index 000000000..40726f58f --- /dev/null +++ b/core/src/net/sf/openrocket/file/svg/export/SVGBuilder.java @@ -0,0 +1,177 @@ +package net.sf.openrocket.file.svg.export; + +import net.sf.openrocket.util.Coordinate; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.awt.Color; +import java.io.File; +import java.util.Locale; + +/** + * SVGBuilder is a class that allows you to build SVG (Scalable Vector Graphics) files. + * The functionality is limited to the bare minimum needed to export shapes from OpenRocket. + * + * @author Sibo Van Gool + */ +public class SVGBuilder { + private final Document doc; + private final Element svgRoot; + + private double minX = Double.MAX_VALUE; + private double minY = Double.MAX_VALUE; + private double maxX = Double.MIN_VALUE; + private double maxY = Double.MIN_VALUE; + + /** + * Different stroke cap styles. + */ + public enum LineCap { + BUTT("butt"), // Stroke does not extend beyond the end of the line + ROUND("round"), // Stroke extends beyond the end of the line by a half circle + SQUARE("square"); // Stroke extends beyond the end of the line by half the stroke width + + private final String value; + + LineCap(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + private static final double OR_UNIT_TO_SVG_UNIT = 1000; // OpenRocket units are in meters, SVG units are in mm + + /** + * Creates a new SVGBuilder instance. + * + * @throws ParserConfigurationException if a DocumentBuilder cannot be created + */ + public SVGBuilder() throws ParserConfigurationException { + DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); + + // Root element + this.doc = docBuilder.newDocument(); + this.svgRoot = this.doc.createElement("svg"); + this.svgRoot.setAttribute("xmlns", "http://www.w3.org/2000/svg"); + this.svgRoot.setAttribute("version", "1.1"); + this.doc.appendChild(this.svgRoot); + } + + /** + * Adds a path to the SVG document. + * The path is defined by a list of coordinates, where each coordinate represents a point on the path. + * + * @param coordinates the array of coordinates defining the path (coordinates are in meters) + * @param xPos the offset x-axis position of the path (coordinates are in meters) + * @param yPos the offset y-axis position of the path (coordinates are in meters) + * @param fill the color used to fill the path, or null if the path should not be filled + * @param stroke the color used to stroke the path, or null if the path should not be stroked + * @param strokeWidth the width of the path stroke (in millimeters) + * @param lineCap the line cap style of the path + */ + public void addPath(Coordinate[] coordinates, double xPos, double yPos, Color fill, Color stroke, double strokeWidth, + LineCap lineCap) { + final Element path = this.doc.createElement("path"); + final StringBuilder dAttribute = new StringBuilder(); + + for (int i = 0; i < coordinates.length; i++) { + final Coordinate coord = coordinates[i]; + double x = (coord.x + xPos) * OR_UNIT_TO_SVG_UNIT; + double y = (coord.y+ yPos) * OR_UNIT_TO_SVG_UNIT; + updateCanvasSize(x, y); + final String command = (i == 0) ? "M" : "L"; + dAttribute.append(String.format(Locale.ENGLISH, "%s%.1f,%.1f ", command, x, y)); // Coordinates are in meters, SVG is in mm + } + + path.setAttribute("d", dAttribute.toString()); + path.setAttribute("fill", colorToString(fill)); + path.setAttribute("stroke", colorToString(stroke)); + path.setAttribute("stroke-width", String.format(Locale.ENGLISH, "%.001f", strokeWidth)); + path.setAttribute("stroke-linecap", lineCap.getValue()); + svgRoot.appendChild(path); + } + + public void addPath(Coordinate[] coordinates, Color fill, Color stroke, double strokeWidth, LineCap lineCap) { + addPath(coordinates, 0, 0, fill, stroke, strokeWidth, lineCap); + } + + public void addPath(Coordinate[] coordinates, Color fill, Color stroke, double strokeWidth) { + addPath(coordinates, fill, stroke, strokeWidth, LineCap.SQUARE); + } + + /** + * Updates the canvas size based on the given coordinates. + * + * @param x the x-coordinate + * @param y the y-coordinate + */ + private void updateCanvasSize(double x, double y) { + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + + /** + * Finalizes the SVG document by setting the width, height and viewBox attributes. + */ + public void finalizeSVG() { + svgRoot.setAttribute("width", (maxX - minX) + "mm"); + svgRoot.setAttribute("height", (maxY - minY) + "mm"); + svgRoot.setAttribute("viewBox", minX + " " + minY + " " + (maxX - minX) + " " + (maxY - minY)); + } + + /** + * Converts a color to an SVG string representation. + * + * @param color the color to convert + * @return the string representation of the color + */ + private String colorToString(Color color) { + return color == null ? + "none" : + String.format("rgb(%d,%d,%d)", color.getRed(), color.getGreen(), color.getBlue()); + } + + /** + * Writes the SVG document to a file. + * @param file the file to write to + * @throws TransformerException if an error occurs while writing the file + */ + public void writeToFile(File file) throws TransformerException { + finalizeSVG(); + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + DOMSource source = new DOMSource(doc); + StreamResult result = new StreamResult(file); + transformer.transform(source, result); + } + + public static void main(String[] args) throws ParserConfigurationException, TransformerException { + SVGBuilder svgBuilder = new SVGBuilder(); + + Coordinate[] coordinates = { + new Coordinate(0, 0), + new Coordinate(0, 0.01), + new Coordinate(0.02, 0.02), + new Coordinate(0.01, 0), + new Coordinate(0, 0)}; + + svgBuilder.addPath(coordinates, null, Color.BLACK, 0.1); + svgBuilder.writeToFile(new File("/Users/SiboVanGool/Downloads/shape.svg")); + } +} diff --git a/core/src/net/sf/openrocket/startup/Preferences.java b/core/src/net/sf/openrocket/startup/Preferences.java index c244276cc..17d70440e 100644 --- a/core/src/net/sf/openrocket/startup/Preferences.java +++ b/core/src/net/sf/openrocket/startup/Preferences.java @@ -1,5 +1,6 @@ package net.sf.openrocket.startup; +import java.awt.Color; import java.util.ArrayList; import java.util.EventListener; import java.util.EventObject; @@ -138,6 +139,10 @@ public abstract class Preferences implements ChangeSource { 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"; + + // SVG export options + public static final String SVG_STROKE_COLOR = "SVGStrokeColor"; + public static final String SVG_STROKE_WIDTH = "SVGStrokeWidth"; private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel(); @@ -1098,6 +1103,42 @@ public abstract class Preferences implements ChangeSource { return options; } + + /** + * Returns the stroke color used for the SVG. + * + * @return the stroke color for the SVG + */ + public Color getSVGStrokeColor() { + return getColor(SVG_STROKE_COLOR, ORColor.fromAWTColor(Color.BLACK)).toAWTColor(); + } + + /** + * Sets the stroke color used for the SVG. + * + * @param c the stroke color to set + */ + public void setSVGStrokeColor(Color c) { + putColor(SVG_STROKE_COLOR, ORColor.fromAWTColor(c)); + } + + /** + * Returns the stroke width used for the SVG in mm. + * + * @return the stroke width for the SVG + */ + public double getSVGStrokeWidth() { + return getDouble(SVG_STROKE_WIDTH, 0.1); + } + + /** + * Sets the stroke width used for the SVG in mm. + * + * @param width the stroke width to set + */ + public void setSVGStrokeWidth(double width) { + putDouble(SVG_STROKE_WIDTH, width); + } /* * Within a holder class so they will load only when needed. diff --git a/core/src/net/sf/openrocket/unit/UnitGroup.java b/core/src/net/sf/openrocket/unit/UnitGroup.java index 86cdda258..933db0280 100644 --- a/core/src/net/sf/openrocket/unit/UnitGroup.java +++ b/core/src/net/sf/openrocket/unit/UnitGroup.java @@ -89,6 +89,8 @@ public class UnitGroup { public static final UnitGroup UNITS_SCALING; + public static final UnitGroup UNITS_STROKE_WIDTH; + public static final Map UNITS; // keys such as "LENGTH", "VELOCITY" public static final Map SIUNITS; // keys such a "m", "m/s" @@ -316,6 +318,12 @@ public class UnitGroup { UNITS_SCALING = new UnitGroup(); UNITS_SCALING.addUnit(new FixedPrecisionUnit("" + ZWSP, 0.1)); // zero-width space + UNITS_STROKE_WIDTH = new UnitGroup(); + UNITS_STROKE_WIDTH.addUnit(new GeneralUnit(1, "mm")); + UNITS_STROKE_WIDTH.addUnit(new GeneralUnit(0.1, MICRO + "m")); + //UNITS_STROKE_WIDTH.addUnit(new GeneralUnit(25.4, "in")); + UNITS_STROKE_WIDTH.addUnit(new GeneralUnit(0.0254, "mil")); + // This is not used by OpenRocket, and not extensively tested: UNITS_FREQUENCY = new UnitGroup(); @@ -354,6 +362,7 @@ public class UnitGroup { map.put("ROUGHNESS", UNITS_ROUGHNESS); map.put("COEFFICIENT", UNITS_COEFFICIENT); map.put("SCALING", UNITS_SCALING); + map.put("STROKE_WIDTH", UNITS_STROKE_WIDTH); map.put("VOLTAGE", UNITS_VOLTAGE); map.put("CURRENT", UNITS_CURRENT); map.put("ENERGY", UNITS_ENERGY); @@ -416,6 +425,7 @@ public class UnitGroup { UNITS_PRESSURE.setDefaultUnit("mbar"); UNITS_RELATIVE.setDefaultUnit("%"); UNITS_ROUGHNESS.setDefaultUnit(MICRO + "m"); + UNITS_STROKE_WIDTH.setDefaultUnit("mm"); } public static void setDefaultImperialUnits() { @@ -445,6 +455,7 @@ public class UnitGroup { UNITS_PRESSURE.setDefaultUnit("mbar"); UNITS_RELATIVE.setDefaultUnit("%"); UNITS_ROUGHNESS.setDefaultUnit("mil"); + UNITS_STROKE_WIDTH.setDefaultUnit("mil"); } public static void resetDefaultUnits() { @@ -485,6 +496,7 @@ public class UnitGroup { UNITS_ROUGHNESS.setDefaultUnit(0); UNITS_COEFFICIENT.setDefaultUnit(0); UNITS_SCALING.setDefaultUnit(0); + UNITS_STROKE_WIDTH.setDefaultUnit(0); UNITS_FREQUENCY.setDefaultUnit(1); } diff --git a/swing/src/net/sf/openrocket/gui/components/SVGOptionPanel.java b/swing/src/net/sf/openrocket/gui/components/SVGOptionPanel.java new file mode 100644 index 000000000..5fd64915a --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/components/SVGOptionPanel.java @@ -0,0 +1,58 @@ +package net.sf.openrocket.gui.components; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.Preferences; +import net.sf.openrocket.unit.UnitGroup; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import java.awt.Color; + +public class SVGOptionPanel extends JPanel { + private static final Translator trans = Application.getTranslator(); + private static final Preferences prefs = Application.getPreferences(); + + private final ColorChooserButton colorChooser; + private double strokeWidth = 0.1; + + public SVGOptionPanel() { + super(new MigLayout()); + + // Stroke color + JLabel label = new JLabel(trans.get("SVGOptionPanel.lbl.strokeColor")); + label.setToolTipText(trans.get("SVGOptionPanel.lbl.strokeColor.ttip")); + add(label); + colorChooser = new ColorChooserButton(prefs.getSVGStrokeColor()); + colorChooser.setToolTipText(trans.get("SVGOptionPanel.lbl.strokeColor.ttip")); + add(colorChooser, "wrap"); + + // Stroke width + label = new JLabel(trans.get("SVGOptionPanel.lbl.strokeWidth")); + label.setToolTipText(trans.get("SVGOptionPanel.lbl.strokeWidth.ttip")); + add(label); + DoubleModel dm = new DoubleModel(this, "StrokeWidth", UnitGroup.UNITS_STROKE_WIDTH, 0.001, 10); + dm.setValue(prefs.getSVGStrokeWidth()); + JSpinner spin = new JSpinner(dm.getSpinnerModel()); + spin.setToolTipText(trans.get("SVGOptionPanel.lbl.strokeWidth.ttip")); + spin.setEditor(new SpinnerEditor(spin, 5)); + add(spin); + add(new UnitSelector(dm), "growx, wrap"); + } + + public Color getStrokeColor() { + return colorChooser.getSelectedColor(); + } + + public double getStrokeWidth() { + return strokeWidth; + } + + public void setStrokeWidth(double strokeWidth) { + this.strokeWidth = strokeWidth; + } +} diff --git a/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java index eb45a473f..66f288d21 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java @@ -1,8 +1,8 @@ package net.sf.openrocket.gui.configdialog; -import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -12,6 +12,7 @@ import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JDialog; +import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSpinner; @@ -19,14 +20,18 @@ import javax.swing.SwingUtilities; import net.miginfocom.swing.MigLayout; import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.file.svg.export.SVGBuilder; import net.sf.openrocket.gui.SpinnerEditor; import net.sf.openrocket.gui.adaptors.DoubleModel; import net.sf.openrocket.gui.adaptors.EnumModel; import net.sf.openrocket.gui.adaptors.MaterialModel; import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.SVGOptionPanel; import net.sf.openrocket.gui.components.StyledLabel; import net.sf.openrocket.gui.components.StyledLabel.Style; import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.gui.util.FileHelper; +import net.sf.openrocket.gui.util.SwingPreferences; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.Markers; import net.sf.openrocket.material.Material; @@ -38,6 +43,7 @@ import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.SymmetricComponent; import net.sf.openrocket.rocketcomponent.position.AxialMethod; import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.Preferences; import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; @@ -51,6 +57,7 @@ import org.slf4j.LoggerFactory; public abstract class FinSetConfig extends RocketComponentConfig { private static final Logger log = LoggerFactory.getLogger(FinSetConfig.class); private static final Translator trans = Application.getTranslator(); + private static final Preferences prefs = Application.getPreferences(); private JButton split = null; @@ -127,15 +134,47 @@ public abstract class FinSetConfig extends RocketComponentConfig { } }); split.setEnabled(((FinSet) component).getFinCount() > 1); + + //// Export to SVG + JButton exportSVGBtn = new SelectColorButton(trans.get("FinSetConfig.lbl.exportSVG")); + exportSVGBtn.setToolTipText(trans.get("FinSetConfig.lbl.exportSVG.ttip")); + exportSVGBtn.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.info(Markers.USER_MARKER, "Export CSV free-form fin"); + + JFileChooser chooser = new JFileChooser(); + chooser.setFileFilter(FileHelper.SVG_FILTER); + chooser.setAccessory(new SVGOptionPanel()); + chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory()); + + if (JFileChooser.APPROVE_OPTION == chooser.showSaveDialog(FinSetConfig.this)){ + File selectedFile= chooser.getSelectedFile(); + selectedFile = FileHelper.forceExtension(selectedFile, "svg"); + if (!FileHelper.confirmWrite(selectedFile, buttonPanel)) { + return; + } + + SVGOptionPanel svgOptions = (SVGOptionPanel) chooser.getAccessory(); + prefs.setSVGStrokeColor(svgOptions.getStrokeColor()); + prefs.setSVGStrokeWidth(svgOptions.getStrokeWidth()); + + FinSetConfig.writeSVGFile((FinSet) component, selectedFile, svgOptions); + ((SwingPreferences) Application.getPreferences()).setDefaultDirectory(chooser.getCurrentDirectory()); + } + } + }); if (convert == null) { - addButtons(split); + addButtons(split, exportSVGBtn); order.add(split); + order.add(exportSVGBtn); } else { - addButtons(split, convert); + addButtons(split, convert, exportSVGBtn); order.add(split); order.add(convert); + order.add(exportSVGBtn); } } @@ -580,4 +619,17 @@ public abstract class FinSetConfig extends RocketComponentConfig { return filletPanel; } + + private static boolean writeSVGFile(FinSet finSet, File file, SVGOptionPanel svgOptions) { + Coordinate[] points = finSet.getFinPointsWithRoot(); + try { + SVGBuilder builder = new SVGBuilder(); + builder.addPath(points, null, svgOptions.getStrokeColor(), svgOptions.getStrokeWidth()); + builder.writeToFile(file); + return true; + } catch (Exception e) { + log.error("Error writing SVG file", e); + return false; + } + } } diff --git a/swing/src/net/sf/openrocket/gui/util/FileHelper.java b/swing/src/net/sf/openrocket/gui/util/FileHelper.java index 1450def77..f291659fe 100644 --- a/swing/src/net/sf/openrocket/gui/util/FileHelper.java +++ b/swing/src/net/sf/openrocket/gui/util/FileHelper.java @@ -68,6 +68,10 @@ public final class FileHelper { public static final FileFilter PNG_FILTER = new SimpleFileFilter(trans.get("FileHelper.PNG_FILTER"), ".png"); + /** File filter for CSV files (*.csv) */ + public static final FileFilter SVG_FILTER = + new SimpleFileFilter(trans.get("FileHelper.SVG_FILTER"), ".svg"); + /** File filter for XML files (*.xml) */ public static final FileFilter XML_FILTER = new SimpleFileFilter(trans.get("FileHelper.XML_FILTER"), ".xml"); From 4cf77bcdc4f51e277351f5b97c5126bf91596d32 Mon Sep 17 00:00:00 2001 From: SiboVG Date: Tue, 30 Jan 2024 20:57:22 +0100 Subject: [PATCH 3/4] Display error when SVG export failed --- core/resources/l10n/messages.properties | 2 ++ .../gui/configdialog/FinSetConfig.java | 36 ++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 163318edd..bbdc236a4 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -968,6 +968,8 @@ FinSetConfig.but.Splitfins = Split fins FinSetConfig.but.Splitfins.ttip = Split the fin set into separate fins. FinSetConfig.lbl.exportSVG = Export to SVG FinSetConfig.lbl.exportSVG.ttip = Export the fin profile to an SVG file. +FinSetConfig.errorSVG.title = SVG Export Error +FinSetConfig.errorSVG.msg = An error occurred while exporting the SVG file: %s. FinSetConfig.but.AutoCalc = Calculate automatically FinSetConfig.but.AutoCalc.ttip = Calculates the height and length of the fin tabs, based on
inner tube and centering ring components of the fin's parent component. FinSetConfig.lbl.Through-the-wall = Through-the-wall fin tabs: diff --git a/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java index 66f288d21..16ed11e58 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java @@ -14,9 +14,12 @@ import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.SwingUtilities; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; import net.miginfocom.swing.MigLayout; import net.sf.openrocket.document.OpenRocketDocument; @@ -155,12 +158,18 @@ public abstract class FinSetConfig extends RocketComponentConfig { return; } + ((SwingPreferences) Application.getPreferences()).setDefaultDirectory(chooser.getCurrentDirectory()); SVGOptionPanel svgOptions = (SVGOptionPanel) chooser.getAccessory(); prefs.setSVGStrokeColor(svgOptions.getStrokeColor()); prefs.setSVGStrokeWidth(svgOptions.getStrokeWidth()); - FinSetConfig.writeSVGFile((FinSet) component, selectedFile, svgOptions); - ((SwingPreferences) Application.getPreferences()).setDefaultDirectory(chooser.getCurrentDirectory()); + try { + FinSetConfig.writeSVGFile((FinSet) component, selectedFile, svgOptions); + } catch (Exception svgErr) { + JOptionPane.showMessageDialog(FinSetConfig.this, + String.format(trans.get("FinSetConfig.errorSVG.msg"), svgErr.getMessage()), + trans.get("FinSetConfig.errorSVG.title"), JOptionPane.ERROR_MESSAGE); + } } } }); @@ -620,16 +629,19 @@ public abstract class FinSetConfig extends RocketComponentConfig { return filletPanel; } - private static boolean writeSVGFile(FinSet finSet, File file, SVGOptionPanel svgOptions) { + /** + * Writes the FinSet object to an SVG file. + * + * @param finSet the FinSet object to write to the SVG file + * @param file the File object representing the SVG file to be written + * @param svgOptions the SVGOptionPanel object containing the options for writing the SVG file + * @throws Exception if there is an error writing the SVG file + */ + private static void writeSVGFile(FinSet finSet, File file, SVGOptionPanel svgOptions) throws ParserConfigurationException, TransformerException { Coordinate[] points = finSet.getFinPointsWithRoot(); - try { - SVGBuilder builder = new SVGBuilder(); - builder.addPath(points, null, svgOptions.getStrokeColor(), svgOptions.getStrokeWidth()); - builder.writeToFile(file); - return true; - } catch (Exception e) { - log.error("Error writing SVG file", e); - return false; - } + + SVGBuilder builder = new SVGBuilder(); + builder.addPath(points, null, svgOptions.getStrokeColor(), svgOptions.getStrokeWidth()); + builder.writeToFile(file); } } From 9ecabd585f8ea8abc18436a66c2bc0e2ef9dc85d Mon Sep 17 00:00:00 2001 From: SiboVG Date: Wed, 31 Jan 2024 12:43:53 +0100 Subject: [PATCH 4/4] Also add fin tabs to SVG export --- .../file/svg/export/SVGBuilder.java | 4 + .../export/components/FinSetExporter.java | 2 +- .../sf/openrocket/rocketcomponent/FinSet.java | 125 ++++++++++++++++-- .../rocketcomponent/FinSetTest.java | 39 ++++++ .../gui/components/SVGOptionPanel.java | 4 + .../gui/configdialog/FinSetConfig.java | 13 +- .../gui/figure3d/geometry/FinRenderer.java | 4 +- .../openrocket/gui/print/PrintableFinSet.java | 2 +- .../gui/rocketfigure/FinSetShapes.java | 5 +- .../gui/configdialog/FinSetConfigTest.java | 41 ++++++ 10 files changed, 216 insertions(+), 23 deletions(-) diff --git a/core/src/net/sf/openrocket/file/svg/export/SVGBuilder.java b/core/src/net/sf/openrocket/file/svg/export/SVGBuilder.java index 40726f58f..8873b64ba 100644 --- a/core/src/net/sf/openrocket/file/svg/export/SVGBuilder.java +++ b/core/src/net/sf/openrocket/file/svg/export/SVGBuilder.java @@ -104,6 +104,10 @@ public class SVGBuilder { svgRoot.appendChild(path); } + public void addPath(Coordinate[] coordinates, double xPos, double yPos, Color fill, Color stroke, double strokeWidth) { + addPath(coordinates, xPos, yPos, fill, stroke, strokeWidth, LineCap.SQUARE); + } + public void addPath(Coordinate[] coordinates, Color fill, Color stroke, double strokeWidth, LineCap lineCap) { addPath(coordinates, 0, 0, fill, stroke, strokeWidth, lineCap); } diff --git a/core/src/net/sf/openrocket/file/wavefrontobj/export/components/FinSetExporter.java b/core/src/net/sf/openrocket/file/wavefrontobj/export/components/FinSetExporter.java index 4a9206b4c..7a2667294 100644 --- a/core/src/net/sf/openrocket/file/wavefrontobj/export/components/FinSetExporter.java +++ b/core/src/net/sf/openrocket/file/wavefrontobj/export/components/FinSetExporter.java @@ -27,7 +27,7 @@ public class FinSetExporter extends RocketComponentExporter { obj.setActiveGroupNames(groupName); final Coordinate[] points = component.getFinPointsWithRoot(); - final Coordinate[] tabPoints = component.getTabPoints(); + final Coordinate[] tabPoints = component.getTabPointsWithRoot(); final Coordinate[] tabPointsReversed = new Coordinate[tabPoints.length]; // We need clockwise points for the PolygonExporter for (int i = 0; i < tabPoints.length; i++) { tabPointsReversed[i] = tabPoints[tabPoints.length - i - 1]; diff --git a/core/src/net/sf/openrocket/rocketcomponent/FinSet.java b/core/src/net/sf/openrocket/rocketcomponent/FinSet.java index 82fb557db..de290cf15 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/FinSet.java +++ b/core/src/net/sf/openrocket/rocketcomponent/FinSet.java @@ -701,7 +701,7 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona // get body points, relTo fin front / centerline); final Coordinate[] upperCurve = getMountPoints( xTabFront_body, xTabTrail_body, -xFinFront_body, 0); - final Coordinate[] lowerCurve = translateToCenterline( getTabPoints()); + final Coordinate[] lowerCurve = translateToCenterline( getTabPointsWithRoot()); final Coordinate[] tabPoints = combineCurves( upperCurve, lowerCurve); return calculateCurveIntegral( tabPoints ); @@ -1083,7 +1083,7 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona final double intervalLength = xEnd - xStart; // for anything more complicated, increase the count: - if ((!MathUtil.equals(getCantAngle(), 0)) || (parent instanceof Transition) && (((Transition)parent).getShapeType() != Shape.CONICAL)) { + if ((!MathUtil.equals(getCantAngle(), 0)) || (parent instanceof Transition && ((Transition)parent).getShapeType() != Shape.CONICAL)) { // the maximum precision to enforce when calculating the areas of fins (especially on curved parent bodies) final double xWidth = 0.0025; // width (in meters) of each individual iteration divisionCount = (int) Math.ceil(intervalLength / xWidth); @@ -1209,6 +1209,33 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona return combineCurves(getFinPoints(), getRootPoints(MAX_ROOT_DIVISIONS_LOW_RES)); } + /** + * Checks if this fin set has a tab. + * + * @return true if the tab dimensions are greater than 0, otherwise false. + */ + public boolean hasTab() { + return (getTabHeight() > 0) && (getTabLength() > 0); + } + + /** + * Checks if the tab is fully beyond the fin. + * + * @return true if the tab is beyond the fin, otherwise false. + */ + public boolean isTabBeyondFin() { + if (!hasTab()) { + return false; + } + + final double xTabFront = getTabFrontEdge(); + final double xTabTrail = getTabTrailingEdge(); + + final double xFinEnd = getLength(); // Fin tab is referenced to the fin front, so the fin end is the fin front (0) + length + + return (xTabFront > xFinEnd && xTabTrail > xFinEnd) || (xTabTrail < 0 && xTabFront < 0); + } + /** * Return a list of X,Y coordinates defining the geometry of a single fin tab. * The origin is the leading root edge, and the tab height (or 'depth') is @@ -1221,9 +1248,8 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona * * @return List of XY-coordinates. */ - public Coordinate[] getTabPoints() { - if (MathUtil.equals(getTabHeight(), 0) || - MathUtil.equals(getTabLength(), 0)){ + public Coordinate[] getTabPointsWithRoot() { + if (!hasTab()) { return new Coordinate[]{}; } @@ -1240,6 +1266,62 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona return generateTabPointsWithRoot(rootPoints); } + /** + * Generates a combined shape of the fin and tab that is uniform and uninterrupted, creating a continuous contour + * around the entire structure. + * + * The function operates under the following conditions: + * - If the tab is present and does not extend beyond the fin, it combines the fin, root, + * and tab points in a way that maintains a continuous shape. + * - If the tab extends beyond the fin or if the tab is not present, only the fin points, + * including the root, are returned, maintaining the fin's original shape. + * + * @return Array of Coordinates representing the continuous shape combining fin and tab points. + * If the tab is not present or extends beyond the fin, returns only the fin points. + */ + public Coordinate[] generateContinuousFinAndTabShape() { + if (!hasTab() || isTabBeyondFin()) { + return getFinPointsWithRoot(); + } + + final Coordinate[] finPoints = getFinPoints(); + final Coordinate[] rootPoints = getRootPoints(); + final Coordinate[] tabPoints = getTabPoints(); + + final double finStart = finPoints[0].x; + + final List uniformPoints = new LinkedList<>(Arrays.asList(finPoints)); + + boolean tabAdded = false; + for (Coordinate rootPoint : rootPoints) { + // If the tab is not yet added, we need to check whether we need to include root tabs before the tab. + if (!tabAdded) { + // Check if the root point is beyond the tab. If so, add it to the list. + if (rootPoint.x > tabPoints[tabPoints.length - 1].x) { + uniformPoints.add(rootPoint); + } + // If the root point is before the tab, we need to first add the tab points. + else { + for (int j = tabPoints.length - 1; j >= 0; j--) { + uniformPoints.add(tabPoints[j]); + } + tabAdded = true; + } + } + // Once the tab is added, we need to add the remaining root points that lie before the tab. + if (tabAdded && rootPoint.x < tabPoints[0].x) { + uniformPoints.add(rootPoint); + } + } + + // Make sure we close the shape in case the tab is before the fin. + if (tabPoints[0].x < finStart) { + uniformPoints.add(finPoints[0]); + } + + return uniformPoints.toArray(new Coordinate[0]); + } + /** * Return a list of X,Y coordinates defining the geometry of a single fin tab. * The origin is the leading root edge, and the tab height (or 'depth') is @@ -1255,7 +1337,7 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona * * @return List of XY-coordinates. */ - public Coordinate[] getTabPointsLowRes() { + public Coordinate[] getTabPointsWithRootLowRes() { if (MathUtil.equals(getTabHeight(), 0) || MathUtil.equals(getTabLength(), 0)){ return new Coordinate[]{}; @@ -1274,7 +1356,26 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona return generateTabPointsWithRoot(rootPoints); } + /** + * Generates an array of tab points with the root side. + * + * @param rootPoints A list of root points + * @return An array of tab points with the root (relative to the fin front) + */ private Coordinate[] generateTabPointsWithRoot(List rootPoints) { + Coordinate[] tabPoints = getTabPoints(); + + rootPoints.add(0, new Coordinate(tabPoints[0].x, tabPoints[0].y)); + + return combineCurves(tabPoints, rootPoints.toArray(new Coordinate[0])); + } + + /** + * Generates an array of coordinates representing the points of a tab (without the root of the tab). + * + * @return an array of Coordinate objects representing the points of a tab (relative to the fin front) + */ + public Coordinate[] getTabPoints() { final double xTabFront = getTabFrontEdge(); final double xTabTrail = getTabTrailingEdge(); @@ -1283,13 +1384,12 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona final SymmetricComponent body = (SymmetricComponent)this.getParent(); - // // limit the new heights to be no greater than the current body radius. double yTabFront = Double.NaN; double yTabTrail = Double.NaN; double yTabBottom = Double.NaN; - if( null != body ){ - yTabFront = body.getRadius( finFront.x + xTabFront) - finFront.y; - yTabTrail = body.getRadius( finFront.x + xTabTrail) - finFront.y; + if (body != null) { + yTabFront = body.getRadius(finFront.x + xTabFront) - finFront.y; + yTabTrail = body.getRadius(finFront.x + xTabTrail) - finFront.y; yTabBottom = MathUtil.min(yTabFront, yTabTrail) - tabHeight; } @@ -1297,9 +1397,8 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona tabPoints[1] = new Coordinate(xTabFront, yTabBottom ); tabPoints[2] = new Coordinate(xTabTrail, yTabBottom ); tabPoints[3] = new Coordinate(xTabTrail, yTabTrail); - rootPoints.add(0, new Coordinate(xTabFront, yTabFront)); - return combineCurves(tabPoints, rootPoints.toArray(new Coordinate[0])); + return tabPoints; } @Override @@ -1568,7 +1667,7 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona if( ! this.isTabTrivial() ) { buf.append(String.format(" TabLength: %6.4f TabHeight: %6.4f @ %6.4f via: %s\n", tabLength, tabHeight, tabPosition, this.tabOffsetMethod)); - buf.append(getPointDescr(this.getTabPoints(), "Tab Points", "")); + buf.append(getPointDescr(this.getTabPointsWithRoot(), "Tab Points", "")); } return buf; } diff --git a/core/test/net/sf/openrocket/rocketcomponent/FinSetTest.java b/core/test/net/sf/openrocket/rocketcomponent/FinSetTest.java index 03d7613e1..fa23f5675 100644 --- a/core/test/net/sf/openrocket/rocketcomponent/FinSetTest.java +++ b/core/test/net/sf/openrocket/rocketcomponent/FinSetTest.java @@ -1,6 +1,7 @@ package net.sf.openrocket.rocketcomponent; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertArrayEquals; import net.sf.openrocket.material.Material; import net.sf.openrocket.util.TestRockets; @@ -266,4 +267,42 @@ public class FinSetTest extends BaseTestCase { } + @Test + public void testGenerateContinuousFinAndTabShape() { + BodyTube parent = new BodyTube(); + final FinSet fins = FinSetTest.createSimpleFin(); + fins.setCantAngle(0); + parent.addChild(fins); + Coordinate[] finShapeContinuous = fins.generateContinuousFinAndTabShape(); + final Coordinate[] finShape = fins.getFinPointsWithRoot(); + + assertEquals("incorrect fin shape length", finShape.length, finShapeContinuous.length); + + assertArrayEquals("incorrect fin shape", finShape, finShapeContinuous); + + // Set the tab + fins.setTabHeight(0.02); + + finShapeContinuous = fins.generateContinuousFinAndTabShape(); + + assertEquals("incorrect fin shape length", finShape.length + 3, finShapeContinuous.length); + + for (int i = 0; i < finShape.length-2; i++) { + assertEquals("incorrect fin shape point " + i, finShape[i], finShapeContinuous[i]); + } + + int idx = finShape.length-2; + assertEquals("incorrect fin shape point " + idx, new Coordinate(0.04, 0.0), finShapeContinuous[idx]); + idx++; + assertEquals("incorrect fin shape point " + idx, new Coordinate(0.04, -0.02), finShapeContinuous[idx]); + idx++; + assertEquals("incorrect fin shape point " + idx, new Coordinate(0.02, -0.02), finShapeContinuous[idx]); + idx++; + assertEquals("incorrect fin shape point " + idx, new Coordinate(0.02, 0.0), finShapeContinuous[idx]); + idx++; + assertEquals("incorrect fin shape point " + idx, new Coordinate(0.0, 0.0), finShapeContinuous[idx]); + + // TODO: test on transition parent... + } + } diff --git a/swing/src/net/sf/openrocket/gui/components/SVGOptionPanel.java b/swing/src/net/sf/openrocket/gui/components/SVGOptionPanel.java index 5fd64915a..6d6c010da 100644 --- a/swing/src/net/sf/openrocket/gui/components/SVGOptionPanel.java +++ b/swing/src/net/sf/openrocket/gui/components/SVGOptionPanel.java @@ -48,6 +48,10 @@ public class SVGOptionPanel extends JPanel { return colorChooser.getSelectedColor(); } + public void setStrokeColor(Color color) { + colorChooser.setSelectedColor(color); + } + public double getStrokeWidth() { return strokeWidth; } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java index 16ed11e58..eb2a481d2 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java @@ -637,11 +637,20 @@ public abstract class FinSetConfig extends RocketComponentConfig { * @param svgOptions the SVGOptionPanel object containing the options for writing the SVG file * @throws Exception if there is an error writing the SVG file */ - private static void writeSVGFile(FinSet finSet, File file, SVGOptionPanel svgOptions) throws ParserConfigurationException, TransformerException { - Coordinate[] points = finSet.getFinPointsWithRoot(); + public static void writeSVGFile(FinSet finSet, File file, SVGOptionPanel svgOptions) throws ParserConfigurationException, TransformerException { + Coordinate[] points = finSet.generateContinuousFinAndTabShape(); SVGBuilder builder = new SVGBuilder(); builder.addPath(points, null, svgOptions.getStrokeColor(), svgOptions.getStrokeWidth()); + + // Export fin tab separately if it's beyond the fin + if (finSet.isTabBeyondFin()) { + Coordinate[] tabPoints = finSet.getTabPointsWithRoot(); + Coordinate finFront = finSet.getFinFront(); + // Need to offset to the fin front because the tab points are relative to the fin front + builder.addPath(tabPoints, finFront.x, finFront.y, null, svgOptions.getStrokeColor(), svgOptions.getStrokeWidth()); + } + builder.writeToFile(file); } } diff --git a/swing/src/net/sf/openrocket/gui/figure3d/geometry/FinRenderer.java b/swing/src/net/sf/openrocket/gui/figure3d/geometry/FinRenderer.java index f6df3dbef..ec866e87e 100644 --- a/swing/src/net/sf/openrocket/gui/figure3d/geometry/FinRenderer.java +++ b/swing/src/net/sf/openrocket/gui/figure3d/geometry/FinRenderer.java @@ -16,8 +16,6 @@ import net.sf.openrocket.util.BoundingBox; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.gui.figure3d.geometry.Geometry.Surface; -import java.util.Collections; - public class FinRenderer { private GLUtessellator tess = GLU.gluNewTess(); @@ -37,7 +35,7 @@ public class FinRenderer { gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); Coordinate[] finPoints = finSet.getFinPointsWithLowResRoot(); - Coordinate[] tabPoints = finSet.getTabPointsLowRes(); + Coordinate[] tabPoints = finSet.getTabPointsWithRootLowRes(); { gl.glPushMatrix(); diff --git a/swing/src/net/sf/openrocket/gui/print/PrintableFinSet.java b/swing/src/net/sf/openrocket/gui/print/PrintableFinSet.java index 6b8f5967d..cf09a0000 100644 --- a/swing/src/net/sf/openrocket/gui/print/PrintableFinSet.java +++ b/swing/src/net/sf/openrocket/gui/print/PrintableFinSet.java @@ -51,7 +51,7 @@ public class PrintableFinSet extends AbstractPrintable { protected void init (FinSet component) { Coordinate[] points = component.getFinPointsWithRoot(); - Coordinate[] tabPoints = component.getTabPoints(); + Coordinate[] tabPoints = component.getTabPointsWithRoot(); finPolygon = new GeneralPath(GeneralPath.WIND_NON_ZERO, points.length); finTabPolygon = new GeneralPath(GeneralPath.WIND_NON_ZERO, tabPoints.length); diff --git a/swing/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java b/swing/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java index d06c7feb6..b374a8fd1 100644 --- a/swing/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java +++ b/swing/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java @@ -7,7 +7,6 @@ import java.util.Arrays; import net.sf.openrocket.rocketcomponent.FinSet; import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.SymmetricComponent; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.Transformation; @@ -35,7 +34,7 @@ public class FinSetShapes extends RocketComponentShape { final Transformation compositeTransform = transformation.applyTransformation(cantRotation); Coordinate[] finPoints = finset.getFinPoints(); - Coordinate[] tabPoints = finset.getTabPoints(); + Coordinate[] tabPoints = finset.getTabPointsWithRoot(); Coordinate[] rootPoints = finset.getRootPoints(); // Translate & rotate points into place @@ -203,7 +202,7 @@ public class FinSetShapes extends RocketComponentShape { Coordinate[] backPoints; int minIndex; - Coordinate[] points = finset.getTabPoints(); + Coordinate[] points = finset.getTabPointsWithRoot(); // this loop finds the index @ min-y, as visible from the back for (minIndex = points.length-1; minIndex > 0; minIndex--) { diff --git a/swing/test/net/sf/openrocket/gui/configdialog/FinSetConfigTest.java b/swing/test/net/sf/openrocket/gui/configdialog/FinSetConfigTest.java index 2a8394e7b..bb9d9a91c 100644 --- a/swing/test/net/sf/openrocket/gui/configdialog/FinSetConfigTest.java +++ b/swing/test/net/sf/openrocket/gui/configdialog/FinSetConfigTest.java @@ -1,9 +1,15 @@ package net.sf.openrocket.gui.configdialog; +import java.awt.Color; +import java.io.File; +import java.io.IOException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import net.sf.openrocket.gui.components.SVGOptionPanel; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -15,6 +21,9 @@ import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.position.AxialMethod; import net.sf.openrocket.util.BaseTestCase.BaseTestCase; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; + public class FinSetConfigTest extends BaseTestCase { static Method method; @@ -256,4 +265,36 @@ public class FinSetConfigTest extends BaseTestCase { Assert.assertEquals(0.0028, result.doubleValue(), 0.0001); } + @Test + public void testExportToSVG() throws IOException, ParserConfigurationException, TransformerException { + BodyTube bodyTube = new BodyTube(); + bodyTube.setOuterRadius(0.06); + bodyTube.setLength(0.2); + + TrapezoidFinSet finSet = new TrapezoidFinSet(); + finSet.setCantAngle(0); + finSet.setRootChord(0.06); + finSet.setRootChord(0.05); + finSet.setHeight(0.03); + finSet.setSweep(0.02); + finSet.setSweepAngle(Math.toRadians(20)); + finSet.setThickness(0.002); + finSet.setTabLength(0.02); + finSet.setTabHeight(0.012); + finSet.setTabOffsetMethod(AxialMethod.MIDDLE); + finSet.setTabOffset(0); + + bodyTube.addChild(finSet); + + SVGOptionPanel options = new SVGOptionPanel(); + options.setStrokeWidth(0.1); + options.setStrokeColor(Color.BLACK); + + File destFile = File.createTempFile("test", ".svg"); + + FinSetConfig.writeSVGFile(finSet, destFile, options); + + // TODO: load the file and check the contents + } + }