Merge pull request #2452 from SiboVG/fin-svg-export
Allow fins to be exported to an SVG file
This commit is contained in:
commit
6ada3ff347
@ -127,6 +127,7 @@ FileHelper.RASAERO_DESIGN_FILTER = RASAero designs (*.CDX1)
|
|||||||
FileHelper.WAVEFRONT_OBJ_FILTER = Wavefront OBJ 3D file (*.obj)
|
FileHelper.WAVEFRONT_OBJ_FILTER = Wavefront OBJ 3D file (*.obj)
|
||||||
FileHelper.OPEN_ROCKET_COMPONENT_FILTER = OpenRocket presets (*.orc)
|
FileHelper.OPEN_ROCKET_COMPONENT_FILTER = OpenRocket presets (*.orc)
|
||||||
FileHelper.PNG_FILTER = PNG image (*.png)
|
FileHelper.PNG_FILTER = PNG image (*.png)
|
||||||
|
FileHelper.SVG_FILTER = SVG files (*.svg)
|
||||||
FileHelper.IMAGES = Image files
|
FileHelper.IMAGES = Image files
|
||||||
FileHelper.XML_FILTER = XML files (*.xml)
|
FileHelper.XML_FILTER = XML files (*.xml)
|
||||||
|
|
||||||
@ -709,6 +710,12 @@ SimExpPan.Col.Unit = Unit
|
|||||||
CsvOptionPanel.separator.space = SPACE
|
CsvOptionPanel.separator.space = SPACE
|
||||||
CsvOptionPanel.separator.tab = TAB
|
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
|
! Custom expression general stuff
|
||||||
customExpression.Name = Name
|
customExpression.Name = Name
|
||||||
@ -960,6 +967,10 @@ FinSetConfig.but.Converttofreeform.ttip = Convert this fin set into a freeform f
|
|||||||
FinSetConfig.Convertfinset = Convert fin set
|
FinSetConfig.Convertfinset = Convert fin set
|
||||||
FinSetConfig.but.Splitfins = Split fins
|
FinSetConfig.but.Splitfins = Split fins
|
||||||
FinSetConfig.but.Splitfins.ttip = Split the fin set into separate 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 = Calculate automatically
|
||||||
FinSetConfig.but.AutoCalc.ttip = <html>Calculates the height and length of the fin tabs, based on<br>inner tube and centering ring components of the fin's parent component.</html>
|
FinSetConfig.but.AutoCalc.ttip = <html>Calculates the height and length of the fin tabs, based on<br>inner tube and centering ring components of the fin's parent component.</html>
|
||||||
FinSetConfig.lbl.Through-the-wall = Through-the-wall fin tabs:
|
FinSetConfig.lbl.Through-the-wall = Through-the-wall fin tabs:
|
||||||
|
181
core/src/net/sf/openrocket/file/svg/export/SVGBuilder.java
Normal file
181
core/src/net/sf/openrocket/file/svg/export/SVGBuilder.java
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
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 <sibo.vangool@hotmail.com>
|
||||||
|
*/
|
||||||
|
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, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,7 @@ public class FinSetExporter extends RocketComponentExporter<FinSet> {
|
|||||||
obj.setActiveGroupNames(groupName);
|
obj.setActiveGroupNames(groupName);
|
||||||
|
|
||||||
final Coordinate[] points = component.getFinPointsWithRoot();
|
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
|
final Coordinate[] tabPointsReversed = new Coordinate[tabPoints.length]; // We need clockwise points for the PolygonExporter
|
||||||
for (int i = 0; i < tabPoints.length; i++) {
|
for (int i = 0; i < tabPoints.length; i++) {
|
||||||
tabPointsReversed[i] = tabPoints[tabPoints.length - i - 1];
|
tabPointsReversed[i] = tabPoints[tabPoints.length - i - 1];
|
||||||
|
@ -702,7 +702,7 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona
|
|||||||
|
|
||||||
// get body points, relTo fin front / centerline);
|
// get body points, relTo fin front / centerline);
|
||||||
final Coordinate[] upperCurve = getMountPoints( xTabFront_body, xTabTrail_body, -xFinFront_body, 0);
|
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);
|
final Coordinate[] tabPoints = combineCurves( upperCurve, lowerCurve);
|
||||||
|
|
||||||
return calculateCurveIntegral( tabPoints );
|
return calculateCurveIntegral( tabPoints );
|
||||||
@ -1084,7 +1084,7 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona
|
|||||||
final double intervalLength = xEnd - xStart;
|
final double intervalLength = xEnd - xStart;
|
||||||
|
|
||||||
// for anything more complicated, increase the count:
|
// 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)
|
// 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
|
final double xWidth = 0.0025; // width (in meters) of each individual iteration
|
||||||
divisionCount = (int) Math.ceil(intervalLength / xWidth);
|
divisionCount = (int) Math.ceil(intervalLength / xWidth);
|
||||||
@ -1210,6 +1210,33 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona
|
|||||||
return combineCurves(getFinPoints(), getRootPoints(MAX_ROOT_DIVISIONS_LOW_RES));
|
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.
|
* 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
|
* The origin is the leading root edge, and the tab height (or 'depth') is
|
||||||
@ -1222,9 +1249,8 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona
|
|||||||
*
|
*
|
||||||
* @return List of XY-coordinates.
|
* @return List of XY-coordinates.
|
||||||
*/
|
*/
|
||||||
public Coordinate[] getTabPoints() {
|
public Coordinate[] getTabPointsWithRoot() {
|
||||||
if (MathUtil.equals(getTabHeight(), 0) ||
|
if (!hasTab()) {
|
||||||
MathUtil.equals(getTabLength(), 0)){
|
|
||||||
return new Coordinate[]{};
|
return new Coordinate[]{};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1241,6 +1267,62 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona
|
|||||||
return generateTabPointsWithRoot(rootPoints);
|
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<Coordinate> 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.
|
* 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
|
* The origin is the leading root edge, and the tab height (or 'depth') is
|
||||||
@ -1256,7 +1338,7 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona
|
|||||||
*
|
*
|
||||||
* @return List of XY-coordinates.
|
* @return List of XY-coordinates.
|
||||||
*/
|
*/
|
||||||
public Coordinate[] getTabPointsLowRes() {
|
public Coordinate[] getTabPointsWithRootLowRes() {
|
||||||
if (MathUtil.equals(getTabHeight(), 0) ||
|
if (MathUtil.equals(getTabHeight(), 0) ||
|
||||||
MathUtil.equals(getTabLength(), 0)){
|
MathUtil.equals(getTabLength(), 0)){
|
||||||
return new Coordinate[]{};
|
return new Coordinate[]{};
|
||||||
@ -1275,7 +1357,26 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona
|
|||||||
return generateTabPointsWithRoot(rootPoints);
|
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<Coordinate> rootPoints) {
|
private Coordinate[] generateTabPointsWithRoot(List<Coordinate> 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 xTabFront = getTabFrontEdge();
|
||||||
final double xTabTrail = getTabTrailingEdge();
|
final double xTabTrail = getTabTrailingEdge();
|
||||||
|
|
||||||
@ -1284,13 +1385,12 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona
|
|||||||
|
|
||||||
final SymmetricComponent body = (SymmetricComponent)this.getParent();
|
final SymmetricComponent body = (SymmetricComponent)this.getParent();
|
||||||
|
|
||||||
// // limit the new heights to be no greater than the current body radius.
|
|
||||||
double yTabFront = Double.NaN;
|
double yTabFront = Double.NaN;
|
||||||
double yTabTrail = Double.NaN;
|
double yTabTrail = Double.NaN;
|
||||||
double yTabBottom = Double.NaN;
|
double yTabBottom = Double.NaN;
|
||||||
if( null != body ){
|
if (body != null) {
|
||||||
yTabFront = body.getRadius( finFront.x + xTabFront) - finFront.y;
|
yTabFront = body.getRadius(finFront.x + xTabFront) - finFront.y;
|
||||||
yTabTrail = body.getRadius( finFront.x + xTabTrail) - finFront.y;
|
yTabTrail = body.getRadius(finFront.x + xTabTrail) - finFront.y;
|
||||||
yTabBottom = MathUtil.min(yTabFront, yTabTrail) - tabHeight;
|
yTabBottom = MathUtil.min(yTabFront, yTabTrail) - tabHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1298,9 +1398,8 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona
|
|||||||
tabPoints[1] = new Coordinate(xTabFront, yTabBottom );
|
tabPoints[1] = new Coordinate(xTabFront, yTabBottom );
|
||||||
tabPoints[2] = new Coordinate(xTabTrail, yTabBottom );
|
tabPoints[2] = new Coordinate(xTabTrail, yTabBottom );
|
||||||
tabPoints[3] = new Coordinate(xTabTrail, yTabTrail);
|
tabPoints[3] = new Coordinate(xTabTrail, yTabTrail);
|
||||||
rootPoints.add(0, new Coordinate(xTabFront, yTabFront));
|
|
||||||
|
|
||||||
return combineCurves(tabPoints, rootPoints.toArray(new Coordinate[0]));
|
return tabPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -1526,7 +1625,7 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona
|
|||||||
|
|
||||||
if( ! this.isTabTrivial() ) {
|
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(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;
|
return buf;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.sf.openrocket.startup;
|
package net.sf.openrocket.startup;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.EventListener;
|
import java.util.EventListener;
|
||||||
import java.util.EventObject;
|
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_X_OFFS = "OrigXOffs";
|
||||||
private static final String OBJ_ORIG_Y_OFFS = "OrigYOffs";
|
private static final String OBJ_ORIG_Y_OFFS = "OrigYOffs";
|
||||||
private static final String OBJ_ORIG_Z_OFFS = "OrigZOffs";
|
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();
|
private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel();
|
||||||
|
|
||||||
@ -1098,6 +1103,42 @@ public abstract class Preferences implements ChangeSource {
|
|||||||
|
|
||||||
return options;
|
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.
|
* Within a holder class so they will load only when needed.
|
||||||
|
@ -89,6 +89,8 @@ public class UnitGroup {
|
|||||||
|
|
||||||
public static final UnitGroup UNITS_SCALING;
|
public static final UnitGroup UNITS_SCALING;
|
||||||
|
|
||||||
|
public static final UnitGroup UNITS_STROKE_WIDTH;
|
||||||
|
|
||||||
|
|
||||||
public static final Map<String, UnitGroup> UNITS; // keys such as "LENGTH", "VELOCITY"
|
public static final Map<String, UnitGroup> UNITS; // keys such as "LENGTH", "VELOCITY"
|
||||||
public static final Map<String, UnitGroup> SIUNITS; // keys such a "m", "m/s"
|
public static final Map<String, UnitGroup> SIUNITS; // keys such a "m", "m/s"
|
||||||
@ -316,6 +318,12 @@ public class UnitGroup {
|
|||||||
UNITS_SCALING = new UnitGroup();
|
UNITS_SCALING = new UnitGroup();
|
||||||
UNITS_SCALING.addUnit(new FixedPrecisionUnit("" + ZWSP, 0.1)); // zero-width space
|
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:
|
// This is not used by OpenRocket, and not extensively tested:
|
||||||
UNITS_FREQUENCY = new UnitGroup();
|
UNITS_FREQUENCY = new UnitGroup();
|
||||||
@ -354,6 +362,7 @@ public class UnitGroup {
|
|||||||
map.put("ROUGHNESS", UNITS_ROUGHNESS);
|
map.put("ROUGHNESS", UNITS_ROUGHNESS);
|
||||||
map.put("COEFFICIENT", UNITS_COEFFICIENT);
|
map.put("COEFFICIENT", UNITS_COEFFICIENT);
|
||||||
map.put("SCALING", UNITS_SCALING);
|
map.put("SCALING", UNITS_SCALING);
|
||||||
|
map.put("STROKE_WIDTH", UNITS_STROKE_WIDTH);
|
||||||
map.put("VOLTAGE", UNITS_VOLTAGE);
|
map.put("VOLTAGE", UNITS_VOLTAGE);
|
||||||
map.put("CURRENT", UNITS_CURRENT);
|
map.put("CURRENT", UNITS_CURRENT);
|
||||||
map.put("ENERGY", UNITS_ENERGY);
|
map.put("ENERGY", UNITS_ENERGY);
|
||||||
@ -416,6 +425,7 @@ public class UnitGroup {
|
|||||||
UNITS_PRESSURE.setDefaultUnit("mbar");
|
UNITS_PRESSURE.setDefaultUnit("mbar");
|
||||||
UNITS_RELATIVE.setDefaultUnit("%");
|
UNITS_RELATIVE.setDefaultUnit("%");
|
||||||
UNITS_ROUGHNESS.setDefaultUnit(MICRO + "m");
|
UNITS_ROUGHNESS.setDefaultUnit(MICRO + "m");
|
||||||
|
UNITS_STROKE_WIDTH.setDefaultUnit("mm");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setDefaultImperialUnits() {
|
public static void setDefaultImperialUnits() {
|
||||||
@ -445,6 +455,7 @@ public class UnitGroup {
|
|||||||
UNITS_PRESSURE.setDefaultUnit("mbar");
|
UNITS_PRESSURE.setDefaultUnit("mbar");
|
||||||
UNITS_RELATIVE.setDefaultUnit("%");
|
UNITS_RELATIVE.setDefaultUnit("%");
|
||||||
UNITS_ROUGHNESS.setDefaultUnit("mil");
|
UNITS_ROUGHNESS.setDefaultUnit("mil");
|
||||||
|
UNITS_STROKE_WIDTH.setDefaultUnit("mil");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void resetDefaultUnits() {
|
public static void resetDefaultUnits() {
|
||||||
@ -485,6 +496,7 @@ public class UnitGroup {
|
|||||||
UNITS_ROUGHNESS.setDefaultUnit(0);
|
UNITS_ROUGHNESS.setDefaultUnit(0);
|
||||||
UNITS_COEFFICIENT.setDefaultUnit(0);
|
UNITS_COEFFICIENT.setDefaultUnit(0);
|
||||||
UNITS_SCALING.setDefaultUnit(0);
|
UNITS_SCALING.setDefaultUnit(0);
|
||||||
|
UNITS_STROKE_WIDTH.setDefaultUnit(0);
|
||||||
UNITS_FREQUENCY.setDefaultUnit(1);
|
UNITS_FREQUENCY.setDefaultUnit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.sf.openrocket.rocketcomponent;
|
package net.sf.openrocket.rocketcomponent;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
|
||||||
import net.sf.openrocket.material.Material;
|
import net.sf.openrocket.material.Material;
|
||||||
import net.sf.openrocket.util.TestRockets;
|
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...
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
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 void setStrokeColor(Color color) {
|
||||||
|
colorChooser.setSelectedColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getStrokeWidth() {
|
||||||
|
return strokeWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStrokeWidth(double strokeWidth) {
|
||||||
|
this.strokeWidth = strokeWidth;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
package net.sf.openrocket.gui.configdialog;
|
package net.sf.openrocket.gui.configdialog;
|
||||||
|
|
||||||
import java.awt.Component;
|
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.ActionListener;
|
import java.awt.event.ActionListener;
|
||||||
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@ -12,21 +12,29 @@ import javax.swing.BorderFactory;
|
|||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JComboBox;
|
import javax.swing.JComboBox;
|
||||||
import javax.swing.JDialog;
|
import javax.swing.JDialog;
|
||||||
|
import javax.swing.JFileChooser;
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.JSpinner;
|
import javax.swing.JSpinner;
|
||||||
import javax.swing.SwingUtilities;
|
import javax.swing.SwingUtilities;
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import javax.xml.transform.TransformerException;
|
||||||
|
|
||||||
import net.miginfocom.swing.MigLayout;
|
import net.miginfocom.swing.MigLayout;
|
||||||
import net.sf.openrocket.document.OpenRocketDocument;
|
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.SpinnerEditor;
|
||||||
import net.sf.openrocket.gui.adaptors.DoubleModel;
|
import net.sf.openrocket.gui.adaptors.DoubleModel;
|
||||||
import net.sf.openrocket.gui.adaptors.EnumModel;
|
import net.sf.openrocket.gui.adaptors.EnumModel;
|
||||||
import net.sf.openrocket.gui.adaptors.MaterialModel;
|
import net.sf.openrocket.gui.adaptors.MaterialModel;
|
||||||
import net.sf.openrocket.gui.components.BasicSlider;
|
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;
|
||||||
import net.sf.openrocket.gui.components.StyledLabel.Style;
|
import net.sf.openrocket.gui.components.StyledLabel.Style;
|
||||||
import net.sf.openrocket.gui.components.UnitSelector;
|
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.l10n.Translator;
|
||||||
import net.sf.openrocket.logging.Markers;
|
import net.sf.openrocket.logging.Markers;
|
||||||
import net.sf.openrocket.material.Material;
|
import net.sf.openrocket.material.Material;
|
||||||
@ -38,6 +46,7 @@ import net.sf.openrocket.rocketcomponent.RocketComponent;
|
|||||||
import net.sf.openrocket.rocketcomponent.SymmetricComponent;
|
import net.sf.openrocket.rocketcomponent.SymmetricComponent;
|
||||||
import net.sf.openrocket.rocketcomponent.position.AxialMethod;
|
import net.sf.openrocket.rocketcomponent.position.AxialMethod;
|
||||||
import net.sf.openrocket.startup.Application;
|
import net.sf.openrocket.startup.Application;
|
||||||
|
import net.sf.openrocket.startup.Preferences;
|
||||||
import net.sf.openrocket.unit.UnitGroup;
|
import net.sf.openrocket.unit.UnitGroup;
|
||||||
import net.sf.openrocket.util.Coordinate;
|
import net.sf.openrocket.util.Coordinate;
|
||||||
import net.sf.openrocket.util.MathUtil;
|
import net.sf.openrocket.util.MathUtil;
|
||||||
@ -51,6 +60,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
public abstract class FinSetConfig extends RocketComponentConfig {
|
public abstract class FinSetConfig extends RocketComponentConfig {
|
||||||
private static final Logger log = LoggerFactory.getLogger(FinSetConfig.class);
|
private static final Logger log = LoggerFactory.getLogger(FinSetConfig.class);
|
||||||
private static final Translator trans = Application.getTranslator();
|
private static final Translator trans = Application.getTranslator();
|
||||||
|
private static final Preferences prefs = Application.getPreferences();
|
||||||
|
|
||||||
private JButton split = null;
|
private JButton split = null;
|
||||||
|
|
||||||
@ -127,15 +137,53 @@ public abstract class FinSetConfig extends RocketComponentConfig {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
split.setEnabled(((FinSet) component).getFinCount() > 1);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
((SwingPreferences) Application.getPreferences()).setDefaultDirectory(chooser.getCurrentDirectory());
|
||||||
|
SVGOptionPanel svgOptions = (SVGOptionPanel) chooser.getAccessory();
|
||||||
|
prefs.setSVGStrokeColor(svgOptions.getStrokeColor());
|
||||||
|
prefs.setSVGStrokeWidth(svgOptions.getStrokeWidth());
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (convert == null) {
|
if (convert == null) {
|
||||||
addButtons(split);
|
addButtons(split, exportSVGBtn);
|
||||||
order.add(split);
|
order.add(split);
|
||||||
|
order.add(exportSVGBtn);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
addButtons(split, convert);
|
addButtons(split, convert, exportSVGBtn);
|
||||||
order.add(split);
|
order.add(split);
|
||||||
order.add(convert);
|
order.add(convert);
|
||||||
|
order.add(exportSVGBtn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -589,4 +637,29 @@ public abstract class FinSetConfig extends RocketComponentConfig {
|
|||||||
|
|
||||||
return filletPanel;
|
return filletPanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,6 @@ import net.sf.openrocket.util.BoundingBox;
|
|||||||
import net.sf.openrocket.util.Coordinate;
|
import net.sf.openrocket.util.Coordinate;
|
||||||
import net.sf.openrocket.gui.figure3d.geometry.Geometry.Surface;
|
import net.sf.openrocket.gui.figure3d.geometry.Geometry.Surface;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
public class FinRenderer {
|
public class FinRenderer {
|
||||||
private GLUtessellator tess = GLU.gluNewTess();
|
private GLUtessellator tess = GLU.gluNewTess();
|
||||||
|
|
||||||
@ -37,7 +35,7 @@ public class FinRenderer {
|
|||||||
gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
|
gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW);
|
||||||
|
|
||||||
Coordinate[] finPoints = finSet.getFinPointsWithLowResRoot();
|
Coordinate[] finPoints = finSet.getFinPointsWithLowResRoot();
|
||||||
Coordinate[] tabPoints = finSet.getTabPointsLowRes();
|
Coordinate[] tabPoints = finSet.getTabPointsWithRootLowRes();
|
||||||
|
|
||||||
{
|
{
|
||||||
gl.glPushMatrix();
|
gl.glPushMatrix();
|
||||||
|
@ -5,7 +5,6 @@ import java.awt.Color;
|
|||||||
import java.awt.Component;
|
import java.awt.Component;
|
||||||
import java.awt.Container;
|
import java.awt.Container;
|
||||||
import java.awt.Dimension;
|
import java.awt.Dimension;
|
||||||
import java.awt.Font;
|
|
||||||
import java.awt.Toolkit;
|
import java.awt.Toolkit;
|
||||||
import java.awt.datatransfer.Clipboard;
|
import java.awt.datatransfer.Clipboard;
|
||||||
import java.awt.datatransfer.DataFlavor;
|
import java.awt.datatransfer.DataFlavor;
|
||||||
@ -501,10 +500,11 @@ public class SimulationPanel extends JPanel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String separator = ((CsvOptionPanel) chooser.getAccessory()).getFieldSeparator();
|
CsvOptionPanel csvOptions = (CsvOptionPanel) chooser.getAccessory();
|
||||||
int precision = ((CsvOptionPanel) chooser.getAccessory()).getDecimalPlaces();
|
String separator = csvOptions.getFieldSeparator();
|
||||||
boolean isExponentialNotation = ((CsvOptionPanel) chooser.getAccessory()).isExponentialNotation();
|
int precision = csvOptions.getDecimalPlaces();
|
||||||
((CsvOptionPanel) chooser.getAccessory()).storePreferences();
|
boolean isExponentialNotation = csvOptions.isExponentialNotation();
|
||||||
|
csvOptions.storePreferences();
|
||||||
|
|
||||||
// Handle some special separator options from CsvOptionPanel
|
// Handle some special separator options from CsvOptionPanel
|
||||||
if (separator.equals(trans.get("CsvOptionPanel.separator.space"))) {
|
if (separator.equals(trans.get("CsvOptionPanel.separator.space"))) {
|
||||||
|
@ -51,7 +51,7 @@ public class PrintableFinSet extends AbstractPrintable<FinSet> {
|
|||||||
protected void init (FinSet component) {
|
protected void init (FinSet component) {
|
||||||
|
|
||||||
Coordinate[] points = component.getFinPointsWithRoot();
|
Coordinate[] points = component.getFinPointsWithRoot();
|
||||||
Coordinate[] tabPoints = component.getTabPoints();
|
Coordinate[] tabPoints = component.getTabPointsWithRoot();
|
||||||
|
|
||||||
finPolygon = new GeneralPath(GeneralPath.WIND_NON_ZERO, points.length);
|
finPolygon = new GeneralPath(GeneralPath.WIND_NON_ZERO, points.length);
|
||||||
finTabPolygon = new GeneralPath(GeneralPath.WIND_NON_ZERO, tabPoints.length);
|
finTabPolygon = new GeneralPath(GeneralPath.WIND_NON_ZERO, tabPoints.length);
|
||||||
|
@ -7,7 +7,6 @@ import java.util.Arrays;
|
|||||||
|
|
||||||
import net.sf.openrocket.rocketcomponent.FinSet;
|
import net.sf.openrocket.rocketcomponent.FinSet;
|
||||||
import net.sf.openrocket.rocketcomponent.RocketComponent;
|
import net.sf.openrocket.rocketcomponent.RocketComponent;
|
||||||
import net.sf.openrocket.rocketcomponent.SymmetricComponent;
|
|
||||||
import net.sf.openrocket.util.Coordinate;
|
import net.sf.openrocket.util.Coordinate;
|
||||||
import net.sf.openrocket.util.MathUtil;
|
import net.sf.openrocket.util.MathUtil;
|
||||||
import net.sf.openrocket.util.Transformation;
|
import net.sf.openrocket.util.Transformation;
|
||||||
@ -35,7 +34,7 @@ public class FinSetShapes extends RocketComponentShape {
|
|||||||
final Transformation compositeTransform = transformation.applyTransformation(cantRotation);
|
final Transformation compositeTransform = transformation.applyTransformation(cantRotation);
|
||||||
|
|
||||||
Coordinate[] finPoints = finset.getFinPoints();
|
Coordinate[] finPoints = finset.getFinPoints();
|
||||||
Coordinate[] tabPoints = finset.getTabPoints();
|
Coordinate[] tabPoints = finset.getTabPointsWithRoot();
|
||||||
Coordinate[] rootPoints = finset.getRootPoints();
|
Coordinate[] rootPoints = finset.getRootPoints();
|
||||||
|
|
||||||
// Translate & rotate points into place
|
// Translate & rotate points into place
|
||||||
@ -203,7 +202,7 @@ public class FinSetShapes extends RocketComponentShape {
|
|||||||
Coordinate[] backPoints;
|
Coordinate[] backPoints;
|
||||||
int minIndex;
|
int minIndex;
|
||||||
|
|
||||||
Coordinate[] points = finset.getTabPoints();
|
Coordinate[] points = finset.getTabPointsWithRoot();
|
||||||
|
|
||||||
// this loop finds the index @ min-y, as visible from the back
|
// this loop finds the index @ min-y, as visible from the back
|
||||||
for (minIndex = points.length-1; minIndex > 0; minIndex--) {
|
for (minIndex = points.length-1; minIndex > 0; minIndex--) {
|
||||||
|
@ -68,6 +68,10 @@ public final class FileHelper {
|
|||||||
public static final FileFilter PNG_FILTER =
|
public static final FileFilter PNG_FILTER =
|
||||||
new SimpleFileFilter(trans.get("FileHelper.PNG_FILTER"), ".png");
|
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) */
|
/** File filter for XML files (*.xml) */
|
||||||
public static final FileFilter XML_FILTER =
|
public static final FileFilter XML_FILTER =
|
||||||
new SimpleFileFilter(trans.get("FileHelper.XML_FILTER"), ".xml");
|
new SimpleFileFilter(trans.get("FileHelper.XML_FILTER"), ".xml");
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
package net.sf.openrocket.gui.configdialog;
|
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.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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.Assert;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
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.rocketcomponent.position.AxialMethod;
|
||||||
import net.sf.openrocket.util.BaseTestCase.BaseTestCase;
|
import net.sf.openrocket.util.BaseTestCase.BaseTestCase;
|
||||||
|
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import javax.xml.transform.TransformerException;
|
||||||
|
|
||||||
public class FinSetConfigTest extends BaseTestCase {
|
public class FinSetConfigTest extends BaseTestCase {
|
||||||
|
|
||||||
static Method method;
|
static Method method;
|
||||||
@ -256,4 +265,36 @@ public class FinSetConfigTest extends BaseTestCase {
|
|||||||
Assert.assertEquals(0.0028, result.doubleValue(), 0.0001);
|
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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user