package net.sf.openrocket.rocketcomponent; import java.util.ArrayList; import java.util.Collection; import java.util.List; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.material.Material; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.ArrayUtils; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.Transformation; public abstract class FinSet extends ExternalComponent { private static final Translator trans = Application.getTranslator(); /** * Maximum allowed cant of fins. */ public static final double MAX_CANT = (15.0 * Math.PI / 180); public enum CrossSection { //// Square SQUARE(trans.get("FinSet.CrossSection.SQUARE"), 1.00), //// Rounded ROUNDED(trans.get("FinSet.CrossSection.ROUNDED"), 0.99), //// Airfoil AIRFOIL(trans.get("FinSet.CrossSection.AIRFOIL"), 0.85); private final String name; private final double volume; CrossSection(String name, double volume) { this.name = name; this.volume = volume; } public double getRelativeVolume() { return volume; } @Override public String toString() { return name; } } public enum TabRelativePosition { //// Root chord leading edge FRONT(trans.get("FinSet.TabRelativePosition.FRONT")), //// Root chord midpoint CENTER(trans.get("FinSet.TabRelativePosition.CENTER")), //// Root chord trailing edge END(trans.get("FinSet.TabRelativePosition.END")); private final String name; TabRelativePosition(String name) { this.name = name; } @Override public String toString() { return name; } } /** * Number of fins. */ protected int fins = 3; /** * Rotation about the x-axis by 2*PI/fins. */ protected Transformation finRotation = Transformation.rotate_x(2 * Math.PI / fins); /** * Rotation angle of the first fin. Zero corresponds to the positive y-axis. */ protected double rotation = 0; /** * Rotation about the x-axis by angle this.rotation. */ protected Transformation baseRotation = Transformation.rotate_x(rotation); /** * Cant angle of fins. */ protected double cantAngle = 0; /* Cached value: */ private Transformation cantRotation = null; /** * Thickness of the fins. */ protected double thickness = 0.003; /** * The cross-section shape of the fins. */ protected CrossSection crossSection = CrossSection.SQUARE; /* * Fin tab properties. */ private double tabHeight = 0; private double tabLength = 0.05; private double tabShift = 0; private TabRelativePosition tabRelativePosition = TabRelativePosition.CENTER; /* * Fin fillet properties */ protected Material filletMaterial = null; protected double filletRadius = 0; protected double filletCenterY = 0; // Cached fin area & CG. Validity of both must be checked using finArea! // Fin area does not include fin tabs, CG does. private double finArea = -1; private double finCGx = -1; private double finCGy = -1; /** * New FinSet with given number of fins and given base rotation angle. * Sets the component relative position to POSITION_RELATIVE_BOTTOM, * i.e. fins are positioned at the bottom of the parent component. */ public FinSet() { super(RocketComponent.Position.BOTTOM); this.filletMaterial = Application.getPreferences().getDefaultComponentMaterial(this.getClass(), Material.Type.BULK); } /** * Return the number of fins in the set. * @return The number of fins. */ public int getFinCount() { return fins; } /** * Sets the number of fins in the set. * @param n The number of fins, greater of equal to one. */ public void setFinCount(int n) { if (fins == n) return; if (n < 1) n = 1; if (n > 8) n = 8; fins = n; finRotation = Transformation.rotate_x(2 * Math.PI / fins); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } public Transformation getFinRotationTransformation() { return finRotation; } /** * Gets the base rotation amount of the first fin. * @return The base rotation amount. */ public double getBaseRotation() { return rotation; } /** * Sets the base rotation amount of the first fin. * @param r The base rotation amount. */ public void setBaseRotation(double r) { r = MathUtil.reduce180(r); if (MathUtil.equals(r, rotation)) return; rotation = r; baseRotation = Transformation.rotate_x(rotation); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } public Transformation getBaseRotationTransformation() { return baseRotation; } public double getCantAngle() { return cantAngle; } public void setCantAngle(double cant) { cant = MathUtil.clamp(cant, -MAX_CANT, MAX_CANT); if (MathUtil.equals(cant, cantAngle)) return; this.cantAngle = cant; fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } public Transformation getCantRotation() { if (cantRotation == null) { if (MathUtil.equals(cantAngle, 0)) { cantRotation = Transformation.IDENTITY; } else { Transformation t = new Transformation(-length / 2, 0, 0); t = Transformation.rotate_y(cantAngle).applyTransformation(t); t = new Transformation(length / 2, 0, 0).applyTransformation(t); cantRotation = t; } } return cantRotation; } public double getThickness() { return thickness; } public void setThickness(double r) { if (thickness == r) return; thickness = Math.max(r, 0); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } public CrossSection getCrossSection() { return crossSection; } public void setCrossSection(CrossSection cs) { if (crossSection == cs) return; crossSection = cs; fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } @Override public void setRelativePosition(RocketComponent.Position position) { super.setRelativePosition(position); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } @Override public void setPositionValue(double value) { super.setPositionValue(value); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } public double getTabHeight() { return tabHeight; } public void setTabHeight(double height) { height = MathUtil.max(height, 0); if (MathUtil.equals(this.tabHeight, height)) return; this.tabHeight = height; fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } public double getTabLength() { return tabLength; } public void setTabLength(double length) { length = MathUtil.max(length, 0); if (MathUtil.equals(this.tabLength, length)) return; this.tabLength = length; fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } public double getTabShift() { return tabShift; } public void setTabShift(double shift) { this.tabShift = shift; fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } public TabRelativePosition getTabRelativePosition() { return tabRelativePosition; } public void setTabRelativePosition(TabRelativePosition position) { if (this.tabRelativePosition == position) return; double front = getTabFrontEdge(); switch (position) { case FRONT: this.tabShift = front; break; case CENTER: this.tabShift = front + tabLength / 2 - getLength() / 2; break; case END: this.tabShift = front + tabLength - getLength(); break; default: throw new IllegalArgumentException("position=" + position); } this.tabRelativePosition = position; fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } /** * Return the tab front edge position from the front of the fin. */ public double getTabFrontEdge() { switch (this.tabRelativePosition) { case FRONT: return tabShift; case CENTER: return getLength() / 2 - tabLength / 2 + tabShift; case END: return getLength() - tabLength + tabShift; default: throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition); } } /** * Return the tab trailing edge position *from the front of the fin*. */ public double getTabTrailingEdge() { switch (this.tabRelativePosition) { case FRONT: return tabLength + tabShift; case CENTER: return getLength() / 2 + tabLength / 2 + tabShift; case END: return getLength() + tabShift; default: throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition); } } /////////// Calculation methods /////////// /** * Return the area of one side of one fin. This does NOT include the area of * the fin tab. * * @return the area of one side of one fin. */ public double getFinArea() { if (finArea < 0) calculateAreaCG(); return finArea; } /** * Return the unweighted CG of a single fin. The X-coordinate is relative to * the root chord trailing edge and the Y-coordinate to the fin root chord. * * @return the unweighted CG coordinate of a single fin. */ public Coordinate getFinCG() { if (finArea < 0) calculateAreaCG(); return new Coordinate(finCGx, finCGy, 0); } @Override public double getComponentMass() { return getFilletMass() + getFinMass(); } public double getFinMass() { return getComponentVolume() * material.getDensity(); } public double getFilletMass() { return getFilletVolume() * filletMaterial.getDensity(); } @Override public double getComponentVolume() { // this is for the fins alone, fillets are taken care of separately. return fins * (getFinArea() + tabHeight * tabLength) * thickness * crossSection.getRelativeVolume(); } @Override public Coordinate getComponentCG() { if (finArea < 0) calculateAreaCG(); double mass = getFinMass(); double filletMass = getFilletMass(); double filletCenter = length / 2; double newCGx = (filletCenter * filletMass + finCGx * mass) / (filletMass + mass); // FilletRadius/5 is a good estimate for where the vertical centroid of the fillet // is. Finding the actual position is very involved and won't make a huge difference. double newCGy = (filletRadius / 5 * filletMass + finCGy * mass) / (filletMass + mass); if (fins == 1) { return baseRotation.transform( new Coordinate(finCGx, finCGy + getBodyRadius(), 0, (filletMass + mass))); } else { return new Coordinate(finCGx, 0, 0, (filletMass + mass)); } } public double getFilletVolume() { /* * Here is how the volume of the fillet is found. It assumes a circular concave * fillet tangent to the fin and the body tube. * * 1. Form a triangle with vertices at the BT center, the tangent point between * the fillet and the fin, and the center of the fillet radius. * 2. The line between the center of the BT and the center of the fillet radius * will pass through the tangent point between the fillet and the BT. * 3. Find the area of the triangle, then subtract the portion of the BT and * fillet that is in that triangle. (angle/2PI * pi*r^2= angle/2 * r^2) * 4. Multiply the remaining area by the length. * 5. Return twice that since there is a fillet on each side of the fin. * */ double btRadius = 1000.0; // assume a really big body tube if we can't get the radius, RocketComponent c = this.getParent(); if (BodyTube.class.isInstance(c)) { btRadius = ((BodyTube) c).getOuterRadius(); } double totalRad = filletRadius + btRadius; double innerAngle = Math.asin(filletRadius / totalRad); double outerAngle = Math.acos(filletRadius / totalRad); double outerArea = Math.tan(outerAngle) * filletRadius * filletRadius / 2; double filletVolume = length * (outerArea - outerAngle * filletRadius * filletRadius / 2 - innerAngle * btRadius * btRadius / 2); return 2 * filletVolume; } private void calculateAreaCG() { Coordinate[] points = this.getFinPoints(); finArea = 0; finCGx = 0; finCGy = 0; for (int i = 0; i < points.length - 1; i++) { final double x0 = points[i].x; final double x1 = points[i + 1].x; final double y0 = points[i].y; final double y1 = points[i + 1].y; double da = (y0 + y1) * (x1 - x0) / 2; finArea += da; if (Math.abs(y0 + y1) < 0.00001) { finCGx += (x0 + x1) / 2 * da; finCGy += y0 / 2 * da; } else { finCGx += (x0 * (2 * y0 + y1) + x1 * (y0 + 2 * y1)) / (3 * (y0 + y1)) * da; finCGy += (y1 + y0 * y0 / (y0 + y1)) / 3 * da; } } if (finArea < 0) finArea = 0; // Add effect of fin tabs to CG double tabArea = tabLength * tabHeight; if (!MathUtil.equals(tabArea, 0)) { double x = (getTabFrontEdge() + getTabTrailingEdge()) / 2; double y = -this.tabHeight / 2; finCGx += x * tabArea; finCGy += y * tabArea; } if ((finArea + tabArea) > 0) { finCGx /= (finArea + tabArea); finCGy /= (finArea + tabArea); } else { finCGx = (points[0].x + points[points.length - 1].x) / 2; finCGy = 0; } } /* * Return an approximation of the longitudinal unitary inertia of the fin set. * The process is the following: * * 1. Approximate the fin with a rectangular fin * * 2. The inertia of one fin is taken as the average of the moments of inertia * through its center perpendicular to the plane, and the inertia through * its center parallel to the plane * * 3. If there are multiple fins, the inertia is shifted to the center of the fin * set and multiplied by the number of fins. */ @Override public double getLongitudinalUnitInertia() { double area = getFinArea(); if (MathUtil.equals(area, 0)) return 0; // Approximate fin with a rectangular fin // w2 and h2 are squares of the fin width and height double w = getLength(); double h = getSpan(); double w2, h2; if (MathUtil.equals(w * h, 0)) { w2 = area; h2 = area; } else { w2 = w * area / h; h2 = h * area / w; } double inertia = (h2 + 2 * w2) / 24; if (fins == 1) return inertia; double radius = getBodyRadius(); return fins * (inertia + MathUtil.pow2(MathUtil.safeSqrt(h2) + radius)); } /* * Return an approximation of the rotational unitary inertia of the fin set. * The process is the following: * * 1. Approximate the fin with a rectangular fin and calculate the inertia of the * rectangular approximate * * 2. If there are multiple fins, shift the inertia center to the fin set center * and multiply with the number of fins. */ @Override public double getRotationalUnitInertia() { double area = getFinArea(); if (MathUtil.equals(area, 0)) return 0; // Approximate fin with a rectangular fin double w = getLength(); double h = getSpan(); if (MathUtil.equals(w * h, 0)) { h = MathUtil.safeSqrt(area); } else { h = MathUtil.safeSqrt(h * area / w); } if (fins == 1) return h * h / 12; double radius = getBodyRadius(); return fins * (h * h / 12 + MathUtil.pow2(h / 2 + radius)); } /** * Adds the fin set's bounds to the collection. */ @Override public Collection<Coordinate> getComponentBounds() { List<Coordinate> bounds = new ArrayList<Coordinate>(); double r = getBodyRadius(); for (Coordinate point : getFinPoints()) { addFinBound(bounds, point.x, point.y + r); } return bounds; } /** * Adds the 2d-coordinate bound (x,y) to the collection for both z-components and for * all fin rotations. */ private void addFinBound(Collection<Coordinate> set, double x, double y) { Coordinate c; int i; c = new Coordinate(x, y, thickness / 2); c = baseRotation.transform(c); set.add(c); for (i = 1; i < fins; i++) { c = finRotation.transform(c); set.add(c); } c = new Coordinate(x, y, -thickness / 2); c = baseRotation.transform(c); set.add(c); for (i = 1; i < fins; i++) { c = finRotation.transform(c); set.add(c); } } @Override public void componentChanged(ComponentChangeEvent e) { if (e.isAerodynamicChange()) { finArea = -1; cantRotation = null; } } /** * Return the radius of the BodyComponent the fin set is situated on. Currently * only supports SymmetricComponents and returns the radius at the starting point of the * root chord. * * @return radius of the underlying BodyComponent or 0 if none exists. */ public double getBodyRadius() { RocketComponent s; s = this.getParent(); while (s != null) { if (s instanceof SymmetricComponent) { double x = this.toRelative(new Coordinate(0, 0, 0), s)[0].x; return ((SymmetricComponent) s).getRadius(x); } s = s.getParent(); } return 0; } @Override public boolean allowsChildren() { return false; } /** * Allows nothing to be attached to a FinSet. * * @return <code>false</code> */ @Override public boolean isCompatible(Class<? extends RocketComponent> type) { return false; } /** * Return a list of coordinates defining the geometry of a single fin. * The coordinates are the XY-coordinates of points defining the shape of a single fin, * where the origin is the leading root edge. Therefore, the first point must be (0,0,0). * All Z-coordinates must be zero, and the last coordinate must have Y=0. * * @return List of XY-coordinates. */ public abstract Coordinate[] getFinPoints(); /** * Return a list of coordinates defining the geometry of a single fin, including a * possible fin tab. The coordinates are the XY-coordinates of points defining the * shape of a single fin, where the origin is the leading root edge. This implementation * calls {@link #getFinPoints()} and adds the necessary points for the fin tab. * The tab coordinates will have a negative y value. * * @return List of XY-coordinates. */ public Coordinate[] getFinPointsWithTab() { Coordinate[] points = getFinPoints(); if (MathUtil.equals(getTabHeight(), 0) || MathUtil.equals(getTabLength(), 0)) return points; double x1 = getTabFrontEdge(); double x2 = getTabTrailingEdge(); double y = -getTabHeight(); boolean add1 = x1 != points[0].x; boolean add2 = x2 != points[points.length - 1].x; int n = points.length; points = ArrayUtils.copyOf(points, points.length + 2 + (add1 ? 1 : 0) + (add2 ? 1 : 0)); if (add2) points[n++] = new Coordinate(x2, 0); points[n++] = new Coordinate(x2, y); points[n++] = new Coordinate(x1, y); if (add1) points[n++] = new Coordinate(x1, 0); return points; } /** * Get the span of a single fin. That is, the length from the root to the tip of the fin. * @return Span of a single fin. */ public abstract double getSpan(); @Override protected List<RocketComponent> copyFrom(RocketComponent c) { FinSet src = (FinSet) c; this.fins = src.fins; this.finRotation = src.finRotation; this.rotation = src.rotation; this.baseRotation = src.baseRotation; this.cantAngle = src.cantAngle; this.cantRotation = src.cantRotation; this.thickness = src.thickness; this.crossSection = src.crossSection; this.tabHeight = src.tabHeight; this.tabLength = src.tabLength; this.tabRelativePosition = src.tabRelativePosition; this.tabShift = src.tabShift; return super.copyFrom(c); } /* * Handle fin fillet mass properties */ public Material getFilletMaterial() { return filletMaterial; } public void setFilletMaterial(Material mat) { if (mat.getType() != Material.Type.BULK) { throw new IllegalArgumentException("ExternalComponent requires a bulk material" + " type=" + mat.getType()); } if (filletMaterial.equals(mat)) return; filletMaterial = mat; clearPreset(); fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } public double getFilletRadius() { return filletRadius; } public void setFilletRadius(double r) { if (MathUtil.equals(filletRadius, r)) return; filletRadius = r; clearPreset(); fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } }