diff --git a/core/resources-src/pix/componenticons/tubefin.xcf.gz b/core/resources-src/pix/componenticons/tubefin.xcf.gz new file mode 100644 index 000000000..ff965698d Binary files /dev/null and b/core/resources-src/pix/componenticons/tubefin.xcf.gz differ diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index d05069199..72f5c27ab 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -662,6 +662,7 @@ compaddbuttons.Transition = Transition compaddbuttons.Trapezoidal = Trapezoidal compaddbuttons.Elliptical = Elliptical compaddbuttons.Freeform = Freeform +compaddbuttons.Tubefin = Tube fins compaddbuttons.Launchlug = Launch lug compaddbuttons.Innercomponent = Inner component compaddbuttons.Innertube = Inner tube @@ -921,6 +922,15 @@ FreeformFinSetConfig.lbl.clickDrag = Click+drag: Add and move points FreeformFinSetConfig.lbl.ctrlClick = Ctrl+click: Remove point FreeformFinSetConfig.lbl.scaleFin = Scale Fin +!TubeFinSetConfig +TubeFinSetCfg.lbl.Nbroffins = Number of fins: +TubeFinSetCfg.lbl.Length = Length: +TubeFinSetCfg.lbl.Outerdiam = Outer diameter: +TubeFinSetCfg.checkbox.Automatic = Automatic +TubeFinSetCfg.lbl.Innerdiam = Inner diameter: +TubeFinSetCfg.lbl.Thickness = Thickness: +TubeFinSetCfg.lbl.Finrotation = Fin rotation: +TubeFinSetCfg.lbl.ttip.Finrotation = The angle of the first fin in the fin set. !InnerTubeConfig InnerTubeCfg.tab.Motor = Motor @@ -1379,6 +1389,8 @@ InnerTube.InnerTube = Inner Tube TrapezoidFinSet.TrapezoidFinSet = Trapezoidal fin set ! FreeformFinSet FreeformFinSet.FreeformFinSet = Freeform fin set +! TubeFinSEt +TubeFinSet.TubeFinSet = Tube fin set !MassComponent MassComponent.MassComponent = Unspecified MassComponent.Altimeter = Altimeter @@ -1429,6 +1441,7 @@ ComponentIcons.Transition = Transition ComponentIcons.Trapezoidalfinset = Trapezoidal fin set ComponentIcons.Ellipticalfinset = Elliptical fin set ComponentIcons.Freeformfinset = Freeform fin set +ComponentIcons.Tubefinset = Tube fin set ComponentIcons.Launchlug = Launch lug ComponentIcons.Innertube = Inner tube ComponentIcons.Tubecoupler = Tube coupler diff --git a/core/resources/pix/componenticons/tubefin-large.png b/core/resources/pix/componenticons/tubefin-large.png new file mode 100644 index 000000000..f16027fb6 Binary files /dev/null and b/core/resources/pix/componenticons/tubefin-large.png differ diff --git a/core/resources/pix/componenticons/tubefin-small.png b/core/resources/pix/componenticons/tubefin-small.png new file mode 100644 index 000000000..e1aa89e52 Binary files /dev/null and b/core/resources/pix/componenticons/tubefin-small.png differ diff --git a/core/src/net/sf/openrocket/aerodynamics/barrowman/TubeFinSetCalc.java b/core/src/net/sf/openrocket/aerodynamics/barrowman/TubeFinSetCalc.java new file mode 100644 index 000000000..d6e1d56d7 --- /dev/null +++ b/core/src/net/sf/openrocket/aerodynamics/barrowman/TubeFinSetCalc.java @@ -0,0 +1,675 @@ +package net.sf.openrocket.aerodynamics.barrowman; + +import static java.lang.Math.pow; +import static net.sf.openrocket.util.MathUtil.pow2; + +import java.util.Arrays; + +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.Warning.Other; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.TubeFinSet; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.LinearInterpolator; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.PolyInterpolator; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Preliminary computation of tube fin aerodynamics. + * + * Uses a complete clone of FinSetCalc modelling each tube fin as 3 individual fins. It does not correctly account for + * fin & tube fin interference. + * + * Changes to BarrowmanCalculator's calculateFrictionDrag are also probably required. + * + * @author kruland + * + */ +public class TubeFinSetCalc extends RocketComponentCalc { + + private final static Logger logger = LoggerFactory.getLogger(FinSetCalc.class); + + private static final double STALL_ANGLE = (20 * Math.PI / 180); + + /** Number of divisions in the fin chords. */ + protected static final int DIVISIONS = 48; + + protected double macLength = Double.NaN; // MAC length + protected double macLead = Double.NaN; // MAC leading edge position + protected double macSpan = Double.NaN; // MAC spanwise position + protected double finArea = Double.NaN; // Fin area + protected double ar = Double.NaN; // Fin aspect ratio + protected double span = Double.NaN; // Fin span + protected double cosGamma = Double.NaN; // Cosine of midchord sweep angle + protected double cosGammaLead = Double.NaN; // Cosine of leading edge sweep angle + protected double rollSum = Double.NaN; // Roll damping sum term + + + protected double[] chordLead = new double[DIVISIONS]; + protected double[] chordTrail = new double[DIVISIONS]; + protected double[] chordLength = new double[DIVISIONS]; + + private double[] poly = new double[6]; + + private final double thickness; + private final double bodyRadius; + private final int finCount; + private final double baseRotation; + private final double cantAngle; + protected final int interferenceFinCount; + private final FinSet.CrossSection crossSection; + + public TubeFinSetCalc(RocketComponent component) { + super(component); + if (!(component instanceof TubeFinSet)) { + throw new IllegalArgumentException("Illegal component type " + component); + } + + TubeFinSet fin = (TubeFinSet) component; + thickness = fin.getThickness(); + bodyRadius = fin.getBodyRadius(); + finCount = 3 * fin.getFinCount(); + baseRotation = fin.getBaseRotation(); + cantAngle = 0; + span = 2 * fin.getOuterRadius(); + finArea = span * fin.getLength(); + crossSection = FinSet.CrossSection.SQUARE; + + calculateFinGeometry(fin); + calculatePoly(); + interferenceFinCount = calculateInterferenceFinCount(fin); + } + + /* + * Calculates the non-axial forces produced by the fins (normal and side forces, + * pitch, yaw and roll moments, CP position, CNa). + */ + @Override + public void calculateNonaxialForces(FlightConditions conditions, + AerodynamicForces forces, WarningSet warnings) { + + if (span < 0.001) { + forces.setCm(0); + forces.setCN(0); + forces.setCNa(0); + forces.setCP(Coordinate.NUL); + forces.setCroll(0); + forces.setCrollDamp(0); + forces.setCrollForce(0); + forces.setCside(0); + forces.setCyaw(0); + return; + } + + // Add warnings (radius/2 == diameter/4) + if (thickness > bodyRadius / 2) { + warnings.add(Warning.THICK_FIN); + } + warnings.add(new Other("Tube fin support is experimental")); + + //////// Calculate CNa. ///////// + + // One fin without interference (both sub- and supersonic): + double cna1 = calculateFinCNa1(conditions); + + // logger.debug("Component cna1 = {}", cna1); + + // Multiple fins with fin-fin interference + double cna; + double theta = conditions.getTheta(); + double angle = baseRotation; + + // Compute basic CNa without interference effects + if (finCount == 1 || finCount == 2) { + // Basic CNa from geometry + double mul = 0; + for (int i = 0; i < finCount; i++) { + mul += MathUtil.pow2(Math.sin(theta - angle)); + angle += 2 * Math.PI / finCount; + } + cna = cna1 * mul; + } else { + // Basic CNa assuming full efficiency + cna = cna1 * finCount / 2.0; + } + + // logger.debug("Component cna = {}", cna); + + // Take into account fin-fin interference effects + switch (interferenceFinCount) { + case 1: + case 2: + case 3: + case 4: + // No interference effect + break; + + case 5: + cna *= 0.948; + break; + + case 6: + cna *= 0.913; + break; + + case 7: + cna *= 0.854; + break; + + case 8: + cna *= 0.81; + break; + + default: + // Assume 75% efficiency + cna *= 0.75; + break; + } + + + // Body-fin interference effect + double r = bodyRadius; + double tau = r / (span + r); + if (Double.isNaN(tau) || Double.isInfinite(tau)) + tau = 0; + cna *= 1 + tau; // Classical Barrowman + // cna *= pow2(1 + tau); // Barrowman thesis (too optimistic??) + // logger.debug("Component cna = {}", cna); + + // TODO: LOW: check for fin tip mach cone interference + // (Barrowman thesis pdf-page 40) + + // TODO: LOW: fin-fin mach cone effect, MIL-HDBK page 5-25 + + // Calculate CP position + double x = macLead + calculateCPPos(conditions) * macLength; + // logger.debug("Component macLead = {}", macLead); + // logger.debug("Component macLength = {}", macLength); + // logger.debug("Component x = {}", x); + + + // Calculate roll forces, reduce forcing above stall angle + + // Without body-fin interference effect: + // forces.CrollForce = fins * (macSpan+r) * cna1 * component.getCantAngle() / + // conditions.getRefLength(); + // With body-fin interference effect: + forces.setCrollForce(finCount * (macSpan + r) * cna1 * (1 + tau) * cantAngle / conditions.getRefLength()); + + if (conditions.getAOA() > STALL_ANGLE) { + // System.out.println("Fin stalling in roll"); + forces.setCrollForce(forces.getCrollForce() * MathUtil.clamp( + 1 - (conditions.getAOA() - STALL_ANGLE) / (STALL_ANGLE / 2), 0, 1)); + } + forces.setCrollDamp(calculateDampingMoment(conditions)); + forces.setCroll(forces.getCrollForce() - forces.getCrollDamp()); + + // System.out.printf(component.getName() + ": roll rate:%.3f force:%.3f damp:%.3f " + + // "total:%.3f\n", + // conditions.getRollRate(), forces.CrollForce, forces.CrollDamp, forces.Croll); + + forces.setCNa(cna); + forces.setCN(cna * MathUtil.min(conditions.getAOA(), STALL_ANGLE)); + forces.setCP(new Coordinate(x, 0, 0, cna)); + forces.setCm(forces.getCN() * x / conditions.getRefLength()); + + /* + * TODO: HIGH: Compute actual side force and yaw moment. + * This is not currently performed because it produces strange results for + * stable rockets that have two fins in the front part of the fuselage, + * where the rocket flies at an ever-increasing angle of attack. This may + * be due to incorrect computation of pitch/yaw damping moments. + */ + // if (fins == 1 || fins == 2) { + // forces.Cside = fins * cna1 * Math.cos(theta-angle) * Math.sin(theta-angle); + // forces.Cyaw = fins * forces.Cside * x / conditions.getRefLength(); + // } else { + // forces.Cside = 0; + // forces.Cyaw = 0; + // } + forces.setCside(0); + forces.setCyaw(0); + + } + + /** + * Returns the MAC length of the fin. This is required in the friction drag + * computation. + * + * @return the MAC length of the fin. + */ + public double getMACLength() { + return macLength; + } + + public double getMidchordPos() { + return macLead + 0.5 * macLength; + } + + /** + * Pre-calculates the fin geometry values. + */ + protected void calculateFinGeometry(TubeFinSet component) { + + ar = 2 * pow2(span) / finArea; + + Coordinate[] points = { + Coordinate.NUL, + new Coordinate(0, span), + new Coordinate(component.getLength(), span), + new Coordinate(component.getLength(), 0) + }; + + + // Calculate the chord lead and trail positions and length + + Arrays.fill(chordLead, Double.POSITIVE_INFINITY); + Arrays.fill(chordTrail, Double.NEGATIVE_INFINITY); + Arrays.fill(chordLength, 0); + + for (int point = 1; point < points.length; point++) { + double x1 = points[point - 1].x; + double y1 = points[point - 1].y; + double x2 = points[point].x; + double y2 = points[point].y; + + // Don't use the default EPSILON since it is too small + // and causes too much numerical instability in the computation of x below + if (MathUtil.equals(y1, y2, 0.001)) + continue; + + int i1 = (int) (y1 * 1.0001 / span * (DIVISIONS - 1)); + int i2 = (int) (y2 * 1.0001 / span * (DIVISIONS - 1)); + i1 = MathUtil.clamp(i1, 0, DIVISIONS - 1); + i2 = MathUtil.clamp(i2, 0, DIVISIONS - 1); + if (i1 > i2) { + int tmp = i2; + i2 = i1; + i1 = tmp; + } + + for (int i = i1; i <= i2; i++) { + // Intersection point (x,y) + double y = i * span / (DIVISIONS - 1); + double x = (y - y2) / (y1 - y2) * x1 + (y1 - y) / (y1 - y2) * x2; + if (x < chordLead[i]) + chordLead[i] = x; + if (x > chordTrail[i]) + chordTrail[i] = x; + + // TODO: LOW: If fin point exactly on chord line, might be counted twice: + if (y1 < y2) { + chordLength[i] -= x; + } else { + chordLength[i] += x; + } + } + } + + // Check and correct any inconsistencies + for (int i = 0; i < DIVISIONS; i++) { + if (Double.isInfinite(chordLead[i]) || Double.isInfinite(chordTrail[i]) || + Double.isNaN(chordLead[i]) || Double.isNaN(chordTrail[i])) { + chordLead[i] = 0; + chordTrail[i] = 0; + } + if (chordLength[i] < 0 || Double.isNaN(chordLength[i])) { + chordLength[i] = 0; + } + if (chordLength[i] > chordTrail[i] - chordLead[i]) { + chordLength[i] = chordTrail[i] - chordLead[i]; + } + } + + /* Calculate fin properties: + * + * macLength // MAC length + * macLead // MAC leading edge position + * macSpan // MAC spanwise position + * ar // Fin aspect ratio (already set) + * span // Fin span (already set) + */ + macLength = 0; + macLead = 0; + macSpan = 0; + cosGamma = 0; + cosGammaLead = 0; + rollSum = 0; + double area = 0; + double radius = component.getBodyRadius(); + + final double dy = span / (DIVISIONS - 1); + for (int i = 0; i < DIVISIONS; i++) { + double length = chordTrail[i] - chordLead[i]; + double y = i * dy; + + macLength += length * length; + logger.debug("macLength = {}, length = {}, i = {}", macLength, length, i); + macSpan += y * length; + macLead += chordLead[i] * length; + area += length; + rollSum += chordLength[i] * pow2(radius + y); + + if (i > 0) { + double dx = (chordTrail[i] + chordLead[i]) / 2 - (chordTrail[i - 1] + chordLead[i - 1]) / 2; + cosGamma += dy / MathUtil.hypot(dx, dy); + + dx = chordLead[i] - chordLead[i - 1]; + cosGammaLead += dy / MathUtil.hypot(dx, dy); + } + } + + macLength *= dy; + logger.debug("macLength = {}", macLength); + macSpan *= dy; + macLead *= dy; + area *= dy; + rollSum *= dy; + + macLength /= area; + macSpan /= area; + macLead /= area; + cosGamma /= (DIVISIONS - 1); + cosGammaLead /= (DIVISIONS - 1); + } + + /////////////// CNa1 calculation //////////////// + + private static final double CNA_SUBSONIC = 0.9; + private static final double CNA_SUPERSONIC = 1.5; + private static final double CNA_SUPERSONIC_B = pow(pow2(CNA_SUPERSONIC) - 1, 1.5); + private static final double GAMMA = 1.4; + private static final LinearInterpolator K1, K2, K3; + private static final PolyInterpolator cnaInterpolator = new PolyInterpolator( + new double[] { CNA_SUBSONIC, CNA_SUPERSONIC }, + new double[] { CNA_SUBSONIC, CNA_SUPERSONIC }, + new double[] { CNA_SUBSONIC } + ); + /* Pre-calculate the values for K1, K2 and K3 */ + static { + // Up to Mach 5 + int n = (int) ((5.0 - CNA_SUPERSONIC) * 10); + double[] x = new double[n]; + double[] k1 = new double[n]; + double[] k2 = new double[n]; + double[] k3 = new double[n]; + for (int i = 0; i < n; i++) { + double M = CNA_SUPERSONIC + i * 0.1; + double beta = MathUtil.safeSqrt(M * M - 1); + x[i] = M; + k1[i] = 2.0 / beta; + k2[i] = ((GAMMA + 1) * pow(M, 4) - 4 * pow2(beta)) / (4 * pow(beta, 4)); + k3[i] = ((GAMMA + 1) * pow(M, 8) + (2 * pow2(GAMMA) - 7 * GAMMA - 5) * pow(M, 6) + + 10 * (GAMMA + 1) * pow(M, 4) + 8) / (6 * pow(beta, 7)); + } + K1 = new LinearInterpolator(x, k1); + K2 = new LinearInterpolator(x, k2); + K3 = new LinearInterpolator(x, k3); + + // System.out.println("K1[m="+CNA_SUPERSONIC+"] = "+k1[0]); + // System.out.println("K2[m="+CNA_SUPERSONIC+"] = "+k2[0]); + // System.out.println("K3[m="+CNA_SUPERSONIC+"] = "+k3[0]); + } + + protected double calculateFinCNa1(FlightConditions conditions) { + double mach = conditions.getMach(); + double ref = conditions.getRefArea(); + double alpha = MathUtil.min(conditions.getAOA(), + Math.PI - conditions.getAOA(), STALL_ANGLE); + + // Subsonic case + if (mach <= CNA_SUBSONIC) { + return 2 * Math.PI * pow2(span) / (1 + MathUtil.safeSqrt(1 + (1 - pow2(mach)) * + pow2(pow2(span) / (finArea * cosGamma)))) / ref; + } + + // Supersonic case + if (mach >= CNA_SUPERSONIC) { + return finArea * (K1.getValue(mach) + K2.getValue(mach) * alpha + + K3.getValue(mach) * pow2(alpha)) / ref; + } + + // Transonic case, interpolate + double subV, superV; + double subD, superD; + + double sq = MathUtil.safeSqrt(1 + (1 - pow2(CNA_SUBSONIC)) * pow2(span * span / (finArea * cosGamma))); + subV = 2 * Math.PI * pow2(span) / ref / (1 + sq); + subD = 2 * mach * Math.PI * pow(span, 6) / (pow2(finArea * cosGamma) * ref * + sq * pow2(1 + sq)); + + superV = finArea * (K1.getValue(CNA_SUPERSONIC) + K2.getValue(CNA_SUPERSONIC) * alpha + + K3.getValue(CNA_SUPERSONIC) * pow2(alpha)) / ref; + superD = -finArea / ref * 2 * CNA_SUPERSONIC / CNA_SUPERSONIC_B; + + // System.out.println("subV="+subV+" superV="+superV+" subD="+subD+" superD="+superD); + + return cnaInterpolator.interpolate(mach, subV, superV, subD, superD, 0); + } + + private double calculateDampingMoment(FlightConditions conditions) { + double rollRate = conditions.getRollRate(); + + if (Math.abs(rollRate) < 0.1) + return 0; + + double mach = conditions.getMach(); + double absRate = Math.abs(rollRate); + + /* + * At low speeds and relatively large roll rates (i.e. near apogee) the + * fin tips rotate well above stall angle. In this case sum the chords + * separately. + */ + if (absRate * (bodyRadius + span) / conditions.getVelocity() > 15 * Math.PI / 180) { + double sum = 0; + for (int i = 0; i < DIVISIONS; i++) { + double dist = bodyRadius + span * i / DIVISIONS; + double aoa = Math.min(absRate * dist / conditions.getVelocity(), 15 * Math.PI / 180); + sum += chordLength[i] * dist * aoa; + } + sum = sum * (span / DIVISIONS) * 2 * Math.PI / conditions.getBeta() / + (conditions.getRefArea() * conditions.getRefLength()); + + // System.out.println("SPECIAL: " + + // (MathUtil.sign(rollRate) *component.getFinCount() * sum)); + return MathUtil.sign(rollRate) * finCount * sum; + } + + if (mach <= CNA_SUBSONIC) { + // System.out.println("BASIC: "+ + // (component.getFinCount() * 2*Math.PI * rollRate * rollSum / + // (conditions.getRefArea() * conditions.getRefLength() * + // conditions.getVelocity() * conditions.getBeta()))); + + return finCount * 2 * Math.PI * rollRate * rollSum / + (conditions.getRefArea() * conditions.getRefLength() * + conditions.getVelocity() * conditions.getBeta()); + } + if (mach >= CNA_SUPERSONIC) { + + double vel = conditions.getVelocity(); + double k1 = K1.getValue(mach); + double k2 = K2.getValue(mach); + double k3 = K3.getValue(mach); + + double sum = 0; + + for (int i = 0; i < DIVISIONS; i++) { + double y = i * span / (DIVISIONS - 1); + double angle = rollRate * (bodyRadius + y) / vel; + + sum += (k1 * angle + k2 * angle * angle + k3 * angle * angle * angle) + * chordLength[i] * (bodyRadius + y); + } + + return finCount * sum * span / (DIVISIONS - 1) / + (conditions.getRefArea() * conditions.getRefLength()); + } + + // Transonic, do linear interpolation + + FlightConditions cond = conditions.clone(); + cond.setMach(CNA_SUBSONIC - 0.01); + double subsonic = calculateDampingMoment(cond); + cond.setMach(CNA_SUPERSONIC + 0.01); + double supersonic = calculateDampingMoment(cond); + + return subsonic * (CNA_SUPERSONIC - mach) / (CNA_SUPERSONIC - CNA_SUBSONIC) + + supersonic * (mach - CNA_SUBSONIC) / (CNA_SUPERSONIC - CNA_SUBSONIC); + } + + /** + * Return the relative position of the CP along the mean aerodynamic chord. + * Below mach 0.5 it is at the quarter chord, above mach 2 calculated using an + * empirical formula, between these two using an interpolation polynomial. + * + * @param cond Mach speed used + * @return CP position along the MAC + */ + private double calculateCPPos(FlightConditions cond) { + double m = cond.getMach(); + // logger.debug("m = {} ", m); + if (m <= 0.5) { + // At subsonic speeds CP at quarter chord + return 0.25; + } + if (m >= 2) { + // At supersonic speeds use empirical formula + double beta = cond.getBeta(); + return (ar * beta - 0.67) / (2 * ar * beta - 1); + } + + // In between use interpolation polynomial + double x = 1.0; + double val = 0; + + for (int i = 0; i < poly.length; i++) { + val += poly[i] * x; + x *= m; + } + // logger.debug("val = {}", val); + return val; + } + + /** + * Calculate CP position interpolation polynomial coefficients from the + * fin geometry. This is a fifth order polynomial that satisfies + * + * p(0.5)=0.25 + * p'(0.5)=0 + * p(2) = f(2) + * p'(2) = f'(2) + * p''(2) = 0 + * p'''(2) = 0 + * + * where f(M) = (ar*sqrt(M^2-1) - 0.67) / (2*ar*sqrt(M^2-1) - 1). + * + * The values were calculated analytically in Mathematica. The coefficients + * are used as poly[0] + poly[1]*x + poly[2]*x^2 + ... + */ + private void calculatePoly() { + double denom = pow2(1 - 3.4641 * ar); // common denominator + + poly[5] = (-1.58025 * (-0.728769 + ar) * (-0.192105 + ar)) / denom; + poly[4] = (12.8395 * (-0.725688 + ar) * (-0.19292 + ar)) / denom; + poly[3] = (-39.5062 * (-0.72074 + ar) * (-0.194245 + ar)) / denom; + poly[2] = (55.3086 * (-0.711482 + ar) * (-0.196772 + ar)) / denom; + poly[1] = (-31.6049 * (-0.705375 + ar) * (-0.198476 + ar)) / denom; + poly[0] = (9.16049 * (-0.588838 + ar) * (-0.20624 + ar)) / denom; + } + + + // @SuppressWarnings("null") + // public static void main(String arg[]) { + // Rocket rocket = TestRocket.makeRocket(); + // FinSet finset = null; + // + // Iterator iter = rocket.deepIterator(); + // while (iter.hasNext()) { + // RocketComponent c = iter.next(); + // if (c instanceof FinSet) { + // finset = (FinSet)c; + // break; + // } + // } + // + // ((TrapezoidFinSet)finset).setHeight(0.10); + // ((TrapezoidFinSet)finset).setRootChord(0.10); + // ((TrapezoidFinSet)finset).setTipChord(0.10); + // ((TrapezoidFinSet)finset).setSweep(0.0); + // + // + // FinSetCalc calc = new FinSetCalc(finset); + // + // calc.calculateFinGeometry(); + // FlightConditions cond = new FlightConditions(new Configuration(rocket)); + // for (double m=0; m < 3; m+=0.05) { + // cond.setMach(m); + // cond.setAOA(0.0*Math.PI/180); + // double cna = calc.calculateFinCNa1(cond); + // System.out.printf("%5.2f "+cna+"\n", m); + // } + // + // } + + @Override + public double calculatePressureDragForce(FlightConditions conditions, + double stagnationCD, double baseCD, WarningSet warnings) { + + double mach = conditions.getMach(); + double drag = 0; + + // Pressure fore-drag + if (crossSection == FinSet.CrossSection.AIRFOIL || + crossSection == FinSet.CrossSection.ROUNDED) { + + // Round leading edge + if (mach < 0.9) { + drag = Math.pow(1 - pow2(mach), -0.417) - 1; + } else if (mach < 1) { + drag = 1 - 1.785 * (mach - 0.9); + } else { + drag = 1.214 - 0.502 / pow2(mach) + 0.1095 / pow2(pow2(mach)); + } + + } else if (crossSection == FinSet.CrossSection.SQUARE) { + drag = stagnationCD; + } else { + throw new UnsupportedOperationException("Unsupported fin profile: " + crossSection); + } + + // Slanted leading edge + drag *= pow2(cosGammaLead); + + // Trailing edge drag + if (crossSection == FinSet.CrossSection.SQUARE) { + drag += baseCD; + } else if (crossSection == FinSet.CrossSection.ROUNDED) { + drag += baseCD / 2; + } + // Airfoil assumed to have zero base drag + + // Scale to correct reference area + drag *= finCount * span * thickness / conditions.getRefArea(); + + return drag; + } + + private static int calculateInterferenceFinCount(TubeFinSet component) { + RocketComponent parent = component.getParent(); + if (parent == null) { + throw new IllegalStateException("fin set without parent component"); + } + + return 3 * component.getFinCount(); + } + +} diff --git a/core/src/net/sf/openrocket/appearance/defaults/DefaultAppearance.java b/core/src/net/sf/openrocket/appearance/defaults/DefaultAppearance.java index d0ca34ef2..82c36c0dd 100644 --- a/core/src/net/sf/openrocket/appearance/defaults/DefaultAppearance.java +++ b/core/src/net/sf/openrocket/appearance/defaults/DefaultAppearance.java @@ -18,6 +18,7 @@ import net.sf.openrocket.rocketcomponent.RadiusRingComponent; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.Transition; import net.sf.openrocket.rocketcomponent.TubeCoupler; +import net.sf.openrocket.rocketcomponent.TubeFinSet; import net.sf.openrocket.util.Color; import net.sf.openrocket.util.Coordinate; @@ -77,7 +78,7 @@ public class DefaultAppearance { public static Appearance getDefaultAppearance(RocketComponent c) { if (c instanceof BodyTube) return ESTES_BT; - if (c instanceof InnerTube || c instanceof TubeCoupler) + if (c instanceof InnerTube || c instanceof TubeCoupler || c instanceof TubeFinSet) return ESTES_IT; if (c instanceof FinSet) return BALSA; diff --git a/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java index 4fa7c4e57..c22099caa 100644 --- a/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java +++ b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java @@ -24,6 +24,7 @@ import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.Stage; import net.sf.openrocket.rocketcomponent.TubeCoupler; +import net.sf.openrocket.rocketcomponent.TubeFinSet; import net.sf.openrocket.simulation.FlightData; import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.FlightDataType; @@ -223,6 +224,7 @@ public class OpenRocketSaver extends RocketSaver { * * File version 1.7 is required for: * - simulation extensions + * - saving tube fins. * * File version 1.6 is required for: * - saving files using appearances and textures, flight configurations. @@ -252,6 +254,13 @@ public class OpenRocketSaver extends RocketSaver { } } + // Search the rocket for any TubeFinSet objects (version 1.7) + for (RocketComponent c : document.getRocket()) { + if (c instanceof TubeFinSet) { + return FILE_VERSION_DIVISOR + 7; + } + } + ///////////////// // Version 1.6 // diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java b/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java index 4734ddffb..129f17bfe 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/DocumentConfig.java @@ -40,6 +40,7 @@ import net.sf.openrocket.rocketcomponent.ThicknessRingComponent; import net.sf.openrocket.rocketcomponent.Transition; import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; import net.sf.openrocket.rocketcomponent.TubeCoupler; +import net.sf.openrocket.rocketcomponent.TubeFinSet; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.Color; import net.sf.openrocket.util.LineStyle; @@ -69,6 +70,7 @@ class DocumentConfig { constructors.put("trapezoidfinset", TrapezoidFinSet.class.getConstructor(new Class[0])); constructors.put("ellipticalfinset", EllipticalFinSet.class.getConstructor(new Class[0])); constructors.put("freeformfinset", FreeformFinSet.class.getConstructor(new Class[0])); + constructors.put("tubefinset", TubeFinSet.class.getConstructor(new Class[0])); constructors.put("launchlug", LaunchLug.class.getConstructor(new Class[0])); // Internal components @@ -230,6 +232,20 @@ class DocumentConfig { // FreeformFinSet points handled as a special handler + // TubeFinSet + setters.put("TubeFinSet:fincount", new IntSetter( + Reflection.findMethod(TubeFinSet.class, "setFinCount", int.class))); + setters.put("TubeFinSet:rotation", new DoubleSetter( + Reflection.findMethod(TubeFinSet.class, "setBaseRotation", double.class), Math.PI / 180.0)); + setters.put("TubeFinSet:thickness", new DoubleSetter( + Reflection.findMethod(TubeFinSet.class, "setThickness", double.class))); + setters.put("TubeFinSet:length", new DoubleSetter( + Reflection.findMethod(TubeFinSet.class, "setLength", double.class))); + setters.put("TubeFinSet:radius", new DoubleSetter( + Reflection.findMethod(TubeFinSet.class, "setOuterRadius", double.class), + "auto", + Reflection.findMethod(TubeFinSet.class, "setOuterRadiusAutomatic", boolean.class))); + // LaunchLug setters.put("LaunchLug:radius", new DoubleSetter( Reflection.findMethod(LaunchLug.class, "setOuterRadius", double.class))); @@ -440,4 +456,4 @@ class DocumentConfig { return Double.NEGATIVE_INFINITY; return Double.parseDouble(s); } -} \ No newline at end of file +} diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/PositionSetter.java b/core/src/net/sf/openrocket/file/openrocket/importt/PositionSetter.java index fc7d00779..ad29d1e35 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/PositionSetter.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/PositionSetter.java @@ -9,6 +9,7 @@ import net.sf.openrocket.rocketcomponent.InternalComponent; import net.sf.openrocket.rocketcomponent.LaunchLug; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.RocketComponent.Position; +import net.sf.openrocket.rocketcomponent.TubeFinSet; class PositionSetter implements Setter { @@ -40,6 +41,9 @@ class PositionSetter implements Setter { } else if (c instanceof InternalComponent) { ((InternalComponent) c).setRelativePosition(type); c.setPositionValue(pos); + } else if (c instanceof TubeFinSet) { + ((TubeFinSet) c).setRelativePosition(type); + c.setPositionValue(pos); } else { warnings.add(Warning.FILE_INVALID_PARAMETER); } diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/TubeFinSetSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/TubeFinSetSaver.java new file mode 100644 index 000000000..af2ec3b2c --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/TubeFinSetSaver.java @@ -0,0 +1,38 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.rocketcomponent.TubeFinSet; + + +public class TubeFinSetSaver extends ExternalComponentSaver { + + private static final TubeFinSetSaver instance = new TubeFinSetSaver(); + + public static List getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + List list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + TubeFinSet fins = (TubeFinSet) c; + + elements.add("" + fins.getFinCount() + ""); + elements.add("" + (fins.getBaseRotation() * 180.0 / Math.PI) + ""); + if (fins.isOuterRadiusAutomatic()) + elements.add("auto"); + else + elements.add("" + fins.getOuterRadius() + ""); + elements.add("" + fins.getLength() + ""); + elements.add("" + fins.getThickness() + ""); + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java b/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java index 865835533..f9ae720ca 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java +++ b/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java @@ -281,8 +281,8 @@ public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial @Override public double getLongitudinalUnitInertia() { - // 1/12 * (3 * (r1^2 + r2^2) + h^2) - return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getOuterRadius()) + MathUtil.pow2(getLength())) / 12; + // 1/12 * (3 * (r2^2 + r1^2) + h^2) + return (3 * (MathUtil.pow2(getOuterRadius()) + MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getLength())) / 12; } @Override diff --git a/core/src/net/sf/openrocket/rocketcomponent/LaunchLug.java b/core/src/net/sf/openrocket/rocketcomponent/LaunchLug.java index 437094368..414c561e1 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/LaunchLug.java +++ b/core/src/net/sf/openrocket/rocketcomponent/LaunchLug.java @@ -86,7 +86,7 @@ public class LaunchLug extends ExternalComponent implements Coaxial { } - + public void setLength(double length) { if (MathUtil.equals(this.length, length)) return; @@ -95,9 +95,9 @@ public class LaunchLug extends ExternalComponent implements Coaxial { } - - - + + + @Override public void setRelativePosition(RocketComponent.Position position) { super.setRelativePosition(position); @@ -112,30 +112,30 @@ public class LaunchLug extends ExternalComponent implements Coaxial { } - + @Override protected void loadFromPreset(ComponentPreset preset) { - if ( preset.has(ComponentPreset.OUTER_DIAMETER) ) { + if (preset.has(ComponentPreset.OUTER_DIAMETER)) { double outerDiameter = preset.get(ComponentPreset.OUTER_DIAMETER); - this.radius = outerDiameter/2.0; - if ( preset.has(ComponentPreset.INNER_DIAMETER) ) { + this.radius = outerDiameter / 2.0; + if (preset.has(ComponentPreset.INNER_DIAMETER)) { double innerDiameter = preset.get(ComponentPreset.INNER_DIAMETER); - this.thickness = (outerDiameter-innerDiameter) / 2.0; + this.thickness = (outerDiameter - innerDiameter) / 2.0; } } - + super.loadFromPreset(preset); - + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } - - + + @Override public Type getPresetType() { return ComponentPreset.Type.LAUNCH_LUG; } - - + + @Override public Coordinate[] shiftCoordinates(Coordinate[] array) { array = super.shiftCoordinates(array); @@ -183,8 +183,8 @@ public class LaunchLug extends ExternalComponent implements Coaxial { } - - + + @Override public double getComponentVolume() { return length * Math.PI * (MathUtil.pow2(radius) - MathUtil.pow2(radius - thickness)); @@ -211,8 +211,8 @@ public class LaunchLug extends ExternalComponent implements Coaxial { @Override public double getLongitudinalUnitInertia() { - // 1/12 * (3 * (r1^2 + r2^2) + h^2) - return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getOuterRadius()) + MathUtil.pow2(getLength())) / 12; + // 1/12 * (3 * (r2^2 + r1^2) + h^2) + return (3 * (MathUtil.pow2(getOuterRadius()) + MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getLength())) / 12; } @Override diff --git a/core/src/net/sf/openrocket/rocketcomponent/TubeFinSet.java b/core/src/net/sf/openrocket/rocketcomponent/TubeFinSet.java new file mode 100644 index 000000000..9af60bff2 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/TubeFinSet.java @@ -0,0 +1,344 @@ +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.preset.ComponentPreset; +import net.sf.openrocket.preset.ComponentPreset.Type; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Transformation; + +public class TubeFinSet extends ExternalComponent { + private static final Translator trans = Application.getTranslator(); + + private final static double DEFAULT_RADIUS = 0.025; + + private boolean autoRadius = true; // Radius chosen automatically based on parent component + private double outerRadius = DEFAULT_RADIUS; + protected double thickness = 0.002; + + protected int fins = 6; + + /** + * 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); + + /** + * Rotation about the x-axis by 2*PI/fins. + */ + protected Transformation finRotation = Transformation.rotate_x(2 * Math.PI / fins); + + + /** + * 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 TubeFinSet() { + super(RocketComponent.Position.BOTTOM); + length = 0.10; + } + + public void setLength(double length) { + if (MathUtil.equals(this.length, length)) + return; + this.length = length; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + public boolean isOuterRadiusAutomatic() { + return autoRadius; + } + + /** + * Return the outer radius of the body tube. + * + * @return the outside radius of the tube + */ + public double getOuterRadius() { + if (autoRadius) { + // Return auto radius from front or rear + double r = -1; + RocketComponent c = this.getParent(); + if (c != null) { + if (c instanceof SymmetricComponent) { + r = ((SymmetricComponent) c).getAftRadius(); + } + } + if (r < 0) { + r = DEFAULT_RADIUS; + } else { + // for 5,6, and 8 fins, adjust the diameter to provide touching fins. + switch (fins) { + case 5: + r *= 1.43; // sin(36) / (1- sin(36), 36 = 360/5/2 + break; + case 7: + r *= 0.77; // sin(25.7) / (1- sin(25.7) + break; + case 8: + r *= 0.62; // sin(22.5) / (1- sin(22.5) + break; + } + } + return r; + } + return outerRadius; + } + + /** + * Set the outer radius of the body tube. If the radius is less than the wall thickness, + * the wall thickness is decreased accordingly of the value of the radius. + * This method sets the automatic radius off. + * + * @param radius the outside radius in standard units + */ + public void setOuterRadius(double radius) { + if ((this.outerRadius == radius) && (autoRadius == false)) + return; + + this.autoRadius = false; + this.outerRadius = Math.max(radius, 0); + + if (this.thickness > this.outerRadius) + this.thickness = this.outerRadius; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + clearPreset(); + } + + /** + * Sets whether the radius is selected automatically or not. + */ + public void setOuterRadiusAutomatic(boolean auto) { + if (autoRadius == auto) + return; + + autoRadius = auto; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + clearPreset(); + } + + public double getInnerRadius() { + return Math.max(getOuterRadius() - thickness, 0); + } + + public void setInnerRadius(double r) { + setThickness(getOuterRadius() - r); + } + + /** + * Return the component wall thickness. + */ + public double getThickness() { + return Math.min(thickness, getOuterRadius()); + } + + + /** + * Set the component wall thickness. Values greater than the maximum radius are not + * allowed, and will result in setting the thickness to the maximum radius. + */ + public void setThickness(double thickness) { + if ((this.thickness == thickness)) + return; + this.thickness = MathUtil.clamp(thickness, 0, getOuterRadius()); + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + clearPreset(); + } + + + /** + * 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); + } + + /** + * Gets the base rotation amount of the first fin. + * @return The base rotation amount. + */ + public double getBaseRotation() { + return rotation; + } + + public double getFinRotation() { + return 2 * Math.PI / fins; + } + + /** + * 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 Transformation getFinRotationTransformation() { + return finRotation; + } + + @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); + } + + @Override + public double getComponentVolume() { + double or = getOuterRadius(); + double ir = getInnerRadius(); + double volume = or * or - ir * ir; + volume *= Math.PI; + volume *= length; + volume *= fins; + return volume; + } + + @Override + public String getComponentName() { + //// Tube Fin Set + return trans.get("TubeFinSet.TubeFinSet"); + } + + @Override + public Coordinate getComponentCG() { + double mass = getComponentMass(); // safe + double halflength = length / 2; + + if (fins == 1) { + return baseRotation.transform(new Coordinate(halflength, getOuterRadius() + getBodyRadius(), 0, mass)); + } else { + return baseRotation.transform(new Coordinate(halflength, 0, 0, mass)); + } + + } + + @Override + public double getLongitudinalUnitInertia() { + // Logitudinal Unit Inertia for a single tube fin. + // 1/12 * (3 * (r1^2 + r2^2) + h^2) + final double inertia = (3 * (MathUtil.pow2(getOuterRadius()) + MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getLength())) / 12; + if (fins == 1) { + return inertia; + } + + // translate each to the center of mass. + final double hypot = getOuterRadius() + getBodyRadius(); + final double finrotation = 2 * Math.PI / fins; + double angularoffset = 0.0; + double totalInertia = 0.0; + for (int i = 0; i < fins; i++) { + double offset = hypot * Math.cos(angularoffset); + totalInertia += inertia + MathUtil.pow2(offset); + angularoffset += finrotation; + } + return totalInertia; + } + + @Override + public double getRotationalUnitInertia() { + // The rotational inertia of a single fin about its center. + // 1/2 * (r1^2 + r2^2) + double icentermass = (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getOuterRadius())) / 2; + if (fins == 1) { + return icentermass; + } else { + // Use parallel axis rule and multiply by number of fins. + return fins * (icentermass + MathUtil.pow2(getOuterRadius()) + getBodyRadius()); + } + } + + @Override + public boolean allowsChildren() { + return false; + } + + @Override + public Type getPresetType() { + return ComponentPreset.Type.BODY_TUBE; + } + + @Override + public boolean isCompatible(Class type) { + // TODO Auto-generated method stub + return false; + } + + @Override + public Collection getComponentBounds() { + List bounds = new ArrayList(); + double r = getBodyRadius(); + + addBound(bounds, 0, 2 * (r + outerRadius)); + addBound(bounds, length, 2 * (r + outerRadius)); + + return bounds; + } + + /** + * 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; + } + +} diff --git a/core/src/net/sf/openrocket/startup/Preferences.java b/core/src/net/sf/openrocket/startup/Preferences.java index b134e8380..a759d9d48 100644 --- a/core/src/net/sf/openrocket/startup/Preferences.java +++ b/core/src/net/sf/openrocket/startup/Preferences.java @@ -20,6 +20,7 @@ import net.sf.openrocket.rocketcomponent.LaunchLug; import net.sf.openrocket.rocketcomponent.MassObject; import net.sf.openrocket.rocketcomponent.RecoveryDevice; import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.TubeFinSet; import net.sf.openrocket.simulation.RK4SimulationStepper; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.BuildProperties; @@ -743,6 +744,7 @@ public abstract class Preferences implements ChangeSource { private static final HashMap, String> DEFAULT_COLORS = new HashMap, String>(); static { DEFAULT_COLORS.put(BodyComponent.class, "0,0,240"); + DEFAULT_COLORS.put(TubeFinSet.class, "0,0,200"); DEFAULT_COLORS.put(FinSet.class, "0,0,200"); DEFAULT_COLORS.put(LaunchLug.class, "0,0,180"); DEFAULT_COLORS.put(InternalComponent.class, "170,0,100"); diff --git a/swing/resources/datafiles/examples/Tube Fin.ork b/swing/resources/datafiles/examples/Tube Fin.ork new file mode 100644 index 000000000..211b210b3 Binary files /dev/null and b/swing/resources/datafiles/examples/Tube Fin.ork differ diff --git a/swing/src/net/sf/openrocket/gui/configdialog/TubeFinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/TubeFinSetConfig.java new file mode 100644 index 000000000..bf12fb16f --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/configdialog/TubeFinSetConfig.java @@ -0,0 +1,170 @@ +package net.sf.openrocket.gui.configdialog; + + +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +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.IntegerModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +public class TubeFinSetConfig extends RocketComponentConfig { + + private MotorConfig motorConfigPane = null; + private static final Translator trans = Application.getTranslator(); + + public TubeFinSetConfig(OpenRocketDocument d, RocketComponent c) { + super(d, c); + + JPanel primary = new JPanel(new MigLayout("fill")); + + + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); + + //// Number of fins + panel.add(new JLabel(trans.get("TubeFinSetCfg.lbl.Nbroffins"))); + + IntegerModel im = new IntegerModel(component, "FinCount", 1, 8); + + JSpinner spin = new JSpinner(im.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx, wrap"); + + //// Length: + panel.add(new JLabel(trans.get("TubeFinSetCfg.lbl.Length"))); + + DoubleModel m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.1)), "w 100lp, wrap para"); + + + //// Outer diameter: + panel.add(new JLabel(trans.get("TubeFinSetCfg.lbl.Outerdiam"))); + + DoubleModel od = new DoubleModel(component, "OuterRadius", 2, UnitGroup.UNITS_LENGTH, 0); + // Diameter = 2*Radius + + spin = new JSpinner(od.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(od), "growx"); + panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap rel"); + + JCheckBox check = new JCheckBox(od.getAutomaticAction()); + //// Automatic + check.setText(trans.get("TubeFinSetCfg.checkbox.Automatic")); + panel.add(check, "skip, span 2, wrap"); + + //// Inner diameter: + panel.add(new JLabel(trans.get("TubeFinSetCfg.lbl.Innerdiam"))); + + // Diameter = 2*Radius + m = new DoubleModel(component, "InnerRadius", 2, UnitGroup.UNITS_LENGTH, 0); + + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)), "w 100lp, wrap rel"); + + + //// Wall thickness + //// Thickness: + panel.add(new JLabel(trans.get("TubeFinSetCfg.lbl.Thickness"))); + + m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap 20lp"); + + + //// Base rotation + //// Fin rotation: + JLabel label = new JLabel(trans.get("TubeFinSetCfg.lbl.Finrotation")); + //// The angle of the first fin in the fin set. + label.setToolTipText(trans.get("TubeFinSetCfg.lbl.ttip.Finrotation")); + panel.add(label); + + m = new DoubleModel(component, "BaseRotation", UnitGroup.UNITS_ANGLE); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); + + primary.add(panel, "grow, gapright 20lp"); + panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); + + //// Position relative to: + panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.Posrelativeto"))); + + JComboBox combo = new JComboBox( + new EnumModel(component, "RelativePosition", + new RocketComponent.Position[] { + RocketComponent.Position.TOP, + RocketComponent.Position.MIDDLE, + RocketComponent.Position.BOTTOM, + RocketComponent.Position.ABSOLUTE + })); + panel.add(combo, "spanx, growx, wrap"); + + //// plus + panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.plus")), "right"); + + m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel( + new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), + new DoubleModel(component.getParent(), "Length"))), + "w 100lp, wrap para"); + + + + //// Material + panel.add(materialPanel(Material.Type.BULK), "span, wrap"); + + primary.add(panel, "grow"); + + //// General and General properties + tabbedPane.insertTab(trans.get("LaunchLugCfg.tab.General"), null, primary, + trans.get("LaunchLugCfg.tab.Generalprop"), 0); + tabbedPane.setSelectedIndex(0); + } + + @Override + public void updateFields() { + super.updateFields(); + } + +} diff --git a/swing/src/net/sf/openrocket/gui/figure3d/geometry/ComponentRenderer.java b/swing/src/net/sf/openrocket/gui/figure3d/geometry/ComponentRenderer.java index 8b244babf..055cfc7a3 100644 --- a/swing/src/net/sf/openrocket/gui/figure3d/geometry/ComponentRenderer.java +++ b/swing/src/net/sf/openrocket/gui/figure3d/geometry/ComponentRenderer.java @@ -17,6 +17,7 @@ import net.sf.openrocket.rocketcomponent.RingComponent; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.Transition; import net.sf.openrocket.rocketcomponent.Transition.Shape; +import net.sf.openrocket.rocketcomponent.TubeFinSet; import net.sf.openrocket.util.Coordinate; import org.slf4j.Logger; @@ -28,28 +29,28 @@ import org.slf4j.LoggerFactory; public class ComponentRenderer { @SuppressWarnings("unused") private static final Logger log = LoggerFactory.getLogger(ComponentRenderer.class); - + private int LOD = 80; - + GLU glu; GLUquadric q; FinRenderer fr = new FinRenderer(); - + public ComponentRenderer() { - + } - + public void init(GLAutoDrawable drawable) { glu = new GLU(); q = glu.gluNewQuadric(); glu.gluQuadricTexture(q, true); } - - + + public void updateFigure(GLAutoDrawable drawable) { - + } - + public Geometry getGeometry(final RocketComponent c, final Surface which) { return new Geometry() { @Override @@ -64,7 +65,7 @@ public class ComponentRenderer { } }; } - + public Geometry getGeometry(final Motor motor, Surface which) { return new Geometry() { @Override @@ -73,20 +74,20 @@ public class ComponentRenderer { } }; } - + protected void renderGeometry(GL2 gl, RocketComponent c, Surface which) { if (glu == null) throw new IllegalStateException(this + " Not Initialized"); - + glu.gluQuadricNormals(q, GLU.GLU_SMOOTH); - + Coordinate[] oo = c.toAbsolute(new Coordinate(0, 0, 0)); - + for (Coordinate o : oo) { gl.glPushMatrix(); - + gl.glTranslated(o.x, o.y, o.z); - + if (c instanceof BodyTube) { renderTube(gl, (BodyTube) c, which); } else if (c instanceof LaunchLug) { @@ -102,14 +103,16 @@ public class ComponentRenderer { } else if (c instanceof FinSet) { if (which == Surface.OUTSIDE) fr.renderFinSet(gl, (FinSet) c); + } else if (c instanceof TubeFinSet) { + renderTubeFins( gl, (TubeFinSet) c, which); } else { renderOther(gl, c); } gl.glPopMatrix(); } - + } - + private void renderOther(GL2 gl, RocketComponent c) { gl.glBegin(GL.GL_LINES); for (Coordinate cc : c.getComponentBounds()) { @@ -120,9 +123,9 @@ public class ComponentRenderer { } gl.glEnd(); } - + private void renderTransition(GL2 gl, Transition t, Surface which) { - + if (which == Surface.OUTSIDE || which == Surface.INSIDE) { gl.glPushMatrix(); gl.glRotated(90, 0, 1.0, 0); @@ -135,7 +138,7 @@ public class ComponentRenderer { } gl.glPopMatrix(); } - + if (which == Surface.EDGES || which == Surface.INSIDE) { //Render aft edge gl.glPushMatrix(); @@ -148,7 +151,7 @@ public class ComponentRenderer { glu.gluDisk(q, Math.max(0, t.getAftRadius() - t.getThickness()), t.getAftRadius(), LOD, 2); } gl.glPopMatrix(); - + // Render AFT shoulder if (t.getAftShoulderLength() > 0) { gl.glPushMatrix(); @@ -161,7 +164,7 @@ public class ComponentRenderer { gl.glRotated(90, 0, 1.0, 0); glu.gluDisk(q, t.getAftShoulderRadius(), t.getAftRadius(), LOD, 2); gl.glPopMatrix(); - + } else { renderTube(gl, Surface.INSIDE, t.getAftShoulderRadius(), iR, t.getAftShoulderLength()); gl.glPushMatrix(); @@ -171,7 +174,7 @@ public class ComponentRenderer { } gl.glPopMatrix(); } - + //Render Fore edge gl.glPushMatrix(); gl.glRotated(180, 0, 1.0, 0); @@ -183,7 +186,7 @@ public class ComponentRenderer { glu.gluDisk(q, Math.max(0, t.getForeRadius() - t.getThickness()), t.getForeRadius(), LOD, 2); } gl.glPopMatrix(); - + // Render Fore shoulder if (t.getForeShoulderLength() > 0) { gl.glPushMatrix(); @@ -197,7 +200,7 @@ public class ComponentRenderer { gl.glRotated(90, 0, 1.0, 0); glu.gluDisk(q, t.getForeShoulderRadius(), t.getForeRadius(), LOD, 2); gl.glPopMatrix(); - + } else { renderTube(gl, Surface.INSIDE, t.getForeShoulderRadius(), iR, t.getForeShoulderLength()); gl.glPushMatrix(); @@ -207,28 +210,28 @@ public class ComponentRenderer { } gl.glPopMatrix(); } - + } - + } - + private void renderTube(final GL2 gl, final Surface which, final double oR, final double iR, final double len) { gl.glPushMatrix(); //outside gl.glRotated(90, 0, 1.0, 0); if (which == Surface.OUTSIDE) glu.gluCylinder(q, oR, oR, len, LOD, 1); - + //edges gl.glRotated(180, 0, 1.0, 0); if (which == Surface.EDGES) glu.gluDisk(q, iR, oR, LOD, 2); - + gl.glRotated(180, 0, 1.0, 0); gl.glTranslated(0, 0, len); if (which == Surface.EDGES) glu.gluDisk(q, iR, oR, LOD, 2); - + //inside if (which == Surface.INSIDE) { glu.gluQuadricOrientation(q, GLU.GLU_INSIDE); @@ -237,59 +240,74 @@ public class ComponentRenderer { } gl.glPopMatrix(); } - + private void renderTube(GL2 gl, BodyTube t, Surface which) { renderTube(gl, which, t.getOuterRadius(), t.getInnerRadius(), t.getLength()); } - + private void renderRing(GL2 gl, RingComponent r) { - + gl.glRotated(90, 0, 1.0, 0); glu.gluCylinder(q, r.getOuterRadius(), r.getOuterRadius(), r.getLength(), LOD, 1); - + gl.glRotated(180, 0, 1.0, 0); glu.gluDisk(q, r.getInnerRadius(), r.getOuterRadius(), LOD, 2); - + gl.glRotated(180, 0, 1.0, 0); gl.glTranslated(0, 0, r.getLength()); glu.gluDisk(q, r.getInnerRadius(), r.getOuterRadius(), LOD, 2); - + glu.gluQuadricOrientation(q, GLU.GLU_INSIDE); glu.gluCylinder(q, r.getInnerRadius(), r.getInnerRadius(), -r.getLength(), LOD, 1); glu.gluQuadricOrientation(q, GLU.GLU_OUTSIDE); - + } - + private void renderLug(GL2 gl, LaunchLug t, Surface which) { renderTube(gl, which, t.getOuterRadius(), t.getInnerRadius(), t.getLength()); } - + + private void renderTubeFins(GL2 gl, TubeFinSet fs, Surface which) { + gl.glPushMatrix(); + gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); + System.out.println(fs.getBaseRotation()); + gl.glRotated(fs.getBaseRotation() * (180.0 / Math.PI), 1, 0, 0); + for( int i = 0; i< fs.getFinCount(); i++ ) { + gl.glPushMatrix(); + gl.glTranslated(0, fs.getOuterRadius() + fs.getBodyRadius(), 0); + renderTube(gl, which, fs.getOuterRadius(), fs.getInnerRadius(), fs.getLength()); + gl.glPopMatrix(); + gl.glRotated(360.0 / fs.getFinCount(), 1, 0, 0); + } + gl.glPopMatrix(); + } + private void renderMassObject(GL2 gl, MassObject o) { gl.glRotated(90, 0, 1.0, 0); - + MassObjectRenderer.drawMassObject(gl, o, LOD / 2, LOD / 2); } - + private void renderMotor(final GL2 gl, Motor motor) { double l = motor.getLength(); double r = motor.getDiameter() / 2; - + gl.glPushMatrix(); - + gl.glRotated(90, 0, 1.0, 0); - + gl.glMatrixMode(GL.GL_TEXTURE); gl.glPushMatrix(); gl.glTranslated(0, .125, 0); gl.glScaled(1, .75, 0); - + glu.gluCylinder(q, r, r, l, LOD, 1); - + gl.glPopMatrix(); gl.glMatrixMode(GLMatrixFunc.GL_MODELVIEW); - + { final double da = (2.0f * Math.PI) / LOD; final double dt = 1.0 / LOD; @@ -300,14 +318,14 @@ public class ComponentRenderer { gl.glVertex3d(r * Math.cos(da * i), r * Math.sin(da * i), 0); gl.glTexCoord2d(i * dt, 0); gl.glVertex3d(0, 0, 0); - + } gl.glEnd(); } - + gl.glTranslated(0, 0, l); gl.glRotated(180, 0, 1.0, 0); - + { final double da = (2.0f * Math.PI) / LOD; final double dt = 1.0 / LOD; @@ -321,7 +339,7 @@ public class ComponentRenderer { } gl.glEnd(); gl.glBegin(GL.GL_TRIANGLE_STRIP); - + for (int i = 0; i < LOD + 1; i++) { gl.glNormal3d(-Math.cos(da * i), -Math.sin(da * i), -1); gl.glTexCoord2d(i * dt, .9); diff --git a/swing/src/net/sf/openrocket/gui/main/ComponentAddButtons.java b/swing/src/net/sf/openrocket/gui/main/ComponentAddButtons.java index 207f397d4..447b7e4c8 100644 --- a/swing/src/net/sf/openrocket/gui/main/ComponentAddButtons.java +++ b/swing/src/net/sf/openrocket/gui/main/ComponentAddButtons.java @@ -25,9 +25,6 @@ import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import net.miginfocom.swing.MigLayout; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.gui.components.StyledLabel; @@ -54,12 +51,16 @@ import net.sf.openrocket.rocketcomponent.Streamer; import net.sf.openrocket.rocketcomponent.Transition; import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; import net.sf.openrocket.rocketcomponent.TubeCoupler; +import net.sf.openrocket.rocketcomponent.TubeFinSet; import net.sf.openrocket.startup.Application; import net.sf.openrocket.startup.Preferences; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.Pair; import net.sf.openrocket.util.Reflection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * A component that contains addition buttons to add different types of rocket components * to a rocket. It enables and disables buttons according to the current selection of a @@ -122,6 +123,8 @@ public class ComponentAddButtons extends JPanel implements Scrollable { new FinButton(EllipticalFinSet.class, trans.get("compaddbuttons.Elliptical")), //// Freeform new FinButton(FreeformFinSet.class, trans.get("compaddbuttons.Freeform")), + //// Freeform + new FinButton(TubeFinSet.class, trans.get("compaddbuttons.Tubefin")), //// Launch lug new FinButton(LaunchLug.class, trans.get("compaddbuttons.Launchlug"))); diff --git a/swing/src/net/sf/openrocket/gui/main/ComponentIcons.java b/swing/src/net/sf/openrocket/gui/main/ComponentIcons.java index a305a1140..168b61133 100644 --- a/swing/src/net/sf/openrocket/gui/main/ComponentIcons.java +++ b/swing/src/net/sf/openrocket/gui/main/ComponentIcons.java @@ -27,6 +27,7 @@ import net.sf.openrocket.rocketcomponent.Streamer; import net.sf.openrocket.rocketcomponent.Transition; import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; import net.sf.openrocket.rocketcomponent.TubeCoupler; +import net.sf.openrocket.rocketcomponent.TubeFinSet; import net.sf.openrocket.startup.Application; public class ComponentIcons { @@ -45,31 +46,25 @@ public class ComponentIcons { load("nosecone", trans.get("ComponentIcons.Nosecone"), NoseCone.class); // // Body tube load("bodytube", trans.get("ComponentIcons.Bodytube"), BodyTube.class); - // // Transition - load("transition", trans.get("ComponentIcons.Transition"), - Transition.class); - // // Trapezoidal fin set - load("trapezoidfin", trans.get("ComponentIcons.Trapezoidalfinset"), - TrapezoidFinSet.class); - // // Elliptical fin set - load("ellipticalfin", trans.get("ComponentIcons.Ellipticalfinset"), - EllipticalFinSet.class); - // // Freeform fin set - load("freeformfin", trans.get("ComponentIcons.Freeformfinset"), - FreeformFinSet.class); - // // Launch lug - load("launchlug", trans.get("ComponentIcons.Launchlug"), - LaunchLug.class); - // // Inner tube - load("innertube", trans.get("ComponentIcons.Innertube"), - InnerTube.class); - // // Tube coupler - load("tubecoupler", trans.get("ComponentIcons.Tubecoupler"), - TubeCoupler.class); - // // Centering ring - load("centeringring", trans.get("ComponentIcons.Centeringring"), - CenteringRing.class); - // // Bulk head + //// Transition + load("transition", trans.get("ComponentIcons.Transition"), Transition.class); + //// Trapezoidal fin set + load("trapezoidfin", trans.get("ComponentIcons.Trapezoidalfinset"), TrapezoidFinSet.class); + //// Elliptical fin set + load("ellipticalfin", trans.get("ComponentIcons.Ellipticalfinset"), EllipticalFinSet.class); + //// Freeform fin set + load("freeformfin", trans.get("ComponentIcons.Freeformfinset"), FreeformFinSet.class); + //// Tube fin set + load("tubefin", trans.get("ComponentIcons.Tubefinset"), TubeFinSet.class); + //// Launch lug + load("launchlug", trans.get("ComponentIcons.Launchlug"), LaunchLug.class); + //// Inner tube + load("innertube", trans.get("ComponentIcons.Innertube"), InnerTube.class); + //// Tube coupler + load("tubecoupler", trans.get("ComponentIcons.Tubecoupler"), TubeCoupler.class); + //// Centering ring + load("centeringring", trans.get("ComponentIcons.Centeringring"), CenteringRing.class); + //// Bulk head load("bulkhead", trans.get("ComponentIcons.Bulkhead"), Bulkhead.class); // // Engine block load("engineblock", trans.get("ComponentIcons.Engineblock"), diff --git a/swing/src/net/sf/openrocket/gui/rocketfigure/TubeFinSetShapes.java b/swing/src/net/sf/openrocket/gui/rocketfigure/TubeFinSetShapes.java new file mode 100644 index 000000000..efe413731 --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/rocketfigure/TubeFinSetShapes.java @@ -0,0 +1,70 @@ +package net.sf.openrocket.gui.rocketfigure; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.Transformation; + +import java.awt.Shape; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Rectangle2D; + + +public class TubeFinSetShapes extends RocketComponentShapes { + + public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, + Transformation transformation) { + net.sf.openrocket.rocketcomponent.TubeFinSet finset = (net.sf.openrocket.rocketcomponent.TubeFinSet)component; + + int fins = finset.getFinCount(); + double length = finset.getLength(); + double outerradius = finset.getOuterRadius(); + double bodyradius = finset.getBodyRadius(); + + Coordinate[] start = finset.toAbsolute(new Coordinate(0,0,0)); + + Transformation baseRotation = finset.getBaseRotationTransformation(); + Transformation finRotation = finset.getFinRotationTransformation(); + + // Translate & rotate the coordinates + for (int i=0; i