diff --git a/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java b/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java index a47262b0e..6d488852e 100644 --- a/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java +++ b/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java @@ -473,7 +473,7 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { FinSet f = (FinSet) c; double mac = ((FinSetCalc) calcMap.get(c)).getMACLength(); double cd = componentCf * (1 + 2 * f.getThickness() / mac) * - 2 * f.getFinCount() * f.getFinArea(); + 2 * f.getFinCount() * f.getPlanformArea(); finFriction += cd; if (map != null) { @@ -757,7 +757,7 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { for (RocketComponent c : configuration.getActiveComponents()) { if (c instanceof FinSet) { FinSet f = (FinSet) c; - mul += 0.6 * Math.min(f.getFinCount(), 4) * f.getFinArea() * + mul += 0.6 * Math.min(f.getFinCount(), 4) * f.getPlanformArea() * MathUtil.pow3(Math.abs(f.toAbsolute(new Coordinate( ((FinSetCalc) calcMap.get(f)).getMidchordPos()))[0].x - cgx)) / diff --git a/core/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java b/core/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java index 36b065ac6..e56f26df6 100644 --- a/core/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java +++ b/core/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java @@ -62,16 +62,19 @@ public class FinSetCalc extends RocketComponentCalc { public FinSetCalc(FinSet component) { super(component); - thickness = component.getThickness(); - bodyRadius = component.getBodyRadius(); - finCount = component.getFinCount(); - baseRotation = component.getBaseRotation(); - cantAngle = component.getCantAngle(); - span = component.getSpan(); - finArea = component.getFinArea(); - crossSection = component.getCrossSection(); + FinSet fin = (FinSet) component; + + thickness = fin.getThickness(); + bodyRadius = fin.getFinFront().y; + finCount = fin.getFinCount(); - calculateFinGeometry(component); + baseRotation = fin.getBaseRotation(); + cantAngle = fin.getCantAngle(); + span = fin.getSpan(); + finArea = fin.getPlanformArea(); + crossSection = fin.getCrossSection(); + + calculateFinGeometry(fin); calculatePoly(); calculateInterferenceFinCount(component); } @@ -246,7 +249,7 @@ public class FinSetCalc extends RocketComponentCalc { protected void calculateFinGeometry(FinSet component) { span = component.getSpan(); - finArea = component.getFinArea(); + finArea = component.getPlanformArea(); ar = 2 * pow2(span) / finArea; Coordinate[] points = component.getFinPoints(); @@ -339,7 +342,7 @@ public class FinSetCalc extends RocketComponentCalc { cosGammaLead = 0; rollSum = 0; double area = 0; - double radius = component.getBodyRadius(); + double radius = component.getFinFront().y; final double dy = span / (DIVISIONS - 1); for (int i = 0; i < DIVISIONS; i++) { diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/FinSetPointHandler.java b/core/src/net/sf/openrocket/file/openrocket/importt/FinSetPointHandler.java index 9d9941270..50dd7f10d 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/FinSetPointHandler.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/FinSetPointHandler.java @@ -3,6 +3,8 @@ package net.sf.openrocket.file.openrocket.importt; import java.util.ArrayList; import java.util.HashMap; +import org.xml.sax.SAXException; + import net.sf.openrocket.aerodynamics.Warning; import net.sf.openrocket.aerodynamics.WarningSet; import net.sf.openrocket.file.DocumentLoadingContext; @@ -10,11 +12,8 @@ import net.sf.openrocket.file.simplesax.AbstractElementHandler; import net.sf.openrocket.file.simplesax.ElementHandler; import net.sf.openrocket.file.simplesax.PlainTextHandler; import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.IllegalFinPointException; import net.sf.openrocket.util.Coordinate; -import org.xml.sax.SAXException; - /** * A handler that reads the specifications within the freeformfinset's * elements. @@ -62,10 +61,7 @@ class FinSetPointHandler extends AbstractElementHandler { @Override public void endHandler(String element, HashMap attributes, String content, WarningSet warnings) { - try { - finset.setPoints(coordinates.toArray(new Coordinate[0])); - } catch (IllegalFinPointException e) { - warnings.add(Warning.fromString("Freeform fin set point definitions illegal, ignoring.")); - } + finset.setPoints(coordinates.toArray(new Coordinate[0])); + } } \ No newline at end of file diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/FinTabPositionSetter.java b/core/src/net/sf/openrocket/file/openrocket/importt/FinTabPositionSetter.java index f6e046d19..b0e3fda77 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/FinTabPositionSetter.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/FinTabPositionSetter.java @@ -5,13 +5,13 @@ import java.util.HashMap; import net.sf.openrocket.aerodynamics.WarningSet; import net.sf.openrocket.rocketcomponent.FinSet; import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.FinSet.TabRelativePosition; +import net.sf.openrocket.rocketcomponent.position.*; import net.sf.openrocket.util.Reflection; class FinTabPositionSetter extends DoubleSetter { public FinTabPositionSetter() { - super(Reflection.findMethod(FinSet.class, "setTabShift", double.class)); + super(Reflection.findMethod(FinSet.class, "setTabOffset", double.class)); } @Override @@ -23,23 +23,30 @@ class FinTabPositionSetter extends DoubleSetter { } String relative = attributes.get("relativeto"); - FinSet.TabRelativePosition position = - (TabRelativePosition) DocumentConfig.findEnum(relative, - FinSet.TabRelativePosition.class); - if (position != null) { - - ((FinSet) c).setTabRelativePosition(position); - + if (relative == null) { + warnings.add("Required attribute 'relativeto' not found for fin tab position."); } else { - if (relative == null) { - warnings.add("Required attribute 'relativeto' not found for fin tab position."); - } else { - warnings.add("Illegal attribute value '" + relative + "' encountered."); + // translate from old enum names to current enum names + if( relative.contains("front")){ + relative = "top"; + }else if( relative.contains("center")){ + relative = "middle"; + }else if( relative.contains("end")){ + relative = "bottom"; } + + AxialMethod position = (AxialMethod) DocumentConfig.findEnum(relative, AxialMethod.class); + + if( null == position ){ + warnings.add("Illegal attribute value '" + relative + "' encountered."); + }else{ + ((FinSet) c).setTabOffsetMethod(position); + super.set(c, s, attributes, warnings); + } + } - super.set(c, s, attributes, warnings); } diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/FinSetSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/FinSetSaver.java index f968215ec..71d55933c 100644 --- a/core/src/net/sf/openrocket/file/openrocket/savers/FinSetSaver.java +++ b/core/src/net/sf/openrocket/file/openrocket/savers/FinSetSaver.java @@ -29,8 +29,8 @@ public class FinSetSaver extends ExternalComponentSaver { elements.add("" + fins.getTabHeight() + ""); elements.add("" + fins.getTabLength() + ""); elements.add("" + - fins.getTabShift() + ""); + fins.getTabOffsetMethod().name().toLowerCase(Locale.ENGLISH) + "\">" + + fins.getTabFrontEdge() + ""); } diff --git a/core/src/net/sf/openrocket/file/rocksim/export/FinSetDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/FinSetDTO.java index 0879c3b78..2f81c8ae7 100644 --- a/core/src/net/sf/openrocket/file/rocksim/export/FinSetDTO.java +++ b/core/src/net/sf/openrocket/file/rocksim/export/FinSetDTO.java @@ -64,7 +64,7 @@ public class FinSetDTO extends BasePartDTO { setCantAngle(theORFinSet.getCantAngle()); setTabDepth(theORFinSet.getTabHeight() * RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH); setTabLength(theORFinSet.getTabLength() * RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH); - setTabOffset(theORFinSet.getTabShift() * RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH); + setTabOffset(theORFinSet.getTabOffset() * RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH); setThickness(theORFinSet.getThickness() * RocksimCommonConstants.ROCKSIM_TO_OPENROCKET_LENGTH); setRadialAngle(theORFinSet.getBaseRotation()); diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/FinSetHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/FinSetHandler.java index 837584bdc..f2f9d7f64 100644 --- a/core/src/net/sf/openrocket/file/rocksim/importt/FinSetHandler.java +++ b/core/src/net/sf/openrocket/file/rocksim/importt/FinSetHandler.java @@ -8,6 +8,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import org.xml.sax.SAXException; + import net.sf.openrocket.aerodynamics.WarningSet; import net.sf.openrocket.file.DocumentLoadingContext; import net.sf.openrocket.file.rocksim.RocksimCommonConstants; @@ -21,14 +23,11 @@ import net.sf.openrocket.rocketcomponent.EllipticalFinSet; import net.sf.openrocket.rocketcomponent.ExternalComponent; import net.sf.openrocket.rocketcomponent.FinSet; import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.IllegalFinPointException; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; import net.sf.openrocket.rocketcomponent.position.AxialMethod; import net.sf.openrocket.util.Coordinate; -import org.xml.sax.SAXException; - /** * A SAX handler for Rocksim fin sets. Because the type of fin may not be known first (in Rocksim file format, the fin * shape type is in the middle of the XML structure), and because we're using SAX not DOM, all of the fin @@ -73,7 +72,7 @@ class FinSetHandler extends AbstractElementHandler { /** * The length of the mid-chord (aka height). */ - @SuppressWarnings("unused") // spoiler: field IS actually used; eclipse complains anyway. + @SuppressWarnings("unused") // stored from file, but not used. private double midChordLen = 0.0d; /** @@ -304,11 +303,8 @@ class FinSetHandler extends AbstractElementHandler { else if (shapeCode == 2) { result = new FreeformFinSet(); - try { - ((FreeformFinSet) result).setPoints(toCoordinates(pointList, warnings)); - } catch (IllegalFinPointException e) { - warnings.add("Illegal fin point set. " + e.getMessage() + " Ignoring."); - } + ((FreeformFinSet) result).setPoints(toCoordinates(pointList, warnings)); + } else { return null; @@ -318,10 +314,10 @@ class FinSetHandler extends AbstractElementHandler { result.setFinCount(finCount); result.setFinish(finish); //All TTW tabs in Rocksim are relative to the front of the fin. - result.setTabRelativePosition(FinSet.TabRelativePosition.FRONT); + result.setTabOffsetMethod( AxialMethod.TOP); result.setTabHeight(tabDepth); result.setTabLength(tabLength); - result.setTabShift(taboffset); + result.setTabOffset(taboffset); result.setBaseRotation(radialAngle); result.setCrossSection(convertTipShapeCode(tipShapeCode)); result.setAxialMethod(axialMethod); diff --git a/core/src/net/sf/openrocket/masscalc/MassCalculation.java b/core/src/net/sf/openrocket/masscalc/MassCalculation.java index 43103beca..9ecbacdc8 100644 --- a/core/src/net/sf/openrocket/masscalc/MassCalculation.java +++ b/core/src/net/sf/openrocket/masscalc/MassCalculation.java @@ -243,10 +243,7 @@ public class MassCalculation { * Returns the mass and inertia data for this component and all subcomponents. * The inertia is returned relative to the CG, and the CG is in the coordinates * of the specified component, not global coordinates. - * - * @param calculation - i/o parameter to specifies the calculation parameters, and - * the instance returned with the calculation's tree data. - * + * */ /* package-scope */ MassCalculation calculateAssembly(){ final RocketComponent component = this.root; @@ -346,10 +343,7 @@ public class MassCalculation { * MOI Calculation needs to be a two-step process: * (1) calculate overall Center-of-Mass (CM) first (down inline with data-gathering) * (2) Move MOIs to CM via parallel axis theorem (this method) - * - * @param Center-of-Mass where the MOI should be calculated around. - * @param inertias a list of component MassData instances to condense into a single MOI - * + * * @return freshly calculated Moment-of-Inertia matrix */ /* package-scope */ RigidBody calculateMomentOfInertia() { diff --git a/core/src/net/sf/openrocket/masscalc/MassCalculator.java b/core/src/net/sf/openrocket/masscalc/MassCalculator.java index eeaedb6c5..404c98f30 100644 --- a/core/src/net/sf/openrocket/masscalc/MassCalculator.java +++ b/core/src/net/sf/openrocket/masscalc/MassCalculator.java @@ -3,7 +3,6 @@ package net.sf.openrocket.masscalc; import java.util.HashMap; import java.util.Map; -import net.sf.openrocket.masscalc.MassCalculation.Type; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.rocketcomponent.FlightConfiguration; import net.sf.openrocket.rocketcomponent.RocketComponent; @@ -25,7 +24,7 @@ public class MassCalculator implements Monitorable { // private MassData rocketSpentMassCache; // private MassData propellantMassCache; - private int modId=0; + private int modId = 0; ////////////////// Constructors /////////////////// public MassCalculator() { @@ -40,7 +39,7 @@ public class MassCalculator implements Monitorable { * - includes motors * - for Black Powder & Composite motors, this generally *excludes* propellant * - * @param configuration the rocket configuration to calculate for + * @param config the rocket configuration to calculate for * @return the MassData struct of the motors at burnout */ public static RigidBody calculateStructure( final FlightConfiguration config) { @@ -53,7 +52,7 @@ public class MassCalculator implements Monitorable { * - includes motors * - for Black Powder & Composite motors, this generally *excludes* propellant * - * @param configuration the rocket configuration to calculate for + * @param config the rocket configuration to calculate for * @return the MassData struct of the motors at burnout */ public static RigidBody calculateBurnout( final FlightConfiguration config) { @@ -124,7 +123,6 @@ public class MassCalculator implements Monitorable { * ( or mount-data collides with motor-data ) * * @param configuration the rocket configuration - * @param type the state of the motors (none, launch mass, burnout mass) * @return a map from each rocket component to its corresponding CG. */ @Deprecated @@ -152,10 +150,6 @@ public class MassCalculator implements Monitorable { ////////////////// Mass property calculations /////////////////// - - - - @Override public int getModID() { return this.modId; diff --git a/core/src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java b/core/src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java index 0fa9497c7..59f8ca330 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java +++ b/core/src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java @@ -71,6 +71,7 @@ public class EllipticalFinSet extends FinSet { if (MathUtil.equals(this.length, length)) return; this.length = length; + validateFinTab(); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } diff --git a/core/src/net/sf/openrocket/rocketcomponent/FinSet.java b/core/src/net/sf/openrocket/rocketcomponent/FinSet.java index d5387aebf..d3f124131 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/FinSet.java +++ b/core/src/net/sf/openrocket/rocketcomponent/FinSet.java @@ -1,25 +1,37 @@ package net.sf.openrocket.rocketcomponent; +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.material.Material; + import net.sf.openrocket.rocketcomponent.position.AngleMethod; import net.sf.openrocket.rocketcomponent.position.AxialMethod; import net.sf.openrocket.rocketcomponent.position.AxialPositionable; import net.sf.openrocket.rocketcomponent.position.RadiusMethod; import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.ArrayUtils; import net.sf.openrocket.util.BoundingBox; + +import net.sf.openrocket.rocketcomponent.Transition.Shape; + import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.Transformation; - public abstract class FinSet extends ExternalComponent implements RingInstanceable, AxialPositionable { private static final Translator trans = Application.getTranslator(); + @SuppressWarnings("unused") + private static final Logger log = LoggerFactory.getLogger(FinSet.class); + + /** * Maximum allowed cant of fins. */ @@ -51,44 +63,24 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab } } - 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. */ private int finCount = 1; - + /** * Rotation about the x-axis by 2*PI/fins. */ private Transformation finRotation = Transformation.IDENTITY; - - + /** * Rotation angle of the first fin. Zero corresponds to the positive y-axis. */ private AngleMethod angleMethod = AngleMethod.RELATIVE; private double firstFinOffset = 0; - + private Transformation baseRotation = Transformation.IDENTITY; // initially, rotate by 0 degrees. + /** * Cant angle of fins. */ @@ -104,8 +96,7 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab * Thickness of the fins. */ protected double thickness = 0.003; - - + /** * The cross-section shape of the fins. */ @@ -115,25 +106,27 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab /* * Fin tab properties. */ + private static final double minimumTabArea = 1e-8; private double tabHeight = 0; private double tabLength = 0.05; - private double tabShift = 0; - private TabRelativePosition tabRelativePosition = TabRelativePosition.CENTER; - + // this is always measured from the the root-lead point. + private double tabPosition = 0.0; + private AxialMethod tabOffsetMethod = AxialMethod.MIDDLE; + private double tabOffset = 0.; + /* * Fin fillet properties */ - private Material filletMaterial; private double filletRadius = 0; - private double filletCenterY = 0; - // Cached fin area & CG. Validity of both must be checked using finArea! + // ==== Cached Values ==== // Fin area does not include fin tabs, CG does. - private double finArea = -1; - private double finCGx = -1; - private double finCGy = -1; - + + // planform area of one side of a single fin + private double singlePlanformArea = Double.NaN; + private double totalVolume = Double.NaN; + private Coordinate centerOfMass = Coordinate.NaN; /** * New FinSet with given number of fins and given base rotation angle. @@ -170,13 +163,21 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab if (n > 8) n = 8; finCount = n; + + finRotation = Transformation.rotate_x(2 * Math.PI / finCount); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } public Transformation getFinRotationTransformation() { return finRotation; } - + + @Override + public double getBoundingRadius(){ + return 0.; + } + /** * Gets the base rotation amount of the first fin. * @return The base rotation amount. @@ -219,8 +220,6 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab } return cantRotation; } - - public double getThickness() { return thickness; @@ -244,16 +243,29 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab crossSection = cs; 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)) + /** + * Set the height from the fin's base at the reference point -- i.e. where the tab is located from. If the tab is located via BOTTOM, then the back edge will be + * height deep, and the bottom edge of the tab will be parallel to the stage centerline. If the tab is located via TOP, the the front edge will have corresponding height/depth. + * If the tab is located via MIDDLE, the tab's midpoint is used. + * + * Note this function also does bounds checking, and will not set a tab height that passes through it's parent's midpoint. + * + * @param newHeightRequest how deep the fin tab should project from the fin root, at the reference point + * + */ + public void setTabHeight(final double newHeightRequest) { + if (MathUtil.equals(this.tabHeight, MathUtil.max(newHeightRequest, 0))){ return; - this.tabHeight = height; + } + + this.tabHeight = newHeightRequest; + + validateFinTab(); fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } @@ -266,247 +278,349 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab length = MathUtil.max(length, 0); if (MathUtil.equals(this.tabLength, length)) return; - this.tabLength = length; + tabLength = length; + validateFinTab(); + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + /** + * internally, set the internal + * + * @param newOffset new requested shift of tab -- from + */ + public void setTabOffset( final double newOffset) { + this.tabOffset = newOffset; + this.tabPosition = this.tabOffsetMethod.getAsPosition( newOffset, this.tabLength, this.length); + + validateFinTab(); fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } - - public double getTabShift() { - return tabShift; + public AxialMethod getTabOffsetMethod() { + return tabOffsetMethod; } - - public void setTabShift(double shift) { - this.tabShift = shift; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + + /** + * the tab's positioning method variable does not change the internal representation -- + * it is merely a lens through which other modules may view the tab's position. + */ + public void setTabOffsetMethod(final AxialMethod newPositionMethod) { + this.tabOffsetMethod = newPositionMethod; + this.tabOffset = tabOffsetMethod.getAsOffset( tabPosition, tabLength, length); } - - 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. */ - private 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); - } + public double getTabFrontEdge() { + return tabPosition; } - + + public double getTabOffset(){ + return this.tabOffsetMethod.getAsOffset(tabPosition, tabLength, length); + } + /** * Return the tab trailing edge position *from the front of the fin*. */ private double getTabTrailingEdge() { - switch (this.tabRelativePosition) { - case FRONT: - return tabLength + tabShift; - case CENTER: - return getLength() / 2 + tabLength / 2 + tabShift; + return tabPosition + tabLength; + } + + public void validateFinTab(){ + this.tabPosition = this.tabOffsetMethod.getAsPosition(tabOffset, tabLength, length); + + //check front bounds: + if( tabPosition < 0){ + this.tabPosition = 0; + } + + //check tail bounds: + final double xTabBack = getTabTrailingEdge(); + if( this.length < xTabBack ){ + this.tabLength -= (xTabBack - this.length); + } + + // check tab height + if( null != getParent() ){ + // pulls the parent-body radius at the fin-tab reference point. + final double xLead = this.getTabFrontEdge(); + final double xTrail = this.getTabTrailingEdge(); - case END: - return getLength() + tabShift; + final SymmetricComponent sym = (SymmetricComponent)this.parent; + final double bodyRadius = MathUtil.min(sym.getRadius( xLead), sym.getRadius( xTrail)); - default: - throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition); + // limit the new heights to be no greater than the current body radius. + this.tabHeight = Math.min( this.tabHeight, bodyRadius ); } } - - - - /////////// Calculation methods /////////// - + /////////// 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 a *single* fin exposed to the airflow (i.e. external area) + * N.B. counts only one side of each fin, + * https://en.wikipedia.org/wiki/Wetted_area * - * @return the area of one side of one fin. + * @return returns the one-sided air-exposed area of a single fin */ - public double getFinArea() { - if (finArea < 0) - calculateAreaCG(); - - return finArea; + public double getPlanformArea() { + if( Double.isNaN(singlePlanformArea) ){ + calculateCM(); + } + return this.singlePlanformArea; } - - /** - * 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(); + if(this.centerOfMass.isNaN()){ + calculateCM(); + } + return this.centerOfMass.weight; } - private double getFinMass() { - return getComponentVolume() * material.getDensity(); - } - - private double getFilletMass() { - return getFilletVolume() * filletMaterial.getDensity(); - } - - @Override public double getComponentVolume() { - // this is for the fins alone, fillets are taken care of separately. - return finCount * (getFinArea() + tabHeight * tabLength) * thickness * - crossSection.getRelativeVolume(); + if(Double.isNaN(this.totalVolume)){ + calculateCM(); + } + + return totalVolume; } - - + /** + * Return the center-of-mass of a single fin. The X-coordinate is relative to + * the root chord leading edge and the Y-coordinate to the fin root chord. + * + * @return the Center-of-Mass coordinate of a single fin. + */ @Override public Coordinate getComponentCG() { - if (finArea < 0) - calculateAreaCG(); - - double mass = getFinMass(); - double filletMass = getFilletMass(); - - if (finCount == 1) { - Transformation rotation = Transformation.rotate_x( getAngleOffset()); - return rotation.transform( - new Coordinate(finCGx, finCGy + getBodyRadius(), 0, (filletMass + mass))); - } else { - return new Coordinate(finCGx, 0, 0, (filletMass + mass)); + if( centerOfMass.isNaN() ){ + calculateCM(); } - } - - private 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; - } - - @Override - public double getBoundingRadius(){ - return 0.; + return centerOfMass; } - 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; + private static Coordinate calculateFilletCrossSection(final double filletRadius, final double bodyRadius){ + final double hypotenuse = filletRadius + bodyRadius; + final double innerArcAngle = Math.asin(filletRadius / hypotenuse); + final double outerArcAngle = Math.acos(filletRadius / hypotenuse); + + final double triangleArea = Math.tan(outerArcAngle) * filletRadius * filletRadius / 2; + double crossSectionArea = (triangleArea + - outerArcAngle * filletRadius * filletRadius / 2 + - innerArcAngle * bodyRadius * bodyRadius / 2); + + // each fin has a fillet on each side + crossSectionArea *= 2; + + // heuristic, relTo the body center + double yCentroid = bodyRadius + filletRadius /5; + + return new Coordinate(0,yCentroid,0,crossSectionArea); + } + + /* + * 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. + */ + protected Coordinate calculateFilletVolumeCentroid() { + Coordinate[] bodyPoints = this.getBodyPoints(); + if (0 == bodyPoints.length) { + return Coordinate.ZERO; + } + + final SymmetricComponent sym = (SymmetricComponent) this.parent; + if (!SymmetricComponent.class.isInstance(this.parent)) { + return Coordinate.ZERO; + } + + Coordinate filletVolumeCentroid = Coordinate.ZERO; + + + Coordinate prev = bodyPoints[0]; + for (int index = 1; index < bodyPoints.length; index++) { + final Coordinate cur = bodyPoints[index]; + + // cross section at mid-segment + final double xAvg = (prev.x + cur.x) / 2; + final double bodyRadius = sym.getRadius(xAvg); + final Coordinate segmentCrossSection = calculateFilletCrossSection(this.filletRadius, bodyRadius).setX(xAvg); + +// final double xCentroid = xAvg; +// final double yCentroid = segmentCrossSection.y; ///< heuristic, not exact + final double segmentLength = Point2D.Double.distance(prev.x, prev.y, cur.x, cur.y); + final double segmentVolume = segmentLength * segmentCrossSection.weight; + + final Coordinate segmentCentroid = segmentCrossSection.setWeight(segmentVolume); + + filletVolumeCentroid = filletVolumeCentroid.add(segmentCentroid); + + prev = cur; + } + + // translate to be relative to the fin-lead-root + filletVolumeCentroid = filletVolumeCentroid.sub(getAxialFront(), 0,0); + + if (finCount == 1) { + Transformation rotation = Transformation.rotate_x( getAngleOffset()); + return rotation.transform(filletVolumeCentroid); + }else{ + return filletVolumeCentroid.setY(0.); + } + } + + /** + * \brief calculate the area-under-the-curve (i.e. the integral) in the form of a centroid + area + * + * @param points define a piece-wise line bounding the area. + * @return x,y,z => centroid of the area; weight => magnitude of the area + */ + protected static Coordinate calculateCurveIntegral( final Coordinate[] points ){ + Coordinate centroidSum = new Coordinate(0); + + if( 0 == points.length ){ + return centroidSum; + } + + Coordinate prev= points[0]; + for( int index = 1; index < points.length; index++){ + Coordinate cur = points[index]; + + final double delta_x = (cur.x - prev.x); + final double y_avg = (cur.y + prev.y)*0.5; - 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; + // calculate marginal area + double area_increment = delta_x*y_avg; + if( MathUtil.equals( 0, area_increment)){ + prev = cur; + // zero area increment: ignore and continue; + continue; } + + // calculate centroid increment + final double common = 1/(3*(cur.y+prev.y)); + final double x_ctr = common*( prev.x*(2*prev.y+cur.y) + cur.x*(2*cur.y+prev.y)); + final double y_ctr = common*( cur.y*prev.y + Math.pow( cur.y, 2) + Math.pow( prev.y, 2)); + + Coordinate centroid_increment = new Coordinate( x_ctr, y_ctr, 0, area_increment); + centroidSum = centroidSum.average( centroid_increment ); + + prev=cur; } - 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; - + return centroidSum; + } + + /** + * calculates the planform area-centroid of a single fin's tab: + */ + private Coordinate calculateTabCentroid(){ + RocketComponent comp = getParent(); + + if( !( comp instanceof SymmetricComponent) || isTabTrivial() ){ + // if null or invalid type: + return Coordinate.ZERO; } + // relto: fin + final double xTabFront_fin = getTabFrontEdge(); + final double xTabTrail_fin = getTabTrailingEdge(); - if ((finArea + tabArea) > 0) { - finCGx /= (finArea + tabArea); - finCGy /= (finArea + tabArea); - } else { - finCGx = (points[0].x + points[points.length - 1].x) / 2; - finCGy = 0; - } + final double xFinFront_body = this.getAxialFront(); + final double xTabFront_body = xFinFront_body + xTabFront_fin; + final double xTabTrail_body = xFinFront_body + xTabTrail_fin; + + // always returns x coordinates relTo fin front: + Coordinate[] upperCurve = getBodyPoints( xTabFront_body, xTabTrail_body ); + // locate relative to fin/body centerline + upperCurve = translatePoints( upperCurve, -xFinFront_body, 0.0); + + Coordinate[] lowerCurve = translateToCenterline( getTabPoints()); + + final Coordinate[] tabPoints = combineCurves( upperCurve, lowerCurve); + + return calculateCurveIntegral( tabPoints ); + } + + private Coordinate[] translateToCenterline( final Coordinate[] fromRoot) { + Coordinate finRoot = this.getFinFront(); + + // locate relative to fin/body centerline + return FinSet.translatePoints( fromRoot, 0.0d, finRoot.y); + } + + /** + * calculates the 2-dimensional area-centroid of a single fin. + * + * Located from the leading end of the fin root. + * + * @return area centroid coordinates (weight is the area) + */ + + /* + * The coordinate contains an x,y coordinate of the centroid, relative to the parent-body-centerline + */ + private Coordinate calculateSinglePlanformCentroid(){ + final Coordinate finFront = getFinFront(); + + final Coordinate[] upperCurve = translatePoints( getFinPoints(), finFront.x, finFront.y ); + final Coordinate[] lowerCurve = getBodyPoints(); + final Coordinate[] totalCurve = combineCurves( upperCurve, lowerCurve); + + Coordinate planformCentroid = calculateCurveIntegral( totalCurve ); + + // return as a position relative to fin-root + return planformCentroid.sub(finFront.x,0,0); + } + + /** + * copies the supplied areas into a third array, such that the first curve is copied forward, and the second is copied in reverse. + * + * The motivation is to use the two sets of forward points to produce a single close curve, suitable for an integration operation + * + * @param c1 forward curve + * @param c2 backward curve + * @return combined curve + */ + private Coordinate[] combineCurves( final Coordinate[] c1, final Coordinate[] c2){ + Coordinate[] combined = new Coordinate[ c1.length + c2.length - 1]; + + // copy the first array to the start of the return array... + System.arraycopy(c1, 0, combined, 0, c1.length); + + Coordinate[] revCurve = reverse( c2); + int writeIndex = c1.length; // start directly after previous array + int writeCount = revCurve.length - 1; // write all-but-first + System.arraycopy(revCurve, 1, combined, writeIndex, writeCount); + + return combined; } + // simply return a reversed copy of the source array + public Coordinate[] reverse( Coordinate[] source){ + Coordinate[] reverse = new Coordinate[ source.length ]; + + int readIndex = 0; + int writeIndex = source.length-1; + while( readIndex < source.length ){ + reverse[writeIndex] = source[readIndex]; + ++readIndex; + --writeIndex; + } + return reverse; + } + /* * Return an approximation of the longitudinal unitary inertia of the fin set. * The process is the following: @@ -522,10 +636,10 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab */ @Override public double getLongitudinalUnitInertia() { - double area = getFinArea(); - if (MathUtil.equals(area, 0)) - return 0; - + if(Double.isNaN(this.singlePlanformArea)){ + calculateCM(); + } + // Approximate fin with a rectangular fin // w2 and h2 are squares of the fin width and height double w = getLength(); @@ -533,11 +647,11 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab double w2, h2; if (MathUtil.equals(w * h, 0)) { - w2 = area; - h2 = area; + w2 = singlePlanformArea; + h2 = singlePlanformArea; } else { - w2 = w * area / h; - h2 = h * area / w; + w2 = w * singlePlanformArea / h; + h2 = h * singlePlanformArea / w; } double inertia = (h2 + 2 * w2) / 24; @@ -545,9 +659,9 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab if (finCount == 1) return inertia; - double radius = getBodyRadius(); + final double rFront = this.getFinFront().y; - return finCount * (inertia + MathUtil.pow2(MathUtil.safeSqrt(h2) + radius)); + return finCount * (inertia + MathUtil.pow2(MathUtil.safeSqrt(h2) + rFront)); } @@ -563,26 +677,23 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab */ @Override public double getRotationalUnitInertia() { - double area = getFinArea(); - if (MathUtil.equals(area, 0)) - return 0; - + if(Double.isNaN(this.singlePlanformArea)){ + calculateCM(); + } + // Approximate fin with a rectangular fin double w = getLength(); double h = getSpan(); - + if (MathUtil.equals(w * h, 0)) { - h = MathUtil.safeSqrt(area); + h = MathUtil.safeSqrt(singlePlanformArea); } else { - h = MathUtil.safeSqrt(h * area / w); + h = MathUtil.safeSqrt(h * singlePlanformArea/ w); } - - if (finCount == 1) - return h * h / 12; - - double radius = getBodyRadius(); - - return finCount * (h * h / 12 + MathUtil.pow2(h / 2 + radius)); + + final double rFront = this.getFinFront().y; + + return finCount * (h * h / 12 + MathUtil.pow2(h / 2 + rFront)); } @@ -606,14 +717,46 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab */ @Override public Collection getComponentBounds() { - return getBoundingBox().toCollection(); + Collection bounds = new ArrayList(8); + + // should simply return this component's bounds in this component's body frame. + + double x_min = Double.MAX_VALUE; + double x_max = Double.MIN_VALUE; + double r_max = 0.0; + + for (Coordinate point : getFinPoints()) { + double hypot = MathUtil.hypot(point.y, point.z); + double x_cur = point.x; + if (x_min > x_cur) { + x_min = x_cur; + } + if (x_max < x_cur) { + x_max = x_cur; + } + if (r_max < hypot) { + r_max = hypot; + } + } + + Coordinate location = this.getLocations()[0]; + x_max += location.x; + + if( parent instanceof SymmetricComponent){ + r_max += ((SymmetricComponent)parent).getRadius(0); + } + + addBoundingBox(bounds, x_min, x_max, r_max); + return bounds; } - + @Override public void componentChanged(ComponentChangeEvent e) { - if (e.isAerodynamicChange()) { - finArea = -1; - cantRotation = null; + if (e.isAerodynamicChange() || e.isMassChange()) { + this.singlePlanformArea = Double.NaN; + this.centerOfMass = Coordinate.NaN; + this.totalVolume = Double.NaN; + this.cantRotation = null; } super.componentChanged(e); } @@ -627,16 +770,7 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab * @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) { - return ((SymmetricComponent) s).getRadius( this.position.x); - } - s = s.getParent(); - } - return 0; + return getFinFront().y; } @Override @@ -653,55 +787,115 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab public boolean isCompatible(Class 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. + * All Z-coordinates must be zero. * * @return List of XY-coordinates. */ public abstract Coordinate[] getFinPoints(); + public boolean isTabTrivial(){ + return ( FinSet.minimumTabArea > (getTabLength()*getTabHeight())); + } + + public boolean isRootStraight( ){ + if( getParent() instanceof Transition){ + if( ((Transition)getParent()).getType() == Transition.Shape.CONICAL ){ + return true; + }else{ + return false; + } + } + + // by default, assume a flat base + return true; + } + + protected static Coordinate[] translatePoints( final Coordinate[] inp, final double x_delta , final double y_delta){ + Coordinate[] returnPoints = new Coordinate[inp.length]; + for( int index=0; index < inp.length; ++index){ + final double new_x = inp[index].x + x_delta; + final double new_y = inp[index].y + y_delta; + returnPoints[index] = new Coordinate(new_x, new_y); + } + return returnPoints; + } + /** - * 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 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 radial distance inwards from the reference point, depending on positioning method: + * if via TOP: tab front edge + * if via MIDDLE: tab middle + * if via BOTTOM: tab trailing edge + * + * The tab coordinates will generally have negative y values. * * @return List of XY-coordinates. */ - public Coordinate[] getFinPointsWithTab() { - Coordinate[] points = getFinPoints(); + public Coordinate[] getTabPoints() { if (MathUtil.equals(getTabHeight(), 0) || - MathUtil.equals(getTabLength(), 0)) - return points; + MathUtil.equals(getTabLength(), 0)){ + return new Coordinate[]{}; + } + + final int pointCount = 4; + Coordinate[] points = new Coordinate[pointCount]; + final Coordinate finFront = this.getFinFront(); - double x1 = getTabFrontEdge(); - double x2 = getTabTrailingEdge(); - double y = -getTabHeight(); + final SymmetricComponent body = (SymmetricComponent)this.getParent(); - boolean add1 = x1 != points[0].x; - boolean add2 = x2 != points[points.length - 1].x; + final double xTabFront = getTabFrontEdge(); + final double xTabTrail = getTabTrailingEdge(); - int n = points.length; - points = ArrayUtils.copyOf(points, points.length + 2 + (add1 ? 1 : 0) + (add2 ? 1 : 0)); + final double xTabReference = finFront.x + getTabOffset(); - 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); + double yTabFront = 0; + double yTabTrail = 0; + double yTabBottom = -tabHeight; + if( null != body ){ + yTabFront = body.getRadius( finFront.x + xTabFront ) - finFront.y; + yTabTrail = body.getRadius( finFront.x + xTabTrail ) - finFront.y; + yTabBottom = body.getRadius( xTabReference ) - tabHeight - finFront.y; + } - return points; + points[0] = new Coordinate(xTabFront, yTabFront); + points[1] = new Coordinate(xTabFront, yTabBottom ); + points[2] = new Coordinate(xTabTrail, yTabBottom ); + points[3] = new Coordinate(xTabTrail, yTabTrail); + + return points; + } + + public Coordinate getFinFront() { + final double xFinFront = this.getAxialFront(); + final SymmetricComponent symmetricParent = (SymmetricComponent)this.getParent(); + if( null == symmetricParent){ + return new Coordinate( 0, 0); + }else{ + final double yFinFront = symmetricParent.getRadius( xFinFront ); + return new Coordinate(xFinFront, yFinFront); + } + } + + + /* + * yes, this may over-count points between the fin and fin tabs, + * but the minor performance hit is not worth the code complexity of dealing with. + */ + public Coordinate[] getFinPointsWithTab() { + final Coordinate[] finPoints = getFinPoints(); + final Coordinate[] tabPoints = getTabPoints(); + + Coordinate[] combinedPoints = Arrays.copyOf(finPoints, finPoints.length + tabPoints.length); + System.arraycopy(tabPoints, 0, combinedPoints, finPoints.length, tabPoints.length); + return combinedPoints; } @Override @@ -725,43 +919,17 @@ public abstract class FinSet extends ExternalComponent implements RingInstanceab } @Override - public double[] getInstanceAngles(){ + public double[] getInstanceAngles(){ final double baseAngle = getAngleOffset(); final double incrAngle = getInstanceAngleIncrement(); double[] result = new double[ getFinCount()]; for( int i=0; i> %s: %d points\n", indent, name, points.length)); + int index =0; + for( Coordinate c : points ){ + buf.append( String.format( indent+" ....[%2d] (%6.4g, %6.4g)\n", index, c.x, c.y)); + index++; + } + return buf.toString(); + } + + @Override + public StringBuilder toDebugDetail(){ + StringBuilder buf = super.toDebugDetail(); + + buf.append( getPointDescr( this.getFinPoints(), "Fin Points", "")); + + if (null != parent) { + buf.append( getPointDescr( this.getBodyPoints(), "Body Points", "")); + } + + if( ! this.isTabTrivial() ) { + buf.append(String.format(" TabLength: %6.4f TabHeight: %6.4f @ %6.4f via: %s\n", tabLength, tabHeight, tabPosition, this.tabOffsetMethod)); + buf.append(getPointDescr(this.getTabPoints(), "Tab Points", "")); + } + return buf; + } + + private void calculateCM(){ + final Coordinate wettedCentroid = calculateSinglePlanformCentroid(); + this.singlePlanformArea = wettedCentroid.weight; + final double wettedVolume = wettedCentroid.weight * thickness * crossSection.getRelativeVolume(); + final double finBulkMass = wettedVolume * material.getDensity(); + final Coordinate wettedCM = wettedCentroid.setWeight(finBulkMass); + + final Coordinate tabCentroid = calculateTabCentroid(); + final double tabVolume = tabCentroid.weight * thickness; + final double tabMass = tabVolume * material.getDensity(); + final Coordinate tabCM = tabCentroid.setWeight(tabMass); + + Coordinate filletCentroid = calculateFilletVolumeCentroid(); + double filletVolume = filletCentroid.weight; + double filletMass = filletVolume * filletMaterial.getDensity(); + final Coordinate filletCM = filletCentroid.setWeight(filletMass); + + this.totalVolume = (wettedVolume + tabVolume + filletVolume) * finCount; + + final double eachFinMass = finBulkMass + tabMass + filletMass; + final Coordinate eachFinCenterOfMass = wettedCM.average(tabCM).average(filletCM).setWeight(eachFinMass); + + // ^^ per fin + // vv per component + + // set y coordinate: rotate around parent, if single fin; otherwise multiple fins will average out to zero + if (finCount == 1) { + this.centerOfMass = baseRotation.transform( eachFinCenterOfMass ); + } else { + this.centerOfMass = eachFinCenterOfMass.setY(0.).setWeight( eachFinMass * this.finCount); + } + } + // ============= Instanceable Interface Methods =============== + @Override + public Coordinate[] getInstanceOffsets(){ + checkState(); + + final double bodyRadius = this.getBodyRadius(); + final double[] angles = getInstanceAngles(); + + final Transformation localCantRotation = getCantRotation(); + + Coordinate[] toReturn = new Coordinate[finCount]; + for (int instanceNumber = 0; instanceNumber < finCount; instanceNumber++) { + final double curY = bodyRadius * Math.cos(angles[instanceNumber]); + final double curZ = bodyRadius * Math.sin(angles[instanceNumber]); + + final Coordinate naiveLocation = new Coordinate(0, curY, curZ); + + final Coordinate adjustedLocation = baseRotation.transform(localCantRotation.transform( naiveLocation)); + + toReturn[instanceNumber] = adjustedLocation; + } + + return toReturn; + } } diff --git a/core/src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java b/core/src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java index 32f660346..0b2176112 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java +++ b/core/src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java @@ -1,6 +1,8 @@ package net.sf.openrocket.rocketcomponent; +import java.awt.geom.Line2D; import java.awt.geom.Point2D; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -9,9 +11,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.position.AxialMethod; import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.ArrayList; -import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.Coordinate; @@ -19,10 +20,12 @@ public class FreeformFinSet extends FinSet { private static final Logger log = LoggerFactory.getLogger(FreeformFinSet.class); private static final Translator trans = Application.getTranslator(); - private ArrayList points = new ArrayList(); + public static final double MIN_ROOT_CHORD=0.01; // enforce this to prevent erroneous 'intersection' exceptions. + + private List points = new ArrayList<>(); public FreeformFinSet() { - points.add(Coordinate.NUL); + points.add(Coordinate.ZERO); points.add(new Coordinate(0.025, 0.05)); points.add(new Coordinate(0.075, 0.05)); points.add(new Coordinate(0.05, 0)); @@ -30,7 +33,7 @@ public class FreeformFinSet extends FinSet { this.length = 0.05; } - public FreeformFinSet(Coordinate[] finpoints) throws IllegalFinPointException { + public FreeformFinSet(Coordinate[] finpoints) { setPoints(finpoints); } @@ -45,11 +48,10 @@ public class FreeformFinSet extends FinSet { * @return the new freeform fin set. */ public static FreeformFinSet convertFinSet(FinSet finset) { - log.info("Converting " + finset.getComponentName() + " into freeform fin set"); final RocketComponent root = finset.getRoot(); FreeformFinSet freeform; List toInvalidate = Collections.emptyList(); - + try { if (root instanceof Rocket) { ((Rocket) root).freeze(); @@ -64,21 +66,15 @@ public class FreeformFinSet extends FinSet { } else { position = -1; } - - + // Create the freeform fin set Coordinate[] finpoints = finset.getFinPoints(); - try { - freeform = new FreeformFinSet(finpoints); - } catch (IllegalFinPointException e) { - throw new BugException("Illegal fin points when converting existing fin to " + - "freeform fin, fin=" + finset + " points=" + Arrays.toString(finpoints), - e); - } - + freeform = new FreeformFinSet(finpoints); + freeform.setAxialOffset(finset.getAxialMethod(), finset.getAxialOffset()); + // Copy component attributes toInvalidate = freeform.copyFrom(finset); - + // Set name final String componentTypeName = finset.getComponentName(); final String name = freeform.getName(); @@ -87,9 +83,9 @@ public class FreeformFinSet extends FinSet { freeform.setName(freeform.getComponentName() + name.substring(componentTypeName.length())); } - + freeform.setAppearance(finset.getAppearance()); - + // Add freeform fin set to parent if (parent != null) { parent.addChild(freeform, position); @@ -107,8 +103,6 @@ public class FreeformFinSet extends FinSet { return freeform; } - - /** * Add a fin point between indices index-1 and index. * The point is placed at the midpoint of the current segment. @@ -123,8 +117,7 @@ public class FreeformFinSet extends FinSet { // adding a point within the segment affects neither mass nor aerodynamics fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); } - - + /** * Remove the fin point with the given index. The first and last fin points * cannot be removed, and will cause an IllegalFinPointException @@ -138,144 +131,176 @@ public class FreeformFinSet extends FinSet { throw new IllegalFinPointException("cannot remove first or last point"); } - ArrayList copy = this.points.clone(); - copy.remove(index); - validate(copy); - this.points = copy; + // copy the old list in case the operation fails + List copy = new ArrayList<>(this.points); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + this.points.remove(index); + if( ! validate()){ + // if error, rollback. + this.points = copy; + } + + fireComponentChangeEvent(ComponentChangeEvent.AEROMASS_CHANGE); } public int getPointCount() { return points.size(); } - - public void setPoints(Coordinate[] points) throws IllegalFinPointException { - setPoints(Arrays.asList(points)); + + /** + * The first point is assumed to be at the origin. If it isn't, it will be moved there. + * + * @param newPoints new fin points ; replaces previous fin points + */ + public void setPoints(Coordinate[] newPoints) { + if( ! Coordinate.ZERO.equals(newPoints[0])) { + final Coordinate p0 = newPoints[0]; + newPoints = translatePoints(newPoints, p0.x, p0.y); + } + + ArrayList newList = new ArrayList<>(Arrays.asList( newPoints)); + setPoints( newList ); } - public void setPoints(List points) throws IllegalFinPointException { - ArrayList list = new ArrayList(points); - validate(list); - this.points = list; + + /** + * The first point is assumed to be at the origin. If it isn't, it will be moved there. + * + * @param newPoints New points to set as the exposed edges of the fin + */ + public void setPoints( List newPoints) { + // copy the old points, in case validation fails + List copy = new ArrayList<>(this.points); + + this.points = newPoints; + update(); + + if( ! validate()){ + // on error, reset to the old points + this.points = copy; + } this.length = points.get(points.size() - 1).x; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + fireComponentChangeEvent(ComponentChangeEvent.AEROMASS_CHANGE); } + private double y_body( final double x){ + return y_body( x, 0.0 ); + } + + private double y_body( final double x_target, final double x_ref){ + final SymmetricComponent sym = (SymmetricComponent)getParent(); + return ( sym.getRadius(x_target) - sym.getRadius( x_ref)); + } + + public void setPointRelToFin( final int index, final double x_request_fin, final double y_request_fin) throws IllegalFinPointException { + final double x_finStart_body = getAxialFront(); // x @ fin start, body frame + final double y_finStart_body = y_body( x_finStart_body); + + setPoint( index, x_request_fin + x_finStart_body , y_request_fin + y_finStart_body); + } /** * Set the point at position i to coordinates (x,y). *

- * Note that this method enforces basic fin shape restrictions (non-negative y, - * first and last point locations) silently, but throws an - * IllegalFinPointException if the point causes fin segments to - * intersect. - *

+ * Note that this method silently enforces basic fin shape restrictions + * - points may not be within the parent body. + * - first point occurs before last (and vice versa) + * - first and last points must be on the parent body + * - non-self-intersecting fin shape (aborts set on invalid fin point) + *

+ * NOTE: the fin-point axes differ from rocket axes: + * +x within the fin points foreward; +x for the rocket points aft + *

* Moving of the first point in the X-axis is allowed, but this actually moves - * all of the other points the corresponding distance back. + * all of the other points the corresponding distance back, relative to the first. + * That is, moving the first point should not change how the rest of the + * points are positioned *relative to the fin-mount*. * * @param index the point index to modify. - * @param x the x-coordinate. - * @param y the y-coordinate. - * @throws IllegalFinPointException if the specified fin point would cause intersecting - * segments - */ - public void setPoint(int index, double x, double y) throws IllegalFinPointException { - if (y < 0) - y = 0; - - double x0, y0, x1, y1; - - if (index == 0) { - - // Restrict point - x = Math.min(x, points.get(points.size() - 1).x); - y = 0; - x0 = Double.NaN; - y0 = Double.NaN; - x1 = points.get(1).x; - y1 = points.get(1).y; -// } else if ( (0 > index) || (points.size() <= index) ){ -// throw new IllegalFinPointException("Point Index not available!"); - } else if (index == points.size() - 1) { - - // Restrict point - x = Math.max(x, 0); - y = 0; - x0 = points.get(index - 1).x; - y0 = points.get(index - 1).y; - x1 = Double.NaN; - y1 = Double.NaN; - - } else { - - x0 = points.get(index - 1).x; - y0 = points.get(index - 1).y; - x1 = points.get(index + 1).x; - y1 = points.get(index + 1).y; - + * @param xRequest the x-coordinate. + * @param yRequest the y-coordinate. + */ + public void setPoint( final int index, final double xRequest, final double yRequest) { + final SymmetricComponent body = (SymmetricComponent)getParent(); + + final int lastPointIndex = this.points.size() - 1; + final double xFinEnd = points.get(lastPointIndex).x; + final double xFinStart = getAxialFront(); // x of fin start, body-frame + final double yFinStart = body.getRadius( xFinStart); // y of fin start, body-frame + final double xBodyStart = -xFinStart; // x-offset from fin to body; fin-frame + + // initial guess at these values; further checks follow. + double xAgreed = xRequest; + double yAgreed = yRequest; + + // clamp x coordinates: + // within bounds, and consistent with the rest of the fin (at this time). + if( 0 == index ) { + // restrict the first point to be between the parent's start, and the last fin point + xAgreed = Math.max( xBodyStart, Math.min( xAgreed, xFinEnd - MIN_ROOT_CHORD )); + }else if( lastPointIndex == index ){ + // restrict the last point to be between the first fin point, and the parent's end length. + xAgreed = Math.max( MIN_ROOT_CHORD, Math.min( xAgreed, xBodyStart + body.getLength())); } + // adjust y-value to be consistent with body + final double yBody_atPoint= body.getRadius( xFinStart + xAgreed) - yFinStart; + if (index == 0 || index == lastPointIndex) { + // for the first and last points: set y-value to *exactly* match parent body: + yAgreed = yBody_atPoint; + }else{ + // for all other points, merely insist that the point is outside the body... + yAgreed = Math.max( yAgreed, yBody_atPoint); + } + // if moving either begin or end points, we'll probably have to update the position, as well. + final AxialMethod locationMethod = getAxialMethod(); + final double priorXOffset = getAxialOffset(); - // Check for intersecting - double px0, py0, px1, py1; - px0 = 0; - py0 = 0; - for (int i = 1; i < points.size(); i++) { - px1 = points.get(i).x; - py1 = points.get(i).y; + if( 0 == index){ + movePoints( xAgreed); + this.length = points.get( lastPointIndex ).x; - if (i != index - 1 && i != index && i != index + 1) { - if (intersects(x0, y0, x, y, px0, py0, px1, py1)) { - throw new IllegalFinPointException("segments intersect"); - } - } - if (i != index && i != index + 1 && i != index + 2) { - if (intersects(x, y, x1, y1, px0, py0, px1, py1)) { - throw new IllegalFinPointException("segments intersect"); - } + if( AxialMethod.TOP == locationMethod){ + setAxialOffset( AxialMethod.TOP, priorXOffset + xAgreed ); + }else if(AxialMethod.MIDDLE == locationMethod){ + setAxialOffset( AxialMethod.MIDDLE, priorXOffset + xAgreed/2 ); } + }else if( lastPointIndex == index ){ + points.set(index, new Coordinate( xAgreed, yAgreed )); + this.length = xAgreed; - px0 = px1; - py0 = py1; + if( AxialMethod.MIDDLE == locationMethod){ + setAxialOffset( AxialMethod.MIDDLE, priorXOffset + (xAgreed - xFinEnd)/2 ); + }else if(AxialMethod.BOTTOM== locationMethod){ + setAxialOffset( AxialMethod.BOTTOM, priorXOffset + (xAgreed - xFinEnd) ); + } + }else{ + points.set(index, new Coordinate( xAgreed, yAgreed )); } - if (index == 0) { - - //System.out.println("Set point zero to x:" + x); - for (int i = 1; i < points.size(); i++) { - Coordinate c = points.get(i); - points.set(i, c.setX(c.x - x)); - } - - } else { - - points.set(index, new Coordinate(x, y)); - + // this maps the last index and the next-to-last-index to the same 'testIndex' + int testIndex = Math.min( index, (points.size() - 2)); + if( intersects( testIndex)){ + // intersection found! log error and abort! + log.error(String.format("ERROR: found an intersection while setting fin point #%d to [%6.4g, %6.4g] : ABORTING setPoint(..) !! ", index, xRequest, yRequest)); + return; } - if (index == 0 || index == points.size() - 1) { - this.length = points.get(points.size() - 1).x; - } - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + + fireComponentChangeEvent(ComponentChangeEvent.AEROMASS_CHANGE); } - - - - private boolean intersects(double ax0, double ay0, double ax1, double ay1, - double bx0, double by0, double bx1, double by1) { - - double d = ((by1 - by0) * (ax1 - ax0) - (bx1 - bx0) * (ay1 - ay0)); - - double ua = ((bx1 - bx0) * (ay0 - by0) - (by1 - by0) * (ax0 - bx0)) / d; - double ub = ((ax1 - ax0) * (ay0 - by0) - (ay1 - ay0) * (ax0 - bx0)) / d; - - return (ua >= 0) && (ua <= 1) && (ub >= 0) && (ub <= 1); + + private void movePoints( final double delta_x){ + // skip 0th index -- it's the local origin and is always (0,0) + for( int index=1; index < points.size(); ++index){ + final Coordinate oldPoint = this.points.get( index); + final Coordinate newPoint = oldPoint.sub( delta_x, 0.0f, 0.0f); + points.set( index, newPoint); + } } - @Override public Coordinate[] getFinPoints() { return points.toArray(new Coordinate[0]); @@ -297,31 +322,206 @@ public class FreeformFinSet extends FinSet { return trans.get("FreeformFinSet.FreeformFinSet"); } - + @SuppressWarnings("unchecked") @Override protected RocketComponent copyWithOriginalID() { RocketComponent c = super.copyWithOriginalID(); - ((FreeformFinSet) c).points = this.points.clone(); + + ((FreeformFinSet) c).points = new ArrayList<>(this.points); + return c; } - private void validate(ArrayList pts) throws IllegalFinPointException { - final int n = pts.size(); - if (pts.get(0).x != 0 || pts.get(0).y != 0 || - pts.get(n - 1).x < 0 || pts.get(n - 1).y != 0) { - throw new IllegalFinPointException("Start or end point illegal."); - } - for (int i = 0; i < n - 1; i++) { - for (int j = i + 2; j < n - 1; j++) { - if (intersects(pts.get(i).x, pts.get(i).y, pts.get(i + 1).x, pts.get(i + 1).y, - pts.get(j).x, pts.get(j).y, pts.get(j + 1).x, pts.get(j + 1).y)) { - throw new IllegalFinPointException("segments intersect"); - } + @Override + public void setAxialOffset( final AxialMethod newAxialMethod, final double newOffsetRequest){ + super.setAxialOffset( newAxialMethod, newOffsetRequest); + + if( null != parent ) { + // if the new position would cause fin overhang, only allow movement up to the end of the parent component. + // N.B. if you want a fin to overhang, add & adjust interior points. + final double backOverhang = getAxialOffset(AxialMethod.BOTTOM); + if (0 < backOverhang) { + final double newOffset = newOffsetRequest - backOverhang; + super.setAxialOffset(newAxialMethod, newOffset); } - if (pts.get(i).z != 0) { - throw new IllegalFinPointException("z-coordinate not zero"); + final double frontOverhang = getAxialFront(); + if (0 > frontOverhang) { + final double newOffset = newOffsetRequest - frontOverhang; + super.setAxialOffset(newAxialMethod, newOffset); } } } + @Override + public void update(){ + final int lastPointIndex = this.points.size() - 1; + this.length = points.get(lastPointIndex).x; + + this.setAxialOffset( this.axialMethod, this.axialOffset); + + clampFirstPoint(); + clampInteriorPoints(); + clampLastPoint(); + + validateFinTab(); + } + + // if we translate the points, correct the first point, because it may be inconsistent + private void clampFirstPoint(){ + double xFinStart = getAxialFront(); // x @ fin start, body frame + final double xFinOffset = getAxialOffset(); + if( 0 > xFinStart ){ + setAxialOffset( xFinOffset - xFinStart); + } + } + + private void clampInteriorPoints(){ + if( null == this.parent ){ + // this is bad, but seems to only occur during unit tests. + return; + } + final SymmetricComponent symmetricParent = (SymmetricComponent)this.getParent(); + + final Coordinate finFront = getFinFront(); + + // omit end points index + for( int index=1; index < (points.size()-1); ++index){ + final Coordinate oldPoint = this.points.get( index); + + final double yBody = symmetricParent.getRadius( oldPoint.x + finFront.x); + final double yFinPoint = finFront.y+ oldPoint.y; + + if( yBody > yFinPoint ){ + final Coordinate newPoint = oldPoint.setY( yBody - finFront.y ); + points.set( index, newPoint); + } + } + } + + // if we translate the points, the final point may become inconsistent + private void clampLastPoint(){ + if( null == this.parent ){ + // this is bad, but seems to only occur during unit tests. + return; + } + + final SymmetricComponent body = (SymmetricComponent)getParent(); + // clamp the final x coord to the end of the parent body. + final int lastPointIndex = points.size() - 1; + final Coordinate oldPoint = points.get( lastPointIndex); + + final double xFinStart_body = getAxialFront(); // x @ fin start, body frame + final double xBodyEnd_fin = body.getLength() - xFinStart_body; + + double x_clamped = Math.min( oldPoint.x, xBodyEnd_fin); + double y_clamped = body.getRadius( x_clamped+xFinStart_body) - body.getRadius( xFinStart_body); + + + points.set( lastPointIndex, new Coordinate( x_clamped, y_clamped, 0)); + } + + private boolean validate() { + final Coordinate firstPoint = this.points.get(0); + if (firstPoint.x != 0 || firstPoint.y != 0 ){ + log.error("Start point illegal -- not located at (0,0): "+firstPoint+ " ("+ getName()+")"); + return false; + } + + final Coordinate lastPoint = this.points.get( points.size() -1); + if( lastPoint.x < 0){ + log.error("End point illegal: end point starts in front of start point: "+lastPoint.x); + return false; + } + + // the last point *is* restricted to be on the surface of its owning component: + SymmetricComponent symBody = (SymmetricComponent)this.getParent(); + if( null != symBody ){ + final double startOffset = this.getAxialFront(); + final Coordinate finStart = new Coordinate( startOffset, symBody.getRadius(startOffset) ); + + // campare x-values + final Coordinate finAtLast = lastPoint.add(finStart); + if( symBody.getLength() < finAtLast.x ){ + log.error("End point falls after parent body ends: ["+symBody.getName()+"]. Exception: ", new IllegalFinPointException("Fin ends after its parent body \""+symBody.getName()+"\". Ignoring.")); + log.error(String.format(" ..fin position: (x: %12.10f via: %s)", this.axialOffset, this.axialMethod.name())); + log.error(String.format(" ..Body Length: %12.10f finLength: %12.10f", symBody.getLength(), this.getLength())); + log.error(String.format(" ..fin endpoint: (x: %12.10f, y: %12.10f)", finAtLast.x, finAtLast.y)); + return false; + } + + // compare the y-values + final Coordinate bodyAtLast = finAtLast.setY( symBody.getRadius( finAtLast.x ) ); + if( 0.0001 < Math.abs( finAtLast.y - bodyAtLast.y) ){ + String numbers = String.format( "finStart=(%6.2g,%6.2g) // fin_end=(%6.2g,%6.2g) // body=(%6.2g,%6.2g)", finStart.x, finStart.y, finAtLast.x, finAtLast.y, bodyAtLast.x, bodyAtLast.y ); + log.error("End point does not touch its parent body ["+symBody.getName()+"]. exception: ", new IllegalFinPointException("End point does not touch its parent body! Expected: "+numbers)); + log.error(" .."+numbers); + return false; + } + } + + if( intersects()){ + log.error("found intersection in finset points!"); + return false; + } + + final int lastIndex = points.size() - 1; + final List pts = this.points; + for (int i = 0; i < lastIndex; i++) { + if (pts.get(i).z != 0) { + log.error("z-coordinate not zero"); + return false; + } + } + + return true; + } + + /** + * Check if *any* of the fin-point line segments intersects with another. + * + * @return true if an intersection is found + */ + public boolean intersects( ){ + for( int index=0; index < (this.points.size()-1); ++index ){ + if( intersects( index )){ + return true; + } + } + return false; + } + + /** + * Check if the line segment from targetIndex to targetIndex+1 intersects with any other part of the fin. + * + * + * + * @return true if an intersection was found + */ + private boolean intersects( final int targetIndex){ + if( (points.size()-2) < targetIndex ){ + throw new IndexOutOfBoundsException("request validate of non-existent fin edge segment: "+ targetIndex + "/"+points.size()); + } + + // (pre-check the indices above.) + Point2D.Double p1 = new Point2D.Double( points.get(targetIndex).x, points.get(targetIndex).y); + Point2D.Double p2 = new Point2D.Double( points.get(targetIndex+1).x, points.get(targetIndex+1).y); + Line2D.Double targetLine = new Line2D.Double( p1, p2); + + for (int comparisonIndex = 0; comparisonIndex < (points.size()-1); ++comparisonIndex ) { + if( 2 > Math.abs( targetIndex - comparisonIndex) ){ + // a line segment will trivially not intersect with itself + // nor can adjacent line segments intersect with each other, because they share a common endpoint. + continue; + } + + Line2D.Double comparisonLine = new Line2D.Double( points.get(comparisonIndex).x, points.get(comparisonIndex).y, // p1 + points.get(comparisonIndex+1).x, points.get(comparisonIndex+1).y); // p2 + + if ( targetLine.intersectsLine( comparisonLine ) ) { + return true; + } + } + return false; + } + } diff --git a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java index 90e92f270..b004c22e1 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java +++ b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java @@ -910,8 +910,8 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab /** * Get the positioning of the component relative to its parent component. - * This is one of the enums of {@link AxialMethod}. A setter method is not provided, - * but can be provided by a subclass. + * + * @return This will return one of the enums of {@link AxialMethod} */ public final AxialMethod getAxialMethod() { return axialMethod; @@ -952,7 +952,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab public double getAxialOffset(AxialMethod asMethod) { double parentLength = 0; if (null != this.parent) { - parentLength = this.parent.length; + parentLength = this.parent.length; } if(AxialMethod.ABSOLUTE == asMethod){ @@ -965,7 +965,11 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab public double getAxialOffset() { return this.axialOffset; } - + + public double getAxialFront(){ + return this.position.x; + } + public double getRadiusOffset() { mutex.verify(); return 0; @@ -1017,14 +1021,14 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab * Set the position value of the component. The exact meaning of the value * depends on the current relative positioning. * - * @param newOffset the position value of the component. - */ - public void setAxialOffset(double newOffset) { + * @param newOffset the desired offset of this component, using the components current axial-method + */ + public void setAxialOffset(double newOffset) { this.setAxialOffset(this.axialMethod, newOffset); this.fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } - final protected void setAxialOffset( final AxialMethod requestedMethod, final double requestedOffset) { + protected void setAxialOffset( final AxialMethod requestedMethod, final double requestedOffset) { checkState(); double newX = Double.NaN; @@ -1060,7 +1064,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab this.setAxialOffset(this.axialMethod, this.axialOffset); } - public final void updateChildren(){ + private final void updateChildren(){ this.update(); for( RocketComponent rc : children ){ rc.updateChildren(); @@ -1079,7 +1083,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab *

* NOTE: the length of this array returned always equals this.getInstanceCount() * - * @return an generated (i.e. new) array of instance locations + * @return a generated (i.e. new) array of instance locations */ // @Override Me ! public Coordinate[] getInstanceLocations(){ @@ -1529,8 +1533,9 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab public final RocketComponent getRoot() { checkState(); RocketComponent gp = this; - while (gp.parent != null) + while (gp.parent != null){ gp = gp.parent; + } return gp; } @@ -2119,12 +2124,21 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab // multi-line output protected StringBuilder toDebugDetail() { StringBuilder buf = new StringBuilder(); + + // infer the calling method name StackTraceElement[] stackTrace = (new Exception()).getStackTrace(); - buf.append(" >> Dumping Detailed Information from: " + stackTrace[1].getMethodName() + "\n"); - buf.append(" current Component: " + this.getName() + " ofClass: " + this.getClass().getSimpleName() + "\n"); - buf.append(" offset: " + this.axialOffset + " via: " + this.axialMethod.name() + " => " + this.getAxialOffset() + "\n"); - buf.append(" thisCenterX: " + this.position.x + "\n"); - buf.append(" this length: " + this.length + "\n"); + String callingMethod = stackTrace[1].getMethodName(); + for( StackTraceElement el : stackTrace ){ + if( ! "toDebugDetail".equals(el.getMethodName())){ + callingMethod = el.getMethodName(); + break; + } + } + + buf.append(String.format(" >> Dumping Detailed Information from: %s\n", callingMethod)); + buf.append(String.format(" At Component: %s, of class: %s \n", this.getName(), this.getClass().getSimpleName())); + buf.append(String.format(" position: %.6f at offset: %.4f via: %s\n", this.position.x, this.axialOffset, this.axialMethod.name())); + buf.append(String.format(" length: %.4f\n", this.length )); return buf; } @@ -2152,7 +2166,24 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab public void toDebugTreeNode(final StringBuilder buffer, final String indent) { String prefix = String.format("%s%s (x%d)", indent, this.getName(), this.getInstanceCount() ); - buffer.append(String.format("%-40s| %6.4f; %24s; %24s; \n", prefix, getLength(), getPosition().toPreciseString(), getComponentLocations()[0].toPreciseString() )); + // 1) instanced vs non-instanced + if( 1 == getInstanceCount() ){ + // un-instanced RocketComponents (usual case) + buffer.append(String.format("%-40s| %5.3f; %24s; %24s; ", prefix, this.getLength(), this.axialOffset, this.getComponentLocations()[0])); + buffer.append(String.format("(offset: %4.1f via: %s )\n", this.getAxialOffset(), this.axialMethod.name())); + }else if( this instanceof Instanceable ){ + // instanced components -- think motor clusters or booster stage clusters + final String patternName = ((Instanceable)this).getPatternName(); + buffer.append(String.format("%-40s (cluster: %s )", prefix, patternName)); + buffer.append(String.format("(offset: %4.1f via: %s )\n", this.getAxialOffset(), this.axialMethod.name())); + + for (int instanceNumber = 0; instanceNumber < this.getInstanceCount(); instanceNumber++) { + final String instancePrefix = String.format("%s [%2d/%2d]", indent, instanceNumber+1, getInstanceCount()); + buffer.append(String.format("%-40s| %5.3f; %24s; %24s;\n", instancePrefix, getLength(), this.axialOffset, getLocations()[0])); + } + }else{ + throw new IllegalStateException("This is a developer error! If you implement an instanced class, please subclass the Instanceable interface."); + } // 2) if this is an ACTING motor mount: if(( this instanceof MotorMount ) &&( ((MotorMount)this).isMotorMount())){ diff --git a/core/src/net/sf/openrocket/rocketcomponent/Transition.java b/core/src/net/sf/openrocket/rocketcomponent/Transition.java index f9ded5e0d..587b2b509 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/Transition.java +++ b/core/src/net/sf/openrocket/rocketcomponent/Transition.java @@ -1,5 +1,11 @@ package net.sf.openrocket.rocketcomponent; +import static java.lang.Math.sin; +import static net.sf.openrocket.util.MathUtil.pow2; +import static net.sf.openrocket.util.MathUtil.pow3; + +import java.util.Collection; + import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.preset.ComponentPreset; import net.sf.openrocket.preset.ComponentPreset.Type; @@ -7,12 +13,6 @@ import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; -import java.util.Collection; - -import static java.lang.Math.sin; -import static net.sf.openrocket.util.MathUtil.pow2; -import static net.sf.openrocket.util.MathUtil.pow3; - public class Transition extends SymmetricComponent { private static final Translator trans = Application.getTranslator(); @@ -526,13 +526,16 @@ public class Transition extends SymmetricComponent { * Check whether the given type can be added to this component. Transitions allow any * InternalComponents to be added. * - * @param ctype The RocketComponent class type to add. + * @param comptype The RocketComponent class type to add. * @return Whether such a component can be added. */ @Override - public boolean isCompatible(Class ctype) { - if (InternalComponent.class.isAssignableFrom(ctype)) + public boolean isCompatible(Class comptype) { + if (InternalComponent.class.isAssignableFrom(comptype)){ return true; + }else if ( FreeformFinSet.class.isAssignableFrom(comptype)){ + return true; + } return false; } @@ -932,4 +935,5 @@ public class Transition extends SymmetricComponent { return null; } } + } diff --git a/core/src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java b/core/src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java index 5ee55d9b5..b8a92a993 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java +++ b/core/src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java @@ -1,13 +1,13 @@ package net.sf.openrocket.rocketcomponent; +import java.util.ArrayList; +import java.util.List; + import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; -import java.util.ArrayList; -import java.util.List; - /** * A set of trapezoidal fins. The root and tip chords are perpendicular to the rocket * base line, while the leading and aft edges may be slanted. @@ -65,6 +65,7 @@ public class TrapezoidFinSet extends FinSet { this.sweep = sweep; this.height = height; this.thickness = thickness; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } @@ -76,6 +77,7 @@ public class TrapezoidFinSet extends FinSet { if (length == r) return; length = Math.max(r, 0); + validateFinTab(); fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } diff --git a/core/src/net/sf/openrocket/rocketcomponent/TubeFinSet.java b/core/src/net/sf/openrocket/rocketcomponent/TubeFinSet.java index ec02d0ab5..d98d3f927 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/TubeFinSet.java +++ b/core/src/net/sf/openrocket/rocketcomponent/TubeFinSet.java @@ -268,14 +268,9 @@ public class TubeFinSet extends ExternalComponent { } // 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; + totalInertia += inertia + MathUtil.pow2( this.axialOffset); } return totalInertia; } diff --git a/core/src/net/sf/openrocket/rocketcomponent/position/AxialMethod.java b/core/src/net/sf/openrocket/rocketcomponent/position/AxialMethod.java index 6e35e4b38..370dcb523 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/position/AxialMethod.java +++ b/core/src/net/sf/openrocket/rocketcomponent/position/AxialMethod.java @@ -32,8 +32,9 @@ public enum AxialMethod implements DistanceMethod { @Override public double getAsOffset(double position, double innerLength, double outerLength){ return position - outerLength; + //return outerLength - position; } - }, + }, // measure from the top of the target component to the top of the subject component TOP (Application.getTranslator().get("RocketComponent.Position.Method.Axial.TOP")){ @@ -61,6 +62,7 @@ public enum AxialMethod implements DistanceMethod { @Override public double getAsPosition(double offset, double innerLength, double outerLength){ return offset + (outerLength - innerLength) / 2; + // return (outerLength - innerLength) / 2 - offset; } @Override @@ -77,6 +79,7 @@ public enum AxialMethod implements DistanceMethod { @Override public double getAsPosition(double offset, double innerLength, double outerLength){ return offset + (outerLength - innerLength); + //return outerLength - innerLength - offset; } @Override diff --git a/core/src/net/sf/openrocket/simulation/BasicTumbleStatus.java b/core/src/net/sf/openrocket/simulation/BasicTumbleStatus.java index 25b08077e..9d895809d 100644 --- a/core/src/net/sf/openrocket/simulation/BasicTumbleStatus.java +++ b/core/src/net/sf/openrocket/simulation/BasicTumbleStatus.java @@ -54,15 +54,16 @@ public class BasicTumbleStatus extends SimulationStatus { continue; } if (component instanceof FinSet) { + final FinSet finComponent = ((FinSet) component); + final double finArea = finComponent.getPlanformArea(); + int finCount = finComponent.getFinCount(); - double finComponent = ((FinSet) component).getFinArea(); - int finCount = ((FinSet) component).getFinCount(); // check bounds on finCount. if (finCount >= finEff.length) { finCount = finEff.length - 1; } - aFins += finComponent * finEff[finCount]; + aFins += finArea * finEff[finCount]; } else if (component instanceof SymmetricComponent) { aBt += ((SymmetricComponent) component).getComponentPlanformArea(); diff --git a/core/src/net/sf/openrocket/util/TestRockets.java b/core/src/net/sf/openrocket/util/TestRockets.java index 9595564ad..fdd7b0061 100644 --- a/core/src/net/sf/openrocket/util/TestRockets.java +++ b/core/src/net/sf/openrocket/util/TestRockets.java @@ -613,17 +613,14 @@ public class TestRockets { bodytube = new BodyTube(0.69, 0.033, 0.001); finset = new FreeformFinSet(); - try { - finset.setPoints(new Coordinate[] { - new Coordinate(0, 0), - new Coordinate(0.115, 0.072), - new Coordinate(0.255, 0.072), - new Coordinate(0.255, 0.037), - new Coordinate(0.150, 0) - }); - } catch (IllegalFinPointException e) { - e.printStackTrace(); - } + final Coordinate[] finPoints = { + new Coordinate(0, 0), + new Coordinate(0.115, 0.072), + new Coordinate(0.255, 0.072), + new Coordinate(0.255, 0.037), + new Coordinate(0.150, 0)}; + finset.setPoints(finPoints); + finset.setThickness(0.003); finset.setFinCount(4); diff --git a/core/test/net/sf/openrocket/file/rocksim/importt/FinSetHandlerTest.java b/core/test/net/sf/openrocket/file/rocksim/importt/FinSetHandlerTest.java index a963e52d4..297528289 100644 --- a/core/test/net/sf/openrocket/file/rocksim/importt/FinSetHandlerTest.java +++ b/core/test/net/sf/openrocket/file/rocksim/importt/FinSetHandlerTest.java @@ -3,29 +3,77 @@ */ package net.sf.openrocket.file.rocksim.importt; -import junit.framework.TestCase; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.lang.reflect.Method; +import java.util.HashMap; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.util.Modules; + +import net.sf.openrocket.ServicesForTesting; import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.l10n.DebugTranslator; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.plugin.PluginModule; import net.sf.openrocket.rocketcomponent.BodyTube; import net.sf.openrocket.rocketcomponent.EllipticalFinSet; import net.sf.openrocket.rocketcomponent.FinSet; import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; +import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; -import java.lang.reflect.Method; -import java.util.HashMap; /** * FinSetHandler Tester. * */ -public class FinSetHandlerTest extends TestCase { +public class FinSetHandlerTest { + final static double EPSILON = MathUtil.EPSILON; + + @BeforeClass + public static void setup() { + Module applicationModule = new ServicesForTesting(); + + Module pluginModule = new PluginModule(); + + Module debugTranslator = new AbstractModule() { + @Override + protected void configure() { + bind(Translator.class).toInstance(new DebugTranslator(null)); + } + }; + + Injector injector = Guice.createInjector(Modules.override(applicationModule).with(debugTranslator), pluginModule); + + Application.setInjector(injector); + + File tmpDir = new File("./tmp"); + if (!tmpDir.exists()) { + boolean success = tmpDir.mkdirs(); + assertTrue("Unable to create core/tmp dir needed for tests.", success); + } + + } + + /** * Method: asOpenRocket(WarningSet warnings) * * @throws Exception thrown if something goes awry */ - @org.junit.Test + @Test public void testAsOpenRocket() throws Exception { FinSetHandler dto = new FinSetHandler(null, new BodyTube()); @@ -37,15 +85,15 @@ public class FinSetHandlerTest extends TestCase { dto.closeElement("ShapeCode", attributes, "0", warnings); dto.closeElement("Xb", attributes, "2", warnings); dto.closeElement("FinCount", attributes, "4", warnings); - dto.closeElement("RootChord", attributes, "10", warnings); - dto.closeElement("TipChord", attributes, "11", warnings); + dto.closeElement("RootChord", attributes, "100", warnings); + dto.closeElement("TipChord", attributes, "50", warnings); dto.closeElement("SemiSpan", attributes, "12", warnings); dto.closeElement("MidChordLen", attributes, "13", warnings); dto.closeElement("SweepDistance", attributes, "14", warnings); dto.closeElement("Thickness", attributes, "200", warnings); dto.closeElement("TipShapeCode", attributes, "1", warnings); - dto.closeElement("TabLength", attributes, "400", warnings); - dto.closeElement("TabDepth", attributes, "500", warnings); + dto.closeElement("TabLength", attributes, "40", warnings); + dto.closeElement("TabDepth", attributes, "50", warnings); dto.closeElement("TabOffset", attributes, "30", warnings); dto.closeElement("RadialAngle", attributes, ".123", warnings); dto.closeElement("PointList", attributes, "20,0|2,2|0,0", warnings); @@ -55,34 +103,37 @@ public class FinSetHandlerTest extends TestCase { FinSet fins = dto.asOpenRocket(set); assertNotNull(fins); assertEquals(0, set.size()); + + String debugInfo = fins.toDebugDetail().toString(); assertEquals("The name", fins.getName()); assertTrue(fins instanceof TrapezoidFinSet); - assertEquals(4, fins.getFinCount()); + assertEquals("imported fin count does not match.", 4, fins.getFinCount()); - assertEquals(0.012d, ((TrapezoidFinSet) fins).getHeight()); - assertEquals(0.012d, fins.getSpan()); + assertEquals("imported fin height does not match.", 0.012d, ((TrapezoidFinSet) fins).getHeight(), EPSILON); + assertEquals("imported fin span does not match.", 0.012d, fins.getSpan(), EPSILON); - assertEquals(0.2d, fins.getThickness()); - assertEquals(0.4d, fins.getTabLength()); - assertEquals(0.5d, fins.getTabHeight()); - assertEquals(0.03d, fins.getTabShift()); - assertEquals(.123d, fins.getBaseRotation()); + assertEquals("imported fin thickness does not match.", 0.2d, fins.getThickness(), EPSILON); + assertEquals("imported fin tab length does not match: "+debugInfo, 0.04d, fins.getTabLength(), EPSILON); + assertEquals("imported fin tab height does not match: "+debugInfo, 0.05d, fins.getTabHeight(), EPSILON); + assertEquals("imported fin shift does not match.", 0.03d, fins.getTabOffset(), EPSILON); + assertEquals("imported fin rotation does not match.", .123d, fins.getBaseRotation(), EPSILON); dto.closeElement("ShapeCode", attributes, "1", warnings); + fins = dto.asOpenRocket(set); assertNotNull(fins); assertEquals(0, set.size()); assertEquals("The name", fins.getName()); assertTrue(fins instanceof EllipticalFinSet); - assertEquals(4, fins.getFinCount()); + assertEquals("imported fin count does not match.", 4, fins.getFinCount()); - assertEquals(0.2d, fins.getThickness()); - assertEquals(0.4d, fins.getTabLength()); - assertEquals(0.5d, fins.getTabHeight()); - assertEquals(0.03d, fins.getTabShift()); - assertEquals(.123d, fins.getBaseRotation()); + assertEquals("imported fin thickness does not match.", 0.2d, fins.getThickness(), EPSILON); + assertEquals("imported fin tab length does not match.", 0.04d, fins.getTabLength(), EPSILON); + assertEquals("imported fin tab height does not match.", 0.05d, fins.getTabHeight(), EPSILON); + assertEquals("imported fin tab shift does not match.", 0.03d, fins.getTabOffset(), EPSILON); + assertEquals("imported fin rotation does not match.", .123d, fins.getBaseRotation(), EPSILON); } diff --git a/core/test/net/sf/openrocket/rocketcomponent/FinSetTest.java b/core/test/net/sf/openrocket/rocketcomponent/FinSetTest.java index 1229b65ad..cd94adee7 100644 --- a/core/test/net/sf/openrocket/rocketcomponent/FinSetTest.java +++ b/core/test/net/sf/openrocket/rocketcomponent/FinSetTest.java @@ -2,22 +2,209 @@ package net.sf.openrocket.rocketcomponent; import static org.junit.Assert.assertEquals; +import net.sf.openrocket.material.Material; import org.junit.Test; +import net.sf.openrocket.rocketcomponent.position.*; +import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.BaseTestCase.BaseTestCase; public class FinSetTest extends BaseTestCase { + private static final double EPSILON = 1E-8; + @Test public void testMultiplicity() { - final TrapezoidFinSet trapFins = new TrapezoidFinSet(); - assertEquals(1, trapFins.getFinCount()); - - final FreeformFinSet fffins = new FreeformFinSet(); - assertEquals(1, fffins.getFinCount()); - - final EllipticalFinSet efins = new EllipticalFinSet(); - assertEquals(1, efins.getFinCount()); + final EllipticalFinSet fins = new EllipticalFinSet(); + assertEquals(1, fins.getFinCount()); } - + + /** + * sweep= 0.02 | tipChord = 0.02 + * | | | + * | +------+ ---------- + * | / \ + * | / \ height = 0.05 + * | / \ + * / \ + * __________/________________\_____ length == rootChord == 0.06 + * | | + * | | tab height = 0.02 + * | | + * +--------+ tab length = 0.02 + * position = 0.0 via middle + * + * Fin Area = 0.05 * ( (0.2 + 0.06)/2) = 0.0 + */ + private static FinSet createSimpleFin() { + + TrapezoidFinSet fins = new TrapezoidFinSet(1, 0.06, 0.02, 0.02, 0.05); + fins.setName("test fins"); + fins.setAxialOffset(AxialMethod.MIDDLE, 0.0); + fins.setMaterial(Material.newMaterial(Material.Type.BULK, "Fin-Test-Material", 1.0, true)); + fins.setThickness(0.005); // == 5 mm + + fins.setTabLength(0.02); + fins.setTabOffsetMethod(AxialMethod.TOP); + fins.setTabOffset(0.02); + + fins.setFilletRadius(0.0); + + return fins; + } + + @Test + public void testTabLocation() { + final FinSet fins = FinSetTest.createSimpleFin(); + assertEquals("incorrect fin length:", 0.06, fins.getLength(), EPSILON); + assertEquals("incorrect fin tab length:", 0.02, fins.getTabLength(), EPSILON); + + final double expFront = 0.02; + final AxialMethod[] methods = AxialMethod.axialOffsetMethods; + final double[] expShift = {0.02, 0.0, -0.02}; + for( int caseIndex=0; caseIndex < methods.length; ++caseIndex ){ + double actFront = fins.getTabFrontEdge(); + assertEquals(" Front edge doesn't match!", expFront, actFront, EPSILON); + + // update + fins.setTabOffsetMethod( methods[caseIndex]); + + //query + double actShift = fins.getTabOffset(); + assertEquals(String.format("Offset doesn't match for: %s \n", methods[caseIndex].name()), expShift[caseIndex], actShift, EPSILON); + } + } + + @Test + public void testTabGetAs(){ + final FinSet fins = FinSetTest.createSimpleFin(); + assertEquals("incorrect fin length:", 0.06, fins.getLength(), EPSILON); + assertEquals("incorrect fin tab length:", 0.02, fins.getTabLength(), EPSILON); + + // TOP -> native(TOP) + fins.setTabOffsetMethod(AxialMethod.TOP); + fins.setTabOffset(0.0); + + assertEquals("Setting by TOP method failed!", 0.0, fins.getTabFrontEdge(), EPSILON); + assertEquals("Setting by TOP method failed!", 0.0, fins.getTabOffset(), EPSILON); + + // MIDDLE -> native + fins.setTabOffsetMethod(AxialMethod.MIDDLE); + fins.setTabOffset(0.0); + assertEquals("Setting by TOP method failed!", 0.02, fins.getTabFrontEdge(), EPSILON); + assertEquals("Setting by TOP method failed!", 0.0, fins.getTabOffset(), EPSILON); + + // BOTTOM -> native + fins.setTabOffsetMethod(AxialMethod.BOTTOM); + fins.setTabOffset(0.0); + + assertEquals("Setting by TOP method failed!", 0.04, fins.getTabFrontEdge(), EPSILON); + assertEquals("Setting by TOP method failed!", 0.0, fins.getTabOffset(), EPSILON); + } + + @Test + public void testTabLocationUpdate() { + final FinSet fins = FinSetTest.createSimpleFin(); + assertEquals("incorrect fin length:", 0.06, fins.getLength(), EPSILON); + assertEquals("incorrect fin tab length:", 0.02, fins.getTabLength(), EPSILON); + + // TOP -> native(TOP) + fins.setTabOffsetMethod(AxialMethod.MIDDLE); + fins.setTabOffset(0.0); + + assertEquals("Setting by TOP method failed!", 0.0, fins.getTabOffset(), EPSILON); + assertEquals("Setting by TOP method failed!", 0.02, fins.getTabFrontEdge(), EPSILON); + + ((TrapezoidFinSet)fins).setRootChord(0.08); + + assertEquals("Front edge doesn't match after adjusting root chord...", 0.03, fins.getTabFrontEdge(), EPSILON); + assertEquals("Offset doesn't match after adjusting root chord....", 0.0, fins.getTabOffset(), EPSILON); + } + + @Test + public void testAreaCalculationsSingleIncrement() { + Coordinate[] basicPoints = { + new Coordinate(0.00, 0.0), + new Coordinate(0.06, 0.06), + new Coordinate(0.06, 0.0), + new Coordinate(0.00, 0.0) }; + // + // [1] + + // /| + // / | + // [0] +--+ [2] + // [3] + // + + final double expArea = 0.06 * 0.06 * 0.5; + final Coordinate actCentroid = FinSet.calculateCurveIntegral(basicPoints); + assertEquals(" basic area doesn't match...", expArea, actCentroid.weight, EPSILON); + assertEquals(" basic centroid x doesn't match: ", 0.04, actCentroid.x, 1e-8); + assertEquals(" basic centroid y doesn't match: ", 0.02, actCentroid.y, 1e-8); + } + + @Test + public void testAreaCalculationsDoubleIncrement() { + Coordinate[] basicPoints = { + new Coordinate(0.00, 0.0), + new Coordinate(0.06, 0.06), + new Coordinate(0.12, 0.0), + new Coordinate(0.00, 0.0) }; + // + // [1] + + // / \ + // / \ + // [0] +-----+ [2] + // [3] + // + + final double expArea = 0.06 * 0.12 * 0.5; + final Coordinate actCentroid = FinSet.calculateCurveIntegral(basicPoints); + assertEquals(" basic area doesn't match...", expArea, actCentroid.weight, EPSILON); + assertEquals(" basic centroid x doesn't match: ", 0.06, actCentroid.x, 1e-8); + assertEquals(" basic centroid y doesn't match: ", 0.02, actCentroid.y, 1e-8); + } + + + @Test + public void testAreaCalculations() { + Coordinate[] basicPoints = { + new Coordinate(0.00, 0.0), + new Coordinate(0.02, 0.05), + new Coordinate(0.04, 0.05), + new Coordinate(0.06, 0.0), + new Coordinate(0.00, 0.0) }; + /* + * [1] +--+ [2] + * / \ + * / \ + * [0] +--------+ [3] + * [4] + */ + final double expArea = 0.04 * 0.05; + final Coordinate actCentroid = FinSet.calculateCurveIntegral(basicPoints); + assertEquals(" basic area doesn't match...", expArea, actCentroid.weight, EPSILON); + assertEquals(" basic centroid x doesn't match: ", 0.03000, actCentroid.x, 1e-8); + assertEquals(" basic centroid y doesn't match: ", 0.020833333, actCentroid.y, 1e-8); + } + + @Test + public void testFinInstanceAngles() { + FinSet fins = createSimpleFin(); + fins.setBaseRotation(Math.PI/6); // == 30d + fins.setInstanceCount(3); // == 120d between each fin + // => [ 30, 150, 270 ] + // => PI*[ 1/6, 5/6, 9/6 ] + // => [ .523, 2.61, 4.71 ] + + final double[] instanceAngles = fins.getInstanceAngles(); + assertEquals( (1./6.)* Math.PI, fins.getBaseRotation(), EPSILON); + assertEquals( (1./6.)* Math.PI, fins.getAngleOffset(), EPSILON); + + assertEquals((1./6.)* Math.PI, instanceAngles[0], EPSILON); + assertEquals((5./6.)* Math.PI, instanceAngles[1], EPSILON); + assertEquals((9./6.)* Math.PI, instanceAngles[2], EPSILON); + } + + } diff --git a/core/test/net/sf/openrocket/rocketcomponent/FreeformFinSetTest.java b/core/test/net/sf/openrocket/rocketcomponent/FreeformFinSetTest.java index 803feb4f5..15457dd03 100644 --- a/core/test/net/sf/openrocket/rocketcomponent/FreeformFinSetTest.java +++ b/core/test/net/sf/openrocket/rocketcomponent/FreeformFinSetTest.java @@ -1,13 +1,17 @@ package net.sf.openrocket.rocketcomponent; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - import java.awt.geom.Point2D; import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertThat; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; + import net.sf.openrocket.aerodynamics.AerodynamicForces; import net.sf.openrocket.aerodynamics.FlightConditions; import net.sf.openrocket.aerodynamics.WarningSet; @@ -16,97 +20,396 @@ import net.sf.openrocket.material.Material; import net.sf.openrocket.material.Material.Type; import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish; import net.sf.openrocket.rocketcomponent.FinSet.CrossSection; -import net.sf.openrocket.rocketcomponent.FinSet.TabRelativePosition; -import net.sf.openrocket.rocketcomponent.position.*; + +import net.sf.openrocket.rocketcomponent.position.AxialMethod; +import net.sf.openrocket.rocketcomponent.Transition.Shape; import net.sf.openrocket.util.Color; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.LineStyle; import net.sf.openrocket.util.BaseTestCase.BaseTestCase; public class FreeformFinSetTest extends BaseTestCase { - + + private static final double EPSILON = 1E-6; + @Test - public void testFreeformCGComputationSimpleTrapezoid() throws Exception { - // This is a trapezoid. Height 1, root 1, tip 1/2 no sweep. - // It can be decomposed into a rectangle followed by a triangle - // +---+ - // | \ - // | \ - // +------+ + public void testMultiplicity() { + final FreeformFinSet fins = new FreeformFinSet(); + assertEquals(1, fins.getFinCount()); + } + + private FreeformFinSet testFreeformConvert(FinSet sourceSet) { + sourceSet.setName("test-convert-finset"); + sourceSet.setBaseRotation(1.1); + sourceSet.setCantAngle(0.001); + sourceSet.setCGOverridden(true); + sourceSet.setColor(Color.BLACK); + sourceSet.setComment("cmt"); + sourceSet.setCrossSection(CrossSection.ROUNDED); + sourceSet.setFinCount(5); + + if( EllipticalFinSet.class.isAssignableFrom(sourceSet.getClass())){ + ((EllipticalFinSet)sourceSet).setLength(0.1); + }else if( TrapezoidFinSet.class.isAssignableFrom(sourceSet.getClass())){ + ((TrapezoidFinSet)sourceSet).setRootChord(0.1); + } + + sourceSet.setFinish(Finish.ROUGH); + sourceSet.setLineStyle(LineStyle.DASHDOT); + sourceSet.setMassOverridden(true); + sourceSet.setMaterial(Material.newMaterial(Type.BULK, "test-material", 0.1, true)); + sourceSet.setOverrideCGX(0.012); + sourceSet.setOverrideMass(0.0123); + sourceSet.setOverrideSubcomponents(true); + sourceSet.setAxialOffset(0.1); + sourceSet.setAxialMethod(AxialMethod.ABSOLUTE); + sourceSet.setTabHeight(0.01); + sourceSet.setTabLength(0.02); + sourceSet.setTabOffsetMethod(AxialMethod.BOTTOM); + sourceSet.setTabOffset(-0.015); + sourceSet.setThickness(0.005); + + return FreeformFinSet.convertFinSet( sourceSet); + } + + private Rocket createTemplateRocket(){ + Rocket rocket = new Rocket(); + AxialStage stage = new AxialStage(); + rocket.addChild(stage); + + NoseCone nose = new NoseCone(); + nose.setForeRadius(0.0); + nose.setLength(1.0); + nose.setAftRadius(1.0); + nose.setType( Shape.ELLIPSOID ); + nose.setShapeParameter(0.5); + nose.setName("Nose Fairing"); + stage.addChild(nose); + + BodyTube body = new BodyTube(1.0,1.0,0.01); + body.setName("Body Tube"); + stage.addChild(body); + + Transition tail = new Transition(); + tail.setType(Shape.CONICAL); + tail.setForeRadius(1.0); + tail.setLength(1.0); + tail.setAftRadius(0.5); + // slope = .5/1.0 = 0.5 + tail.setName("Tail Cone"); + stage.addChild(tail); + + createFinOnEllipsoidNose(nose); + createFinOnTube(body); + createFinOnConicalTransition(tail); + + rocket.enableEvents(); + return rocket; + } + + + private void createFinOnEllipsoidNose(NoseCone nose){ + FreeformFinSet fins = new FreeformFinSet(); + fins.setName("test-freeform-finset"); + fins.setFinCount(1); + fins.setAxialOffset( AxialMethod.TOP, 0.02); + final Coordinate[] points = { + new Coordinate( 0.0, 0.0), + new Coordinate( 0.4, 1.0), + new Coordinate( 0.6, 1.0), + new Coordinate( 0.8, 0.9) + }; + fins.setPoints(points); + + nose.addChild(fins); + } + + private void createFinOnTube(final BodyTube body){ + // This is a trapezoid: + // - Height: 1 + // - Root Chord: 1 + // - Tip Chord: 1/2 + // - Sweep: 1/2 + // It can be decomposed into a triangle followed by a rectangle + // +--+ + // /. |x + // / . | + // +=====+ FreeformFinSet fins = new FreeformFinSet(); fins.setFinCount(1); - Coordinate[] points = new Coordinate[] { + Coordinate[] points = new Coordinate[]{ new Coordinate(0, 0), - new Coordinate(0, 1), - new Coordinate(.5, 1), + new Coordinate(0.5, 1), + new Coordinate(1, 1), new Coordinate(1, 0) }; fins.setPoints(points); - Coordinate coords = fins.getCG(); - assertEquals(0.75, fins.getFinArea(), 0.001); - assertEquals(0.3889, coords.x, 0.001); - assertEquals(0.4444, coords.y, 0.001); + fins.setAxialOffset( AxialMethod.BOTTOM, 0.0); + + body.addChild(fins); } - + + private void createFinOnConicalTransition(final Transition body) { + // ----+ (1) + // (0) ----- | + // ---+ | + // ----- | + // ----+ (2) + FreeformFinSet fins = new FreeformFinSet(); + fins.setName("test-freeform-finset"); + fins.setFinCount(1); + fins.setThickness(0.005); + fins.setAxialOffset( AxialMethod.TOP, 0.4); + Coordinate[] initPoints = new Coordinate[] { + new Coordinate( 0.0, 0.0), + new Coordinate( 0.4, 0.2), + new Coordinate( 0.4,-0.2) + }; + fins.setPoints(initPoints); + + body.addChild(fins); + } + + // ==================== Test Methods ==================== @Test - public void testFreeformCGComputationTrapezoidExtraPoints() throws Exception { + public void testConvertEllipticalToFreeform() { + final FreeformFinSet finSet = testFreeformConvert(new EllipticalFinSet()); + + assertEquals( finSet.getName(), "test-convert-finset"); + + assertEquals(1.1, finSet.getBaseRotation(), EPSILON); + assertEquals(0.001, finSet.getCantAngle(), EPSILON); + assertTrue(finSet.isCGOverridden()); + assertTrue(finSet.isMassOverridden()); + assertEquals(Color.BLACK, finSet.getColor()); + assertEquals("cmt", finSet.getComment()); + assertEquals(CrossSection.ROUNDED, finSet.getCrossSection()); + assertEquals(5, finSet.getFinCount()); + assertEquals(Finish.ROUGH, finSet.getFinish()); + assertEquals(LineStyle.DASHDOT, finSet.getLineStyle()); + { + final Material mat = finSet.getMaterial(); + assertEquals(Type.BULK, mat.getType()); + assertEquals("test-material", mat.getName()); + assertEquals(0.1, mat.getDensity(), EPSILON); + assertTrue(mat.isUserDefined()); + } + assertEquals(0.012, finSet.getOverrideCGX(), EPSILON); + assertEquals(0.0123, finSet.getOverrideMass(), EPSILON); + assertTrue(finSet.getOverrideSubcomponents()); + + assertEquals(AxialMethod.ABSOLUTE, finSet.getAxialMethod()); + assertEquals(0.1, finSet.getAxialOffset(), EPSILON); + assertEquals(0.01, finSet.getTabHeight(), EPSILON); + assertEquals( 0.02, finSet.getTabLength(), EPSILON); + assertEquals( AxialMethod.BOTTOM, finSet.getTabOffsetMethod()); + assertEquals( -0.015, finSet.getTabOffset(), EPSILON); + assertEquals( 0.005, finSet.getThickness(), EPSILON); + } + + @Test + public void testConvertTrapezoidToFreeform() { + final FreeformFinSet finSet = testFreeformConvert(new TrapezoidFinSet()); + + assertEquals( finSet.getName(), "test-convert-finset"); + + assertEquals(1.1, finSet.getBaseRotation(), EPSILON); + assertEquals(0.001, finSet.getCantAngle(), EPSILON); + assertTrue(finSet.isCGOverridden()); + assertTrue(finSet.isMassOverridden()); + assertEquals(Color.BLACK, finSet.getColor()); + assertEquals("cmt", finSet.getComment()); + assertEquals(CrossSection.ROUNDED, finSet.getCrossSection()); + assertEquals(5, finSet.getFinCount()); + assertEquals(Finish.ROUGH, finSet.getFinish()); + assertEquals(LineStyle.DASHDOT, finSet.getLineStyle()); + { + final Material mat = finSet.getMaterial(); + assertEquals(Type.BULK, mat.getType()); + assertEquals("test-material", mat.getName()); + assertEquals(0.1, mat.getDensity(), EPSILON); + assertTrue(mat.isUserDefined()); + } + assertEquals(0.012, finSet.getOverrideCGX(), EPSILON); + assertEquals(0.0123, finSet.getOverrideMass(), EPSILON); + assertTrue(finSet.getOverrideSubcomponents()); + + assertEquals(AxialMethod.ABSOLUTE, finSet.getAxialMethod()); + assertEquals(0.1, finSet.getAxialOffset(), EPSILON); + assertEquals(0.01, finSet.getTabHeight(), EPSILON); + assertEquals( 0.02, finSet.getTabLength(), EPSILON); + assertEquals( AxialMethod.BOTTOM, finSet.getTabOffsetMethod()); + assertEquals( -0.015, finSet.getTabOffset(), EPSILON); + assertEquals( 0.005, finSet.getThickness(), EPSILON); + + } + + @Test + public void testFreeformCMComputation_trapezoidOnTube() { + final Rocket rkt = createTemplateRocket(); + final BodyTube finMount= (BodyTube)rkt.getChild(0).getChild(1); + final FreeformFinSet fins = (FreeformFinSet)rkt.getChild(0).getChild(1).getChild(0); + + // assert pre-condition: + final Coordinate[] finPoints = fins.getFinPoints(); + assertEquals(4, finPoints.length); + assertEquals(finPoints[0], Coordinate.ZERO); + assertEquals(finPoints[1], new Coordinate(0.5, 1.0)); + assertEquals(finPoints[2], new Coordinate(1.0, 1.0)); + assertEquals(finPoints[3], new Coordinate(1.0, 0.0)); + + final double x0 = fins.getAxialFront(); + assertEquals(0., x0, EPSILON); + assertEquals(1.0, finMount.getRadius(x0), EPSILON); + + // NOTE: this will be relative to the center of the finset -- which is at the center of it's mounted body + final Coordinate coords = fins.getCG(); + assertEquals(0.75, fins.getPlanformArea(), EPSILON); + assertEquals(0.611111, coords.x, EPSILON); + assertEquals(1.444444, coords.y, EPSILON); + } + + @Test + public void testFreeformCMComputation_triangleOnTransition(){ + Rocket rkt = createTemplateRocket(); + final Transition finMount = (Transition)rkt.getChild(0).getChild(2); + FinSet fins = (FinSet)rkt.getChild(0).getChild(2).getChild(0); + + // assert pre-condition: + final Coordinate[] finPoints = fins.getFinPoints(); + assertEquals(3, finPoints.length); + assertEquals(finPoints[0], Coordinate.ZERO); + assertEquals(finPoints[1], new Coordinate(0.4, 0.2)); + assertEquals(finPoints[2], new Coordinate(0.4, -0.2)); + + final double x0 = fins.getAxialFront(); + assertEquals(0.4, x0, EPSILON); + assertEquals(0.8, finMount.getRadius(x0), EPSILON); + + // vv Test target vv + final Coordinate coords = fins.getCG(); + // ^^ Test target ^^ + + // in fin-mount frame coordinates + final double expectedWettedArea = 0.08; + assertEquals(expectedWettedArea, fins.getPlanformArea(), EPSILON); + assertEquals(0.266666, coords.x, EPSILON); + assertEquals(0.8, coords.y, EPSILON); + + { // now, add a tab + fins.setTabOffsetMethod(AxialMethod.TOP); + fins.setTabOffset(0.1); + fins.setTabHeight(0.2); + fins.setTabLength(0.2); + + // fin is a simple trapezoid against a linearly changing body... + // height is set s.t. the tab trailing edge height == 0 + final double expectedTabArea = (fins.getTabHeight())*3/4 * fins.getTabLength(); + final double expectedTotalVolume = (expectedWettedArea + expectedTabArea)*fins.getThickness(); + assertEquals("Calculated fin volume is wrong: ", expectedTotalVolume, fins.getComponentVolume(), EPSILON); + + Coordinate tcg = fins.getCG(); // relative to parent. also includes fin tab CG. + assertEquals("Calculated fin centroid is wrong! ", 0.245454, tcg.x, EPSILON); + assertEquals("Calculated fin centroid is wrong! ", 0.75303, tcg.y, EPSILON); + } + } + + + @Test + public void testFreeformCMComputation_triangleOnEllipsoid(){ + final Rocket rkt = createTemplateRocket(); + final Transition body = (Transition) rkt.getChild(0).getChild(0); + final FinSet fins = (FreeformFinSet) rkt.getChild(0).getChild(0).getChild(0); + + // assert preconditions + assertEquals(Shape.ELLIPSOID, body.getType()); + assertEquals(1.0, body.getLength(), EPSILON); + + assertEquals(0.8, fins.getLength(), EPSILON); + final Coordinate[] finPoints = fins.getFinPoints(); + assertEquals(4, finPoints.length); + assertEquals(finPoints[0], Coordinate.ZERO); + assertEquals(finPoints[1], new Coordinate(0.4, 1.0)); + assertEquals(finPoints[2], new Coordinate(0.6, 1.0)); + assertEquals(finPoints[3], new Coordinate(0.8, 0.78466912)); + // [1] [2] + // +======+ + // / \ [3] + // / ---+---- + // / -------- + // [0] / -------- + // ---+---- + // + // [0] ( 0.0, 0.0) + // [1] ( 0.4, 1.0) + // [2] ( 0.6, 1.0) + // [3] ( 0.8, 0.7847) + + final double expectedWettedArea = 0.13397384; + final double actualWettedArea = fins.getPlanformArea(); + Coordinate wcg = fins.getCG(); // relative to parent + assertEquals("Calculated fin area is wrong: ", expectedWettedArea, actualWettedArea, EPSILON); + assertEquals("Calculated fin centroid is wrong! ", 0.4793588, wcg.x, EPSILON); + assertEquals("Calculated fin centroid is wrong! ", 0.996741, wcg.y, EPSILON); + } + + + @Test + public void testFreeformCMComputationTrapezoidExtraPoints() { + final Rocket rkt = createTemplateRocket(); + final FreeformFinSet fins = (FreeformFinSet)rkt.getChild(0).getChild(1).getChild(0); + // This is the same trapezoid as previous free form, but it has // some extra points along the lines. - FreeformFinSet fins = new FreeformFinSet(); - fins.setFinCount(1); - Coordinate[] points = new Coordinate[] { - new Coordinate(0, 0), - new Coordinate(0, .5), - new Coordinate(0, 1), - new Coordinate(.25, 1), - new Coordinate(.5, 1), - new Coordinate(.75, .5), - new Coordinate(1, 0) + Coordinate[] points = new Coordinate[]{ + new Coordinate(0.0, 0.0), // original + new Coordinate(0.5, 1.0), // original + new Coordinate(0.6, 1.0), + new Coordinate(0.8, 1.0), + new Coordinate(1.0, 1.0), // original + new Coordinate(1.0, 0.6), + new Coordinate(1.0, 0.0) // original }; fins.setPoints(points); + Coordinate coords = fins.getCG(); - assertEquals(0.75, fins.getFinArea(), 0.001); - assertEquals(0.3889, coords.x, 0.001); - assertEquals(0.4444, coords.y, 0.001); + assertEquals(0.75, fins.getPlanformArea(), EPSILON); + assertEquals(0.611111, coords.x, EPSILON); + assertEquals(1.444444, coords.y, EPSILON); } @Test - public void testFreeformCGComputationAdjacentPoinst() throws Exception { + public void testFreeformCMComputationAdjacentPoints() { + Rocket rkt = createTemplateRocket(); + FreeformFinSet fins = (FreeformFinSet)rkt.getChild(0).getChild(1).getChild(0); + // This is the same trapezoid as previous free form, but it has // some extra points which are very close to previous points. // in particular for points 0 & 1, // y0 + y1 is very small. - FreeformFinSet fins = new FreeformFinSet(); - fins.setFinCount(1); + final double PERMUTATION = 1e-15; Coordinate[] points = new Coordinate[] { - new Coordinate(0, 0), - new Coordinate(0, 1E-15), - new Coordinate(0, 1), - new Coordinate(1E-15, 1), - new Coordinate(.5, 1), - new Coordinate(.5, 1 - 1E-15), - new Coordinate(1, 1E-15), - new Coordinate(1, 0) + new Coordinate(0.0, 0.0), // original + new Coordinate(0, PERMUTATION), + new Coordinate(0.5, 1.0), // original + new Coordinate(0.5 + PERMUTATION, 1.0), + new Coordinate(1.0, 1.0), // original + new Coordinate(1.0, PERMUTATION), + new Coordinate(1.0, 0.0) // original }; fins.setPoints(points); + Coordinate coords = fins.getCG(); - assertEquals(0.75, fins.getFinArea(), 0.001); - assertEquals(0.3889, coords.x, 0.001); - assertEquals(0.4444, coords.y, 0.001); + assertEquals(0.75, fins.getPlanformArea(), EPSILON); + assertEquals(0.611111, coords.x, EPSILON); + assertEquals(1.444444, coords.y, EPSILON); } @Test - public void testFreeformFinAddPoint() throws Exception { - FreeformFinSet fin = new FreeformFinSet(); - fin.setFinCount(1); - fin.setFinCount(1); - Coordinate[] points = new Coordinate[] { - new Coordinate(0, 0), - new Coordinate(0.5, 1.0), - new Coordinate(1.0, 1.0), - new Coordinate(1, 0) - }; - fin.setPoints(points); + public void testFreeformFinAddPoint() { + Rocket rkt = createTemplateRocket(); + FreeformFinSet fin = (FreeformFinSet)rkt.getChild(0).getChild(1).getChild(0); + assertEquals(4, fin.getPointCount()); // +--+ @@ -121,18 +424,443 @@ public class FreeformFinSetTest extends BaseTestCase { assertEquals(1.1,added.x, 0.1); assertEquals(0.8, added.y, 0.1); } - + + @Test + public void testSetPoint_firstPoint_boundsCheck() throws IllegalFinPointException { + // more transitions trigger more complicated positioning math: + Rocket rkt = createTemplateRocket(); + FreeformFinSet fins = (FreeformFinSet) rkt.getChild(0).getChild(0).getChild(0); + final int startIndex = 0; + final int lastIndex = fins.getPointCount()-1; + // assert pre-conditions + assertEquals( 1, fins.getFinCount()); + assertEquals( 3, lastIndex); + + fins.setPoint( startIndex, -1, -1); + final Coordinate act_p_0 = fins.getFinPoints()[0]; + { // first point x is restricted to the front of the parent body: + assertEquals( 0.0, act_p_0.x, EPSILON); + assertEquals( AxialMethod.TOP, fins.getAxialMethod() ); + assertEquals( 0.0, fins.getAxialOffset(), EPSILON); + } + + {// first point y is restricted to the body + assertEquals( 0.0, act_p_0.y, EPSILON); + } + } + + @Test + public void testSetFirstPoint() throws IllegalFinPointException { + // more transitions trigger more complicated positioning math: + final Rocket rkt = createTemplateRocket(); + final Transition finMount = (Transition) rkt.getChild(0).getChild(2); + final FreeformFinSet fins = (FreeformFinSet) rkt.getChild(0).getChild(2).getChild(0); + final Coordinate[] initialPoints = fins.getFinPoints(); + + // assert pre-conditions: + assertEquals(0.4, fins.getAxialFront(), EPSILON); + assertEquals(0.4, fins.getLength(), EPSILON); + assertEquals(initialPoints[0], Coordinate.ZERO); + assertEquals(initialPoints[1], new Coordinate(0.4, 0.2)); + assertEquals(initialPoints[2], new Coordinate(0.4, -0.2)); + assertEquals(1.0, finMount.getLength(), EPSILON); + assertEquals(0.8, finMount.getRadius(fins.getAxialFront()), EPSILON); + + // for a fin at these positions: + final AxialMethod[] inputMethods = { AxialMethod.TOP, AxialMethod.TOP, AxialMethod.MIDDLE, AxialMethod.MIDDLE, AxialMethod.BOTTOM, AxialMethod.BOTTOM}; + final double[] inputOffsets = { 0.1, 0.1, 0.0, 0.0, 0.0, 0.0}; + + // move first by this delta... + final double[] xDelta = { 0.2, -0.2, 0.1, -0.1, 0.1, -0.1}; + + // and check against these expected values: + final double[] expectedFinStartx = { 0.3, 0.0, 0.4, 0.2, 0.7, 0.5}; + final double[] expectedFinStarty = { 0.85, 1.0, 0.8, 0.9, 0.65, 0.75}; + final double[] expectedFinLength = { 0.2, 0.5, 0.3, 0.5, 0.3, 0.5}; + final double[] expectedFinOffset = { 0.3, 0.0, 0.05, -0.05, 0.0, 0.0}; + final double[] expectedMidpointOffset = { 0.2, 0.5, 0.3, 0.5, 0.3, 0.5}; + final double[] expectedFinalPointOffset = { 0.2, 0.5, 0.3, 0.5, 0.3, 0.5}; + + for( int caseIndex=0; caseIndex < inputMethods.length; ++caseIndex ){ + // this builds a string to describe this particular iteration, and provide useful output, should it fail. + StringBuilder buf = new StringBuilder(); + buf.append(String.format("\n## For Case #%d: [%s]@%4.2f \n", caseIndex, inputMethods[caseIndex].name(), inputOffsets[caseIndex])); + buf.append(String.format(" >> setting initial point to: ( %6.4f, %6.4f) \n", xDelta[caseIndex], 0.0f)); + final String dump = buf.toString(); + + fins.setAxialOffset( inputMethods[caseIndex], inputOffsets[caseIndex]); + fins.setPoints(initialPoints); + + // vvvv function under test vvvv + fins.setPoint( 0, xDelta[caseIndex], 0.1f); + // ^^^^ function under test ^^^^ + + //System.err.println(String.format("@[%d]:", caseIndex)); + //System.err.println(fins.toDebugDetail()); + + assertEquals("Fin front not updated as expected..."+dump, expectedFinStartx[caseIndex], fins.getAxialFront(), EPSILON); + assertEquals("Fin front not updated as expected..."+dump, expectedFinStarty[caseIndex], fins.getFinFront().y, EPSILON); + + final Coordinate[] postPoints = fins.getFinPoints(); + assertEquals("Middle fin point has moved!: "+dump, expectedMidpointOffset[caseIndex], postPoints[1].x, EPSILON); + assertEquals("Final fin point has moved!: "+dump, expectedFinalPointOffset[caseIndex], postPoints[2].x, EPSILON); + + assertEquals("Fin offset not updated as expected..."+dump, expectedFinOffset[caseIndex], fins.getAxialOffset(), EPSILON); + assertEquals("Fin length not updated as expected..."+dump, expectedFinLength[caseIndex], fins.getLength(), EPSILON); + } + } + + @Test + public void testSetLastPoint() { + final Rocket rkt = createTemplateRocket(); + Transition finMount = (Transition) rkt.getChild(0).getChild(2); + FreeformFinSet fins = (FreeformFinSet) rkt.getChild(0).getChild(2).getChild(0); + final Coordinate[] initialPoints = fins.getFinPoints(); + + // assert pre-conditions: + assertEquals(0.4, fins.getLength(), EPSILON); + assertEquals(0.4, fins.getAxialFront(), EPSILON); + assertEquals(initialPoints[0], Coordinate.ZERO); + assertEquals(initialPoints[1], new Coordinate(0.4, 0.2)); + assertEquals(initialPoints[2], new Coordinate(0.4, -0.2)); + assertEquals(1.0, finMount.getLength(), EPSILON); + assertEquals(0.8, finMount.getRadius(fins.getAxialFront()), EPSILON); + + final int lastIndex = initialPoints.length - 1; + + // set to these positions + final AxialMethod[] inputMethods = {AxialMethod.TOP, AxialMethod.TOP, AxialMethod.MIDDLE, AxialMethod.MIDDLE, AxialMethod.BOTTOM, AxialMethod.BOTTOM}; + final double[] inputOffsets = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + + // set first point to this location... + final double[] xDelta= { 0.1, -0.1, 0.1, -0.1, 0.1, -0.1}; + + // and check against these expected values: + final double[] expectedFinStartx = { 0.0, 0.0, 0.3, 0.3, 0.6, 0.6}; + final double[] expectedFinStarty = { 1.0, 1.0, 0.85, 0.85, 0.7, 0.7}; + final double[] expectedFinOffset = { 0.0, 0.0, 0.05, -0.05, 0.0, -0.1}; + final double[] expectedFinalPointOffset = { 0.5, 0.3, 0.5, 0.3, 0.4, 0.3}; + + for( int caseIndex=0; caseIndex < inputMethods.length; ++caseIndex ){ + final double xRequest = initialPoints[lastIndex].x + xDelta[caseIndex]; + final double yRequest = Double.NaN; // irrelevant; will be clamped to the body regardless + + // this builds a string to describe this particular iteration, and provide useful output, should it fail. + StringBuilder buf = new StringBuilder(); + buf.append(String.format("\n## For Case #%d: [%s]@%4.2f \n", caseIndex, inputMethods[caseIndex].name(), inputOffsets[caseIndex])); + buf.append(String.format(" >> setting last point to: ( %6.4f, %6.4f) \n", xRequest, yRequest)); + final String dump = buf.toString(); + + fins.setAxialOffset( inputMethods[caseIndex], inputOffsets[caseIndex]); + fins.setPoints(initialPoints); + + // vvvv function under test vvvv + fins.setPoint( lastIndex, xRequest, yRequest); + // ^^^^ function under test ^^^^ + + // System.err.println(String.format("@[%d]:", caseIndex)); + // System.err.println(fins.toDebugDetail()); + + assertEquals("Fin offset not updated as expected..."+dump, expectedFinOffset[caseIndex], fins.getAxialOffset(), EPSILON); + assertEquals("Fin front not updated as expected..."+dump, expectedFinStartx[caseIndex], fins.getAxialFront(), EPSILON); + assertEquals("Fin front not updated as expected..."+dump, expectedFinStarty[caseIndex], fins.getFinFront().y, EPSILON); + + final Coordinate[] postPoints = fins.getFinPoints(); + assertEquals("Middle fin point has moved!: "+dump, initialPoints[1].x, postPoints[1].x, EPSILON); + assertEquals("Final fin point has moved!: "+dump, expectedFinalPointOffset[caseIndex], postPoints[2].x, EPSILON); + assertEquals("Fin length not updated as expected..."+dump, expectedFinalPointOffset[caseIndex], fins.getLength(), EPSILON); + } + } + + @Test + public void testSetFirstPoint_testNonIntersection() { + final Rocket rkt = createTemplateRocket(); + FreeformFinSet fins = (FreeformFinSet) rkt.getChild(0).getChild(2).getChild(0); + + assertEquals( 1, fins.getFinCount()); + final int lastIndex = fins.getPointCount()-1; + assertEquals( 2, lastIndex); + final double initialOffset = fins.getAxialOffset(); + assertEquals( 0.4, initialOffset, EPSILON); // pre-condition + + final double attemptedDelta = 0.6; + fins.setPoint( 0, attemptedDelta, 0); + // fin offset: 0.4 -> 0.59 (just short of prev fin end) + // fin end: 0.4 ~> min root chord + + assertEquals(fins.getFinPoints()[ 0], Coordinate.ZERO); + + // setting the first point actually offsets the whole fin by that amount: + final double expFinOffset = 0.79; + assertEquals("Resultant fin offset does not match!", expFinOffset, fins.getAxialOffset(), EPSILON); + + // SHOULD NOT CHANGE (in this case): + Coordinate actualLastPoint = fins.getFinPoints()[ lastIndex]; + assertEquals("last point did not adjust correctly: ", FreeformFinSet.MIN_ROOT_CHORD, actualLastPoint.x, EPSILON); + assertEquals("last point did not adjust correctly: ", -0.005, actualLastPoint.y, EPSILON); // magic number + assertEquals("New fin length is wrong: ", FreeformFinSet.MIN_ROOT_CHORD, fins.getLength(), EPSILON); + } + + + @Test + public void testSetLastPoint_TubeBody() throws IllegalFinPointException { + Rocket rkt = createTemplateRocket(); + + // combine the simple case with the complicated to ensure that the simple case is + // flagged, tested, and debugged before running the more complicated case... + { // setting points on a Tube Body is the simpler case. Test this first: + FreeformFinSet fins = (FreeformFinSet) rkt.getChild(0).getChild(1).getChild(0); + + // verify preconditions + assertEquals(AxialMethod.BOTTOM, fins.getAxialMethod()); + assertEquals(0.0, fins.getAxialOffset(), EPSILON); + + // last point is restricted to the body + final int lastIndex = fins.getPointCount()-1; + final Coordinate expectedFinalPoint = new Coordinate( 1.0, 0.0, 0.0); + + // vvvv function under test vvvv + fins.setPoint( lastIndex, 10.0, Double.NaN); + // ^^^^ function under test ^^^^ + + Coordinate actualFinalPoint = fins.getFinPoints()[lastIndex]; + assertEquals( expectedFinalPoint.x, actualFinalPoint.x, EPSILON); + assertEquals( expectedFinalPoint.y, actualFinalPoint.y, EPSILON); + } + + { // a transitions will trigger more complicated positioning math: + FreeformFinSet fins = (FreeformFinSet) rkt.getChild(0).getChild(2).getChild(0); + final int lastIndex = fins.getPointCount()-1; + + Coordinate expectedLastPoint; + Coordinate actualLastPoint; + { // this is where the point starts off at: + actualLastPoint = fins.getFinPoints()[lastIndex]; + assertEquals( 0.4, actualLastPoint.x, EPSILON); + assertEquals( -0.2, actualLastPoint.y, EPSILON); + } + + { // (1): move point within bounds + // move last point, and verify that its y-value is still clamped to the body ( at the new location) + expectedLastPoint = new Coordinate( 0.6, -0.3, 0.0); + fins.setPoint(lastIndex, 0.6, 0.0); // w/ incorrect y-val. The function should correct the y-value as above. + + actualLastPoint = fins.getFinPoints()[lastIndex]; + assertEquals( expectedLastPoint.x, actualLastPoint.x, EPSILON); + assertEquals( expectedLastPoint.y, actualLastPoint.y, EPSILON); + } + } + } + + @Test + public void testSetPoint_otherPoint() throws IllegalFinPointException { + // combine the simple case with the complicated to ensure that the simple case is flagged, tested, and debugged before running the more complicated case... + { // setting points on a Tube Body is the simpler case. Test this first: + Rocket rkt = createTemplateRocket(); + FreeformFinSet fins = (FreeformFinSet) rkt.getChild(0).getChild(2).getChild(0); + + // all points are restricted to be outside the parent body: + Coordinate exp_pt = fins.getFinPoints()[0]; + fins.setPoint(0, -0.6, 0); + Coordinate act_pt = fins.getFinPoints()[0]; + assertEquals( exp_pt.x, act_pt.x, EPSILON); + // the last point is already clamped to the body; It should remain so. + assertEquals( 0.0, act_pt.y, EPSILON); + } + { // more transitions trigger more complicated positioning math: + Rocket rkt = createTemplateRocket(); + FreeformFinSet fins = (FreeformFinSet) rkt.getChild(0).getChild(2).getChild(0); + assertEquals( 1, fins.getFinCount()); + + Coordinate act_p_l; + Coordinate exp_p_l; + + final int testIndex = 1; + // move point, and verify that it is coerced to be outside the parent body: + exp_p_l = new Coordinate( 0.2, -0.1, 0.0); + fins.setPoint( testIndex, 0.2, -0.2); // incorrect y-val. The function should correct the y-value to above. + + act_p_l = fins.getFinPoints()[ testIndex ]; + assertEquals( exp_p_l.x, act_p_l.x, EPSILON); + assertEquals( exp_p_l.y, act_p_l.y, EPSILON); + } + } + + @Test + public void testSetOffset_triggerClampCorrection() { + // test correction of last point due to moving entire fin: + Rocket rkt = createTemplateRocket(); + Transition body = (Transition) rkt.getChild(0).getChild(2); + FreeformFinSet fins = (FreeformFinSet) rkt.getChild(0).getChild(2).getChild(0); + + final int lastIndex = fins.getPointCount()-1; + + final double initXOffset = fins.getAxialOffset(); + assertEquals( 0.4, initXOffset, EPSILON); // pre-condition + final double newXTop = 0.85; + final double expFinOffset = 0.6; + final double expLength = body.getLength() - expFinOffset; + fins.setAxialOffset( AxialMethod.TOP, newXTop); + // fin start: 0.4 => 0.8 [body] + // fin end: 0.8 => 0.99 [body] + assertEquals( expFinOffset, fins.getAxialOffset(), EPSILON); + assertEquals( expLength, fins.getLength(), EPSILON); + + // SHOULD DEFINITELY CHANGE + Coordinate actualLastPoint = fins.getFinPoints()[ lastIndex]; + assertEquals( 0.4, actualLastPoint.x, EPSILON); + assertEquals( -0.2, actualLastPoint.y, EPSILON); + } + + @Test + public void testComputeCM_mountlesFin(){ + // This is a trapezoid. Height 1, root 1, tip 1/2 no sweep. + // It can be decomposed into a rectangle followed by a triangle + // +---+ + // | \ + // | \ + // +------+ + FreeformFinSet fins = new FreeformFinSet(); + fins.setFinCount(1); + Coordinate[] points = new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(0, 1), + new Coordinate(.5, 1), + new Coordinate(1, 0) + }; + fins.setPoints(points); + Coordinate coords = fins.getCG(); + assertEquals(0.75, fins.getPlanformArea(), EPSILON); + + assertEquals(0.388889, coords.x, EPSILON); + assertEquals(0.444444, coords.y, EPSILON); + } + + @Test + public void testTranslatePoints(){ + final Rocket rkt = new Rocket(); + final AxialStage stg = new AxialStage(); + rkt.addChild(stg); + BodyTube body = new BodyTube(2.0, 0.01); + stg.addChild(body); + + // Fin length = 1 + // Body Length = 2 + // +--+ + // / | + // / | + // +---+-----+---+ + // + FreeformFinSet fins = new FreeformFinSet(); + fins.setFinCount(1); + Coordinate[] initPoints = new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(0.5, 1), + new Coordinate(1, 1), + new Coordinate(1, 0) + }; + fins.setPoints(initPoints); + body.addChild(fins); + + final AxialMethod[] pos={AxialMethod.TOP, AxialMethod.MIDDLE, AxialMethod.MIDDLE, AxialMethod.BOTTOM}; + final double[] offs = {1.0, 0.0, 0.4, -0.2}; + final double[] expOffs = {1.0, 0.5, 0.9, 0.8}; + for( int caseIndex=0; caseIndex < pos.length; ++caseIndex ){ + fins.setAxialOffset( pos[caseIndex], offs[caseIndex]); + final double x_delta = fins.getAxialOffset(AxialMethod.TOP); + + Coordinate actualPoints[] = fins.getFinPoints(); + + final String rawPointDescr = "\n"+fins.toDebugDetail().toString()+"\n>> axial offset: "+x_delta; + + Coordinate[] displayPoints = FinSet.translatePoints( actualPoints, x_delta, 0); + for( int index=0; index < displayPoints.length; ++index){ + assertEquals(String.format("Bad Fin Position.x (%6.2g via:%s at point: %d) %s\n",offs[caseIndex], pos[caseIndex].name(), index, rawPointDescr), + (initPoints[index].x + expOffs[caseIndex]), displayPoints[index].x, EPSILON); + assertEquals(String.format("Bad Fin Position.y (%6.2g via:%s at point: %d) %s\n",offs[caseIndex], pos[caseIndex].name(), index, rawPointDescr), + initPoints[index].y, displayPoints[index].y, EPSILON); + } + } + + } + + @Test + public void testForNonIntersection() { + final Rocket rkt = new Rocket(); + final AxialStage stg = new AxialStage(); + rkt.addChild(stg); + BodyTube body = new BodyTube(2.0, 0.01); + stg.addChild(body); + + // Fin length = 1 + // Body Length = 2 + // +--+ + // / | + // / | + // +---+-----+---+ + // + FreeformFinSet fins = new FreeformFinSet(); + fins.setFinCount(1); + Coordinate[] initPoints = new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(0.5, 1), + new Coordinate(1, 1), + new Coordinate(1, 0) + }; + fins.setPoints(initPoints); + body.addChild(fins); + + assertFalse( " Fin detects false positive intersection in fin points: ", fins.intersects()); + } + + @Test + public void testForIntersection() { + final Rocket rkt = new Rocket(); + final AxialStage stg = new AxialStage(); + rkt.addChild(stg); + BodyTube body = new BodyTube(2.0, 0.01); + stg.addChild(body); + // + // An obviously intersecting fin: + // [2] +-----+ [1] + // \ / + // \ / + // X + // / \ + // [0] / \ [3] + // +---+-----+---+ + // = +x => + FreeformFinSet fins = new FreeformFinSet(); + fins.setFinCount(1); + Coordinate[] initPoints = new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(1, 1), + new Coordinate(0, 1), + new Coordinate(1, 0) + }; + // this line throws an exception? + fins.setPoints(initPoints); + body.addChild(fins); + + // this *already* has detected the intersection, and aborted... + Coordinate p1 = fins.getFinPoints()[1]; + // ... which makes a rather hard-to-test functionality... + assertThat( "Fin Set failed to detect an intersection! ", p1.x, not(equalTo(initPoints[1].x))); + assertThat( "Fin Set failed to detect an intersection! ", p1.y, not(equalTo(initPoints[1].y))); + } + + @Test public void testWildmanVindicatorShape() throws Exception { // This fin shape is similar to the aft fins on the Wildman Vindicator. // A user noticed that if the y values are similar but not equal, - // the compuation of CP was incorrect because of numerical instability. + // the computation of CP was incorrect because of numerical instability. // // +-----------------+ // \ \ // \ \ - // + \ - // / \ + // + \ +x + // / \ <=+ // +---------------------+ // FreeformFinSet fins = new FreeformFinSet(); @@ -146,9 +874,11 @@ public class FreeformFinSetTest extends BaseTestCase { }; fins.setPoints(points); Coordinate coords = fins.getCG(); - assertEquals(0.00130, fins.getFinArea(), 0.00001); - assertEquals(0.03423, coords.x, 0.00001); - assertEquals(0.01427, coords.y, 0.00001); + + assertEquals(0.00130708, fins.getPlanformArea(), EPSILON); + + assertEquals(0.03423168, coords.x, EPSILON); + assertEquals(0.01427544, coords.y, EPSILON); BodyTube bt = new BodyTube(); bt.addChild(fins); @@ -160,30 +890,159 @@ public class FreeformFinSetTest extends BaseTestCase { //System.out.println(forces); assertEquals(0.023409, forces.getCP().x, 0.0001); } + + @Test + public void testGenerateBodyPointsOnBodyTube(){ + final Rocket rkt = createTemplateRocket(); + final FreeformFinSet fins = (FreeformFinSet) rkt.getChild(0).getChild(1).getChild(0); + + final Coordinate[] finPoints = fins.getFinPoints(); + final Coordinate[] finPointsFromBody = FinSet.translatePoints( finPoints, 0.0, fins.getFinFront().y); + + { // body points (relative to body) + final Coordinate[] bodyPoints = fins.getBodyPoints(); + + assertEquals("Method should only generate minimal points for a conical transition fin body! ", 2, bodyPoints.length ); + assertEquals("incorrect body points! ", finPointsFromBody[0].x, bodyPoints[0].x, EPSILON); + assertEquals("incorrect body points! ", finPointsFromBody[0].y, bodyPoints[0].y, EPSILON); + assertEquals("incorrect body points! ", finPointsFromBody[finPoints.length-1].x, bodyPoints[1].x, EPSILON); + assertEquals("incorrect body points! ", finPointsFromBody[finPoints.length-1].y, bodyPoints[1].y, EPSILON); + } + { // root points (relative to fin-front) + final Coordinate[] rootPoints = fins.getRootPoints(); + + assertEquals("Method should only generate minimal points for a conical transition fin body! ", 2, rootPoints.length ); + assertEquals("incorrect body points! ", finPoints[0].x, rootPoints[0].x, EPSILON); + assertEquals("incorrect body points! ", finPoints[0].y, rootPoints[0].y, EPSILON); + assertEquals("incorrect body points! ", finPoints[finPoints.length-1].x, rootPoints[1].x, EPSILON); + assertEquals("incorrect body points! ", finPoints[finPoints.length-1].y, rootPoints[1].y, EPSILON); + } + } + + @Test + public void testGenerateBodyPointsOnConicalTransition(){ + final Rocket rkt = createTemplateRocket(); + final FreeformFinSet fins = (FreeformFinSet) rkt.getChild(0).getChild(2).getChild(0); + + final Coordinate finFront = fins.getFinFront(); + final Coordinate[] finPoints = fins.getFinPoints(); + final Coordinate[] finPointsFromBody = FinSet.translatePoints( finPoints, finFront.x, finFront.y); + + { // body points (relative to body) + final Coordinate[] bodyPoints = fins.getBodyPoints(); + + assertEquals("Method should only generate minimal points for a conical transition fin body! ", 2, bodyPoints.length ); + assertEquals("incorrect body points! ", finPointsFromBody[0].x, bodyPoints[0].x, EPSILON); + assertEquals("incorrect body points! ", finPointsFromBody[0].y, bodyPoints[0].y, EPSILON); + assertEquals("incorrect body points! ", finPointsFromBody[finPoints.length-1].x, bodyPoints[1].x, EPSILON); + assertEquals("incorrect body points! ", finPointsFromBody[finPoints.length-1].y, bodyPoints[1].y, EPSILON); + } + { // body points (relative to root) + final Coordinate[] rootPoints = fins.getRootPoints(); + + assertEquals("Method should only generate minimal points for a conical transition fin body! ", 2, rootPoints.length ); + assertEquals("incorrect body points! ", finPoints[0].x, rootPoints[0].x, EPSILON); + assertEquals("incorrect body points! ", finPoints[0].y, rootPoints[0].y, EPSILON); + assertEquals("incorrect body points! ", finPoints[finPoints.length-1].x, rootPoints[1].x, EPSILON); + assertEquals("incorrect body points! ", finPoints[finPoints.length-1].y, rootPoints[1].y, EPSILON); + } + } @Test - public void testFreeFormCGWithNegativeY() throws Exception { - // This particular fin shape is currently not allowed in OR since the y values are negative - // however, it is possible to convert RockSim files and end up with fins which - // have negative y values. - + public void testGenerateBodyPointsOnEllipsoidNose(){ + final Rocket rocket = createTemplateRocket(); + final Transition body = (Transition)rocket.getChild(0).getChild(0); + final FinSet fins = (FreeformFinSet) body.getChild(0); + + final Coordinate finFront = fins.getFinFront(); + final Coordinate[] finPoints = fins.getFinPoints(); + // translate from fin-frame to body-frame + final Coordinate[] finPointsFromBody = FinSet.translatePoints( fins.getFinPoints(), finFront.x, finFront.y ); + final Coordinate expectedStartPoint = finPointsFromBody[0]; + final Coordinate expectedEndPoint = finPointsFromBody[ finPoints.length-1]; + + { // body points (relative to body) + final Coordinate[] bodyPoints = fins.getBodyPoints(); + + // trivial, and uninteresting: + assertEquals("incorrect body points! ", expectedStartPoint.x, bodyPoints[0].x, EPSILON); + assertEquals("incorrect body points! ", expectedStartPoint.y, bodyPoints[0].y, EPSILON); + + // n.b.: This should match EXACTLY the end point of the fin. (in fin coordinates) + assertEquals("incorrect body points! ", expectedEndPoint.x, bodyPoints[bodyPoints.length-1].x, EPSILON); + assertEquals("incorrect body points! ", expectedEndPoint.y, bodyPoints[bodyPoints.length-1].y, EPSILON); + + {// the tests within this scope is are rather fragile, and may break for reasons other than bugs :( + // the number of points is somewhat arbitrary, but if this test fails, the rest *definitely* will. + assertEquals("Method is generating how many points, in general? ", 101, bodyPoints.length ); + + final int[] testIndices = { 2, 5, 61, 88}; + final double[] expectedX = { 0.036, 0.06, 0.508, 0.724}; + + for( int testCase = 0; testCase < testIndices.length; testCase++){ + final int testIndex = testIndices[testCase]; + assertEquals(String.format("Body points @ %d :: x coordinate mismatch!", testIndex), + expectedX[testCase], bodyPoints[testIndex].x, EPSILON); + assertEquals(String.format("Body points @ %d :: y coordinate mismatch!", testIndex), + body.getRadius(bodyPoints[testIndex].x), bodyPoints[testIndex].y, EPSILON); + } + } + } + { // body points (relative to fin) + final Coordinate[] rootPoints = fins.getRootPoints(); + + // trivial, and uninteresting: + assertEquals("incorrect body points! ", finPoints[0].x, rootPoints[0].x, EPSILON); + assertEquals("incorrect body points! ", finPoints[0].y, rootPoints[0].y, EPSILON); + + // n.b.: This should match EXACTLY the end point of the fin. (in fin coordinates) + assertEquals("incorrect body points! ", finPoints[finPoints.length-1].x, rootPoints[rootPoints.length-1].x, EPSILON); + assertEquals("incorrect body points! ", finPoints[finPoints.length-1].y, rootPoints[rootPoints.length-1].y, EPSILON); + + {// the tests within this scope is are rather fragile, and may break for reasons other than bugs :( + // the number of points is somewhat arbitrary, but if this test fails, the rest *definitely* will. + assertEquals("Method is generating how many points? ", 101, rootPoints.length ); + + final int[] testIndices = { 2, 5, 61, 88}; + final double[] expectedX = { 0.016, 0.04, 0.488, 0.704}; + + for( int testCase = 0; testCase < testIndices.length; testCase++){ + final int testIndex = testIndices[testCase]; + assertEquals(String.format("Root points @ %d :: x coordinate mismatch!", testIndex), + expectedX[testCase], rootPoints[testIndex].x, EPSILON); + assertEquals(String.format("Root points @ %d :: y coordinate mismatch!", testIndex), + body.getRadius(rootPoints[testIndex].x + finFront.x) - finFront.y, rootPoints[testIndex].y, EPSILON); + } + } + } + } + + @Test + public void testFreeFormCMWithNegativeY() throws Exception { // A user submitted an ork file which could not be simulated because the fin // was constructed on a tail cone. It so happened that for one pair of points // y_n = - y_(n+1) which caused a divide by zero and resulted in CGx = NaN. - - // This Fin set is constructed to have the same problem. It is a square and rectagle + // + // This Fin set is constructed to have the same problem. It is a square and rectangle // where the two trailing edge corners of the rectangle satisfy y_0 = -y_1 // - // +---------+ - // | | - // | | - // +----+ | - // | | - // | | - // +----+ - + // +=> +x + // 0 1 2 3 + // | | | | + // + // +---------+ - +1 + // | | ^ + // | | | +y + //----+----+ | - 0 + + // | | + // | | + // +----+ - -1 + // | + // | FreeformFinSet fins = new FreeformFinSet(); + fins.setCrossSection( CrossSection.SQUARE ); // to ensure uniform density fins.setFinCount(1); + // fins.setAxialOffset( Position.BOTTOM, 1.0); // ERROR: no parent! Coordinate[] points = new Coordinate[] { new Coordinate(0, 0), new Coordinate(0, 1), @@ -192,108 +1051,26 @@ public class FreeformFinSetTest extends BaseTestCase { new Coordinate(1, -1), new Coordinate(1, 0) }; + fins.setPoints(points); Coordinate coords = fins.getCG(); - assertEquals(3.0, fins.getFinArea(), 0.001); - assertEquals(3.5 / 3.0, coords.x, 0.001); - assertEquals(0.5 / 3.0, coords.y, 0.001); + assertEquals(3.0, fins.getPlanformArea(), EPSILON); + assertEquals(3.5 / 3.0, coords.x, EPSILON); + assertEquals(0.5 / 3.0, coords.y, EPSILON); + fins.setPoints( points); + fins.setFilletRadius( 0.0); + fins.setTabHeight( 0.0); + fins.setMaterial( Material.newMaterial(Type.BULK, "dummy", 1.0, true)); + +// assertEquals( 3.0, fins.getFinWettedArea(), EPSILON); +// +// Coordinate cg = fins.getCG(); +// assertEquals( 1.1666, cg.x, EPSILON); +// assertEquals( 0.1666, cg.y, EPSILON); +// assertEquals( 0.0, cg.z, EPSILON); +// assertEquals( 0.009, cg.weight, EPSILON); + } - - - @Test - public void testFreeformConvert() { - testFreeformConvert(new TrapezoidFinSet()); - testFreeformConvert(new EllipticalFinSet()); - testFreeformConvert(new FreeformFinSet()); - } - - - private void testFreeformConvert(FinSet fin) { - FreeformFinSet converted; - Material mat = Material.newMaterial(Type.BULK, "foo", 0.1, true); - - fin.setBaseRotation(1.1); - fin.setCantAngle(0.001); - fin.setCGOverridden(true); - fin.setColor(Color.BLACK); - fin.setComment("cmt"); - fin.setCrossSection(CrossSection.ROUNDED); - fin.setFinCount(5); - fin.setFinish(Finish.ROUGH); - fin.setLineStyle(LineStyle.DASHDOT); - fin.setMassOverridden(true); - fin.setMaterial(mat); - fin.setOverrideCGX(0.012); - fin.setOverrideMass(0.0123); - fin.setOverrideSubcomponents(true); - fin.setAxialOffset(0.1); - fin.setAxialMethod(AxialMethod.ABSOLUTE); - fin.setTabHeight(0.01); - fin.setTabLength(0.02); - fin.setTabRelativePosition(TabRelativePosition.END); - fin.setTabShift(0.015); - fin.setThickness(0.005); - - - converted = FreeformFinSet.convertFinSet((FinSet) fin.copy()); - - /// what do we want to ACTUALLY compare? - // ComponentCompare.assertSimilarity(fin, converted, true); // deprecated; removed - - - assertEquals(converted.getComponentName(), converted.getName()); - - - // Create test rocket - Rocket rocket = new Rocket(); - AxialStage stage = new AxialStage(); - BodyTube body = new BodyTube(); - - rocket.addChild(stage); - stage.addChild(body); - body.addChild(fin); - rocket.enableEvents(); - - Listener l1 = new Listener("l1"); - rocket.addComponentChangeListener(l1); - - fin.setName("Custom name"); - assertEquals("FinSet listener has not been notified: ", l1.changed, true); - assertEquals(ComponentChangeEvent.NONFUNCTIONAL_CHANGE, l1.changetype); - - - // Create copy - RocketComponent rocketcopy = rocket.copy(); - - Listener l2 = new Listener("l2"); - rocketcopy.addComponentChangeListener(l2); - - FinSet fincopy = (FinSet) rocketcopy.getChild(0).getChild(0).getChild(0); - FreeformFinSet.convertFinSet(fincopy); - - assertTrue("FinSet listener is changed", l2.changed); - assertEquals(ComponentChangeEvent.TREE_CHANGE, - l2.changetype & ComponentChangeEvent.TREE_CHANGE); - - } - - - private static class Listener implements ComponentChangeListener { - private boolean changed = false; - private int changetype = 0; - private final String name; - - public Listener(String name) { - this.name = name; - } - - @Override - public void componentChanged(ComponentChangeEvent e) { - assertFalse("Ensuring listener " + name + " has not been called.", changed); - changed = true; - changetype = e.getType(); - } - } - + } diff --git a/core/test/net/sf/openrocket/rocketcomponent/TrapezoidFinSetTest.java b/core/test/net/sf/openrocket/rocketcomponent/TrapezoidFinSetTest.java index 0267c64ae..96d51332b 100644 --- a/core/test/net/sf/openrocket/rocketcomponent/TrapezoidFinSetTest.java +++ b/core/test/net/sf/openrocket/rocketcomponent/TrapezoidFinSetTest.java @@ -4,11 +4,189 @@ import static org.junit.Assert.assertEquals; import org.junit.Test; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.position.*; + import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.BaseTestCase.BaseTestCase; public class TrapezoidFinSetTest extends BaseTestCase { - + + private static final double EPSILON = 1E-8; + + private Rocket createSimpleTrapezoidalFin() { + final Rocket rkt = new Rocket(); + final AxialStage stg = new AxialStage(); + rkt.addChild(stg); + BodyTube body = new BodyTube(0.2, 0.1); + stg.addChild(body); + TrapezoidFinSet fins = new TrapezoidFinSet(1, 0.06, 0.02, 0.02, 0.05); + // + // sweep= 0.02 | tipChord = 0.02 + // | | | + // | +------+ ---------- + // | / \ + // | / \ height = 0.05 + // | / \ + // / \ + // __________/________________\_____ length == rootChord == 0.06 + // | | + // | | tab height = 0.02 + // | | + // +--------+ tab length = 0.02 + // position = 0.0 via middle + // + // Fin Area = 0.05 * ( (0.2 + 0.06)/2) = 0.0 + // + fins.setAxialOffset(AxialMethod.MIDDLE, 0.0); + fins.setMaterial(Material.newMaterial(Material.Type.BULK, "Fin-Test-Material", 1.0, true)); + fins.setThickness(0.005); // == 5 mm + + body.addChild(fins); + + fins.setTabLength(0.00); + + fins.setFilletRadius(0.0); + + rkt.enableEvents(); + return rkt; + } + + @Test + public void testMultiplicity() { + final TrapezoidFinSet trapFins = new TrapezoidFinSet(); + assertEquals(1, trapFins.getFinCount()); + } + + @Test + public void testGenerateTrapezoidalPoints() { + final Rocket rkt = createSimpleTrapezoidalFin(); + FinSet fins = (FinSet) rkt.getChild(0).getChild(0).getChild(0); + + // Fin length = 0.05 + // Tab Length = 0.01 + // +--+ + // / \ + // / \ + // +---+--------+---+ + // + Coordinate[] actPoints = fins.getFinPoints(); + + Coordinate[] expPoints = { new Coordinate(0.00, 0.0), + new Coordinate(0.02, 0.05), + new Coordinate(0.04, 0.05), + new Coordinate(0.06, 0.0), + new Coordinate(0.00, 0.0) }; + + for (int index = 0; index < actPoints.length; ++index) { + assertEquals(" generated fin point [" + index + "] doesn't match! ", expPoints[index].x, actPoints[index].x, EPSILON); + assertEquals(" generated fin point [" + index + "] doesn't match!", expPoints[index].x, actPoints[index].x, EPSILON); + assertEquals(" generated fin point [" + index + "] doesn't match!", expPoints[index].x, actPoints[index].x, EPSILON); + } + } + + @Test + public void testCGCalculation_simpleSquareFin() { + final Rocket rkt = createSimpleTrapezoidalFin(); + final TrapezoidFinSet fins = (TrapezoidFinSet)rkt.getChild(0).getChild(0).getChild(0); + + // This is a simple square fin with sides of 1.0. + fins.setFinShape(0.1, 0.1, 0.0, 0.1, .005); + + // should return a single-fin-planform area + assertEquals("area calculation doesn't match: ", 0.01, fins.getPlanformArea(), 0.00001); + + final double expSingleMass = 0.00005; + final Coordinate singleCG = fins.getComponentCG(); + assertEquals("Fin mass is wrong! ", expSingleMass, singleCG.weight, EPSILON); + assertEquals("Centroid x coordinate is wrong! ", 0.05, singleCG.x, EPSILON); + assertEquals("Centroid y coordinate is wrong! ", 0.15, singleCG.y, EPSILON); + + // should still return a single-fin-wetted area + assertEquals(0.00005, fins.getComponentVolume(), 0.0000001); + + { // test instancing code + // this should also trigger a recalculation + fins.setFinCount(2); + + // should still return a single-fin-planform area + assertEquals(0.01, fins.getPlanformArea(), 0.00001); + + Coordinate doubleCG = fins.getComponentCG(); + final double expDoubleMass = expSingleMass*2; + assertEquals("Fin x2 mass does not change from single fin instance! ", expDoubleMass, doubleCG.weight, EPSILON); + assertEquals(0.05, doubleCG.x, EPSILON); + assertEquals(0.0, doubleCG.y, EPSILON); + } + } + + + @Test + public void testCGCalculations_finWithTab() throws IllegalFinPointException { + final Rocket rkt = createSimpleTrapezoidalFin(); + FinSet fins = (FinSet) rkt.getChild(0).getChild(0).getChild(0); + + fins.setTabLength(0.02); + fins.setTabHeight(0.02); + fins.setTabOffsetMethod(AxialMethod.MIDDLE); + fins.setTabOffset(0.0); + + assertEquals("Wetted Area does not match!", 0.0020, fins.getPlanformArea(), EPSILON); + + final double expVol1 = 0.00001200; + final double actVol1 = fins.getComponentVolume(); + assertEquals(" fin volume is incorrect", expVol1, actVol1, EPSILON); + + Coordinate actCentroid1 = fins.getCG(); + assertEquals(" basic centroid x doesn't match: ", 0.03000, actCentroid1.x, EPSILON); + assertEquals(" basic centroid y doesn't match: ", 0.11569444, actCentroid1.y, EPSILON); + + { + fins.setFinCount(2); + final double expVol2 = expVol1 * 2; + final double actVol2 = fins.getComponentVolume(); + assertEquals(" fin volume is incorrect", expVol2, actVol2, EPSILON); + + Coordinate actCentroid2 = fins.getCG(); + // x coordinate will be the same.... + assertEquals(" basic centroid y doesn't match: ", 0.0, actCentroid2.y, EPSILON); + } + } + + + @Test + public void testFilletCalculations() { + final Rocket rkt = createSimpleTrapezoidalFin(); + BodyTube body = (BodyTube) rkt.getChild(0).getChild(0); + FinSet fins = (FinSet) rkt.getChild(0).getChild(0).getChild(0); + + fins.setFilletRadius(0.005); + fins.setFilletMaterial(Material.newMaterial(Material.Type.BULK, "Fillet-Test-Material", 1.0, true)); + + // used for fillet and edge calculations: + // + // [1] +--+ [2] + // / \ + // / \ + // [0] +--------+ [3] + // + assertEquals("Body radius doesn't match: ", 0.1, body.getOuterRadius(), EPSILON); + + final Coordinate actVolume = fins.calculateFilletVolumeCentroid(); + + assertEquals("Line volume doesn't match: ", 5.973e-07, actVolume.weight, EPSILON); + + assertEquals("Line mass center.x doesn't match: ", 0.03, actVolume.x, EPSILON); + assertEquals("Line mass center.y doesn't match: ", 0.101, actVolume.y, EPSILON); + + + { // and then, check that the fillet volume feeds into a correct overall CG: + Coordinate actCentroid = fins.getCG(); + assertEquals("Complete centroid x doesn't match: ", 0.03000, actCentroid.x, EPSILON); + assertEquals("Complete centroid y doesn't match: ", 0.11971548, actCentroid.y, EPSILON); + } + } + @Test public void testTrapezoidCGComputation() { @@ -19,7 +197,7 @@ public class TrapezoidFinSetTest extends BaseTestCase { fins.setFinShape(1.0, 1.0, 0.0, 1.0, .005); Coordinate coords = fins.getCG(); - assertEquals(1.0, fins.getFinArea(), 0.001); + assertEquals(1.0, fins.getPlanformArea(), 0.001); assertEquals(0.5, coords.x, 0.001); assertEquals(0.5, coords.y, 0.001); } @@ -36,7 +214,7 @@ public class TrapezoidFinSetTest extends BaseTestCase { fins.setFinShape(1.0, 0.5, 0.0, 1.0, .005); Coordinate coords = fins.getCG(); - assertEquals(0.75, fins.getFinArea(), 0.001); + assertEquals(0.75, fins.getPlanformArea(), 0.001); assertEquals(0.3889, coords.x, 0.001); assertEquals(0.4444, coords.y, 0.001); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java index 08c5b42d7..1c3821b46 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java @@ -201,7 +201,7 @@ public abstract class FinSetConfig extends RocketComponentConfig { label.setToolTipText(trans.get("FinSetConfig.ttip.Tabposition")); panel.add(label, "gapleft para"); - final DoubleModel mts = new DoubleModel(component, "TabShift", UnitGroup.UNITS_LENGTH); + final DoubleModel mts = new DoubleModel(component, "TabOffset", UnitGroup.UNITS_LENGTH); component.addChangeListener( mts); spin = new JSpinner(mts.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); @@ -215,7 +215,7 @@ public abstract class FinSetConfig extends RocketComponentConfig { panel.add(label, "right, gapright unrel"); - final EnumModel em = new EnumModel<>(component, "TabRelativePosition"); + final EnumModel em = new EnumModel<>(component, "TabOffsetMethod"); JComboBox enumCombo = new JComboBox<>(em); diff --git a/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java index 64fe1f06f..0df6f6555 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java @@ -340,10 +340,6 @@ public class FreeformFinSetConfig extends FinSetConfig { List points = importer.getPoints(chooser.getSelectedFile()); document.startUndo(trans.get("CustomFinImport.undo")); finset.setPoints( points); - } catch (IllegalFinPointException e) { - log.warn("Error storing fin points", e); - JOptionPane.showMessageDialog(this, trans.get("CustomFinImport.error.badimage"), - trans.get("CustomFinImport.error.title"), JOptionPane.ERROR_MESSAGE); } catch (IOException e) { log.warn("Error loading file", e); JOptionPane.showMessageDialog(this, e.getLocalizedMessage(), @@ -426,11 +422,7 @@ public class FreeformFinSetConfig extends FinSetConfig { } Point2D.Double point = getCoordinates(event); - try { - finset.setPoint(dragIndex, point.x, point.y); - } catch (IllegalFinPointException ignore) { - log.debug("Ignoring IllegalFinPointException while dragging, dragIndex=" + dragIndex + " x=" + point.x + " y=" + point.y); - } + finset.setPoint(dragIndex, point.x, point.y); updateFields(); } @@ -581,8 +573,6 @@ public class FreeformFinSetConfig extends FinSetConfig { updateFields(); } catch (NumberFormatException ignore) { log.warn("ignoring NumberFormatException while editing a Freeform Fin"); - } catch (IllegalFinPointException ignore) { - log.warn("ignoring IllegalFinPointException while editing a Freeform Fin"); } } } diff --git a/swing/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java index 90216f366..4fc60db93 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java @@ -115,7 +115,7 @@ public class ScaleDialog extends JDialog { addScaler(FinSet.class, "Thickness"); addScaler(FinSet.class, "TabHeight"); addScaler(FinSet.class, "TabLength"); - addScaler(FinSet.class, "TabShift"); + addScaler(FinSet.class, "TabOffset"); // TrapezoidFinSet addScaler(TrapezoidFinSet.class, "Sweep"); @@ -623,11 +623,9 @@ public class ScaleDialog extends JDialog { for (int i = 0; i < points.length; i++) { points[i] = points[i].multiply(multiplier); } - try { - finset.setPoints(points); - } catch (IllegalFinPointException e) { - throw new BugException("Failed to set points after scaling, original=" + Arrays.toString(finset.getFinPoints()) + " scaled=" + Arrays.toString(points), e); - } + + finset.setPoints(points); + } } diff --git a/swing/src/net/sf/openrocket/gui/main/ComponentAddButtons.java b/swing/src/net/sf/openrocket/gui/main/ComponentAddButtons.java index 2f4e41dfb..8c27ed950 100644 --- a/swing/src/net/sf/openrocket/gui/main/ComponentAddButtons.java +++ b/swing/src/net/sf/openrocket/gui/main/ComponentAddButtons.java @@ -128,17 +128,17 @@ public class ComponentAddButtons extends JPanel implements Scrollable { //// Transition new BodyComponentButton(Transition.class, trans.get("compaddbuttons.Transition")), //// Trapezoidal - new FinButton(TrapezoidFinSet.class, trans.get("compaddbuttons.Trapezoidal")), // TODO: MEDIUM: freer fin placing + new ComponentButton(TrapezoidFinSet.class, trans.get("compaddbuttons.Trapezoidal")), // TODO: MEDIUM: freer fin placing //// Elliptical - new FinButton(EllipticalFinSet.class, trans.get("compaddbuttons.Elliptical")), + new ComponentButton(EllipticalFinSet.class, trans.get("compaddbuttons.Elliptical")), //// Freeform - new FinButton(FreeformFinSet.class, trans.get("compaddbuttons.Freeform")), + new ComponentButton(FreeformFinSet.class, trans.get("compaddbuttons.Freeform")), //// Freeform - new FinButton(TubeFinSet.class, trans.get("compaddbuttons.Tubefin")), - //// Rail Button // TODO: implement drawing graphics for the component - new FinButton( RailButton.class, trans.get("compaddbuttons.RailButton")), + new ComponentButton(TubeFinSet.class, trans.get("compaddbuttons.Tubefin")), + //// Rail Button + new ComponentButton( RailButton.class, trans.get("compaddbuttons.RailButton")), //// Launch lug - new FinButton(LaunchLug.class, trans.get("compaddbuttons.Launchlug"))); + new ComponentButton(LaunchLug.class, trans.get("compaddbuttons.Launchlug"))); row++; ///////////////////////////////////////////// @@ -627,38 +627,7 @@ public class ComponentAddButtons extends JPanel implements Scrollable { } } - - - /** - * Class for fin sets, that attach only to BodyTubes. - */ - private class FinButton extends ComponentButton { - /** - * - */ - private static final long serialVersionUID = -219204844803871258L; - - public FinButton(Class c, String text) { - super(c, text); - } - - public FinButton(String text, Icon enabled, Icon disabled) { - super(text, enabled, disabled); - } - - public FinButton(String text) { - super(text); - } - - @Override - public boolean isAddable(RocketComponent c) { - if (c == null) - return false; - return (c.getClass().equals(BodyTube.class)); - } - } - ///////// Scrolling functionality diff --git a/swing/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java b/swing/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java index 6602e462b..28040eb87 100644 --- a/swing/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java +++ b/swing/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java @@ -2,56 +2,125 @@ package net.sf.openrocket.gui.rocketfigure; import java.awt.Shape; import java.awt.geom.Path2D; +import java.util.ArrayList; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.Transformation; - public class FinSetShapes extends RocketComponentShape { - // TODO: LOW: Clustering is ignored (FinSet cannot currently be clustered) + public static RocketComponentShape[] getShapesSide( - net.sf.openrocket.rocketcomponent.RocketComponent component, + RocketComponent component, Transformation transformation, - Coordinate instanceAbsoluteLocation) { - net.sf.openrocket.rocketcomponent.FinSet finset = (net.sf.openrocket.rocketcomponent.FinSet)component; - Coordinate finSetFront = instanceAbsoluteLocation; - Coordinate finPoints[] = finset.getFinPointsWithTab(); + Coordinate componentAbsoluteLocation) { + FinSet finset = (FinSet)component; - Transformation cantRotation = finset.getCantRotation(); - finPoints = cantRotation.transform(finPoints); - finPoints = transformation.transform(finPoints); + int finCount = finset.getFinCount(); + // TODO: MEDIUM: sloping radius + final double rFront = finset.getFinFront().y; + + Transformation cantRotation = finset.getCantRotation(); + Transformation baseRotation = new Transformation(finset.getBaseRotation(), 0, 0); // rotation about x-axis + Transformation radialTranslation = new Transformation( 0, rFront, 0); + Transformation finRotation = finset.getFinRotationTransformation(); + Transformation compositeTransform = baseRotation + .applyTransformation( radialTranslation) + .applyTransformation( cantRotation) + .applyTransformation( transformation); + + Coordinate finSetFront = componentAbsoluteLocation; + Coordinate finPoints[] = finset.getFinPoints(); + Coordinate tabPoints[] = finset.getTabPoints(); + Coordinate basePoints[] = finset.getRootPoints(); + + // Translate & rotate points into place + finPoints = compositeTransform.transform( finPoints ); + tabPoints = compositeTransform.transform( tabPoints); + basePoints = compositeTransform.transform( basePoints ); + // Generate shapes - Path2D.Float p; - { - // Make polygon - p = new Path2D.Float(); - for (int i=0; i shapeList = new ArrayList<>(); + for (int finNum=0; finNum