diff --git a/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java index cc25c704c..2f8cf3f35 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java @@ -367,7 +367,7 @@ public class FreeformFinSetConfig extends FinSetConfig { private int dragIndex = -1; private FinPointScrollPane( final FinPointFigure _figure) { - super( _figure, true); + super( _figure); } @Override diff --git a/swing/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java index 33ec67823..14cda1d8d 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java @@ -506,7 +506,6 @@ public class GeneralOptimizationDialog extends JDialog { // // Rocket figure figure = new RocketFigure( getSelectedSimulation().getRocket() ); - figure.setBorderPixels(1, 1); ScaleScrollPane figureScrollPane = new ScaleScrollPane(figure); figureScrollPane.setFitting(true); panel.add(figureScrollPane, "span, split, height 200lp, grow"); diff --git a/swing/src/net/sf/openrocket/gui/print/DesignReport.java b/swing/src/net/sf/openrocket/gui/print/DesignReport.java index 0a56cce7d..d95f9fb8f 100644 --- a/swing/src/net/sf/openrocket/gui/print/DesignReport.java +++ b/swing/src/net/sf/openrocket/gui/print/DesignReport.java @@ -177,7 +177,7 @@ public class DesignReport { canvas.beginText(); canvas.setFontAndSize(ITextHelper.getBaseFont(), PrintUtilities.NORMAL_FONT_SIZE); - int figHeightPts = (int) (PrintUnit.METERS.toPoints(figure.getFigureHeight()) * 0.4 * (scale / PrintUnit.METERS + int figHeightPts = (int) (PrintUnit.METERS.toPoints(figure.getHeight()) * 0.4 * (scale / PrintUnit.METERS .toPoints(1))); final int diagramHeight = pageImageableHeight * 2 - 70 - (figHeightPts); canvas.moveText(document.leftMargin() + pageSize.getBorderWidthLeft(), diagramHeight); @@ -274,7 +274,7 @@ public class DesignReport { theFigure.updateFigure(); double scale = - (thePageImageableWidth * 2.2) / theFigure.getFigureWidth(); + (thePageImageableWidth * 2.2) / theFigure.getWidth(); theFigure.setScale(scale); /* * page dimensions are in points-per-inch, which, in Java2D, are the same as pixels-per-inch; thus we don't need any conversion @@ -288,7 +288,7 @@ public class DesignReport { int y = PrintUnit.POINTS_PER_INCH; //If the y dimension is negative, then it will potentially be drawn off the top of the page. Move the origin //to allow for this. - if (theFigure.getDimensions().getY() < 0.0d) { + if (theFigure.getHeight() < 0.0d) { y += (int) halfFigureHeight; } g2d.translate(20, y); diff --git a/swing/src/net/sf/openrocket/gui/print/PrintFigure.java b/swing/src/net/sf/openrocket/gui/print/PrintFigure.java index 13661be97..344713505 100644 --- a/swing/src/net/sf/openrocket/gui/print/PrintFigure.java +++ b/swing/src/net/sf/openrocket/gui/print/PrintFigure.java @@ -22,18 +22,12 @@ public class PrintFigure extends RocketFigure { super(rkt); } - @Override - protected double computeTy(int heightPx) { - super.computeTy(heightPx); - return 0; - } - public void setScale(final double theScale) { this.scale = theScale; //dpi/0.0254*scaling; updateFigure(); } public double getFigureHeightPx() { - return this.figureHeightPx; + return this.getSize().height; } } diff --git a/swing/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java b/swing/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java index 2955b34d5..258e1f0dc 100644 --- a/swing/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java +++ b/swing/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java @@ -2,6 +2,8 @@ package net.sf.openrocket.gui.scalefigure; import java.awt.Color; import java.awt.Dimension; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; import java.util.EventListener; import java.util.EventObject; import java.util.LinkedList; @@ -9,124 +11,194 @@ import java.util.List; import javax.swing.JPanel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.StateChangeListener; - @SuppressWarnings("serial") -public abstract class AbstractScaleFigure extends JPanel implements ScaleFigure { - +public abstract class AbstractScaleFigure extends JPanel { + + private final static Logger log = LoggerFactory.getLogger(AbstractScaleFigure.class); + + /** + * Extra scaling applied to the figure. The f***ing Java JRE doesn't know + * how to draw shapes when using very large scaling factors, so this must + * be manually applied to every single shape used. + *

+ * The scaling factor used is divided by this value, and every coordinate used + * in the figures must be multiplied by this factor. + */ + public static final double EXTRA_SCALE = 1.0; + + public static final double INCHES_PER_METER = 39.3701; + public static final double METERS_PER_INCH = 0.0254; + + public static final double MINIMUM_ZOOM = 0.01; // == 1 % + public static final double MAXIMUM_ZOOM = 1000.00; // == 10,000 % + // Number of pixels to leave at edges when fitting figure private static final int DEFAULT_BORDER_PIXELS_WIDTH = 30; private static final int DEFAULT_BORDER_PIXELS_HEIGHT = 20; + // constant factor that scales screen real-estate to rocket-space + private final double baseScale; + private double userScale = 1.0; + protected double scale = -1; - protected final double dpi; - - protected double scale = 1.0; - protected double scaling = 1.0; - - protected int borderPixelsWidth = DEFAULT_BORDER_PIXELS_WIDTH; - protected int borderPixelsHeight = DEFAULT_BORDER_PIXELS_HEIGHT; + protected static final Dimension borderThickness_px = new Dimension(DEFAULT_BORDER_PIXELS_WIDTH, DEFAULT_BORDER_PIXELS_HEIGHT); + protected Dimension originLocation_px = new Dimension(0,0); + // ======= whatever this figure is drawing, in real-space coordinates: meters + protected Rectangle2D subjectBounds_m = null; + + + // combines the translation and scale in one place: + // which frames does this transform between ? + protected AffineTransform projection = null; + protected final List listeners = new LinkedList(); public AbstractScaleFigure() { - this.dpi = GUIUtil.getDPI(); - this.scaling = 1.0; - this.scale = dpi / 0.0254 * scaling; - + // produces a pixels-per-meter scale factor + // + // dots dots inch + // ---- = ------ * ----- + // meter inch meter + // + this.baseScale = GUIUtil.getDPI() * INCHES_PER_METER; + this.userScale = 1.0; + this.scale = baseScale * userScale; + + this.setPreferredSize(new Dimension(100,100)); + setSize(100,100); + setBackground(Color.WHITE); setOpaque(true); } - - - public abstract void updateFigure(); - - public abstract double getFigureWidth(); - - public abstract double getFigureHeight(); - - - @Override - public double getScaling() { - return scaling; + public double getUserScale(){ + return userScale; } - @Override - public double getAbsoluteScale() { - return scale; + public double getAbsoluteScale() { + return scale; + } + + public Dimension getSubjectOrigin() { + return originLocation_px; + } + + /** + * Set the scale level of the figure. A scale value of 1.0 is equivalent to 100 % scale. + * smaller scale display the subject smaller. + * + * @param newScaleRequest the scale level. + */ + public void scaleTo(final double newScaleRequest) { + if (MathUtil.equals(this.userScale, newScaleRequest, 0.01)){ + return;} + if (Double.isInfinite(newScaleRequest) || Double.isNaN(newScaleRequest)) { + return;} + + log.warn(String.format("scaling Request from %g => %g @%s\n", this.userScale, newScaleRequest, this.getClass().getSimpleName()), new Throwable()); + + this.userScale = MathUtil.clamp( newScaleRequest, MINIMUM_ZOOM, MAXIMUM_ZOOM); + + this.scale = baseScale * userScale; } - @Override - public void setScaling(double scaling) { - if (Double.isInfinite(scaling) || Double.isNaN(scaling)) - scaling = 1.0; - if (scaling < 0.001) - scaling = 0.001; - if (scaling > 1000) - scaling = 1000; - if (Math.abs(this.scaling - scaling) < 0.01) - return; - this.scaling = scaling; - this.scale = dpi / 0.0254 * scaling; - updateFigure(); - } - - @Override - public void setScaling(Dimension bounds) { - double zh = 1, zv = 1; - int w = bounds.width - 2 * borderPixelsWidth - 20; - int h = bounds.height - 2 * borderPixelsHeight - 20; - - if (w < 10) - w = 10; - if (h < 10) - h = 10; - - zh = (w) / getFigureWidth(); - zv = (h) / getFigureHeight(); - - double s = Math.min(zh, zv) / dpi * 0.0254 - 0.001; - - // Restrict to 100% - if (s > 1.0) { - s = 1.0; + /** + * Set the scale level to display newBounds + * + * @param bounds the bounds of the figure. + */ + public void scaleTo(Dimension newBounds) { + if( 0 == newBounds.getWidth() || 0 == newBounds.getHeight()) + return; + + updateSubjectDimensions(); + updateCanvasOrigin(); + updateCanvasSize(); + updateTransform(); + + // dimensions within the viewable area, which are available to draw + final int drawable_width_px = newBounds.width - 2 * borderThickness_px.width; + final int drawable_height_px = newBounds.height - 2 * borderThickness_px.height; + + if(( 0 < drawable_width_px ) && ( 0 < drawable_height_px)) { + final double width_scale = (drawable_width_px) / ( subjectBounds_m.getWidth() * baseScale); + final double height_scale = (drawable_height_px) / ( subjectBounds_m.getHeight() * baseScale); + final double minScale = Math.min(height_scale, width_scale); + + scaleTo(minScale); } + } + + /** + * Return the pixel coordinates of the subject's origin. + * + * @return the pixel coordinates of the figure origin. + */ + protected abstract void updateSubjectDimensions(); - setScaling(s); + protected abstract void updateCanvasOrigin(); + + /** + * update preferred figure Size + + */ + protected void updateCanvasSize() { + Dimension preferredFigureSize_px = new Dimension((int)(subjectBounds_m.getWidth()*scale) + 2*borderThickness_px.width, + (int)(subjectBounds_m.getHeight()*scale) + 2*borderThickness_px.height); + + setPreferredSize(preferredFigureSize_px); + setMinimumSize(preferredFigureSize_px); + revalidate(); + } + + protected void updateTransform(){ + // Calculate and store the transformation used + // (inverse is used in detecting clicks on objects) + projection = new AffineTransform(); + projection.translate(this.originLocation_px.width, originLocation_px.height); + // Mirror position Y-axis upwards + projection.scale(scale, -scale); + } + + /** + * Updates the figure shapes and figure size. + */ + public void updateFigure() { + log.debug(String.format("____ Updating %s to: %g user scale, %g overall scale", this.getClass().getSimpleName(), this.getAbsoluteScale(), this.scale)); + + updateSubjectDimensions(); + updateCanvasOrigin(); + updateCanvasSize(); + updateTransform(); + + revalidate(); + repaint(); + } + + protected Dimension getBorderPixels() { + return borderThickness_px; } - - - @Override - public Dimension getBorderPixels() { - return new Dimension(borderPixelsWidth, borderPixelsHeight); - } - - @Override - public void setBorderPixels(int width, int height) { - this.borderPixelsWidth = width; - this.borderPixelsHeight = height; - } - - - @Override + public void addChangeListener(StateChangeListener listener) { listeners.add(0, listener); } - @Override public void removeChangeListener(StateChangeListener listener) { listeners.remove(listener); } - private EventObject changeEvent = null; - protected void fireChangeEvent() { - if (changeEvent == null) - changeEvent = new EventObject(this); + final EventObject changeEvent = new EventObject(this); + // Copy the list before iterating to prevent concurrent modification exceptions. EventListener[] list = listeners.toArray(new EventListener[0]); for (EventListener l : list) { @@ -135,5 +207,5 @@ public abstract class AbstractScaleFigure extends JPanel implements ScaleFigure } } } - + } diff --git a/swing/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java b/swing/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java index b6fdd2bef..1a275ae63 100644 --- a/swing/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java +++ b/swing/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java @@ -13,96 +13,73 @@ import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; +import java.util.LinkedList; +import java.util.List; +import org.slf4j.*; import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.SymmetricComponent; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.rocketcomponent.position.AxialMethod; import net.sf.openrocket.unit.Tick; import net.sf.openrocket.unit.Unit; import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.BoundingBox; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.StateChangeListener; -// TODO: MEDIUM: the figure jumps and bugs when using automatic fitting - @SuppressWarnings("serial") public class FinPointFigure extends AbstractScaleFigure { - - private static final int BOX_SIZE = 4; - - private final FreeformFinSet finset; + + private final static Logger log = LoggerFactory.getLogger(FinPointFigure.class); + + + private static final float MINIMUM_CANVAS_SIZE_METERS = 0.01f; // i.e. 1 cm + + private static final Color GRID_LINE_COLOR = new Color( 137, 137, 137, 32); + private static final float GRID_LINE_BASE_WIDTH = 0.001f; + + private static final int LINE_WIDTH_PIXELS = 1; + + // the size of the boxes around each fin point vertex + private static final float BOX_WIDTH_PIXELS = 12; + + private static final double MINOR_TICKS = 0.05; + private static final double MAJOR_TICKS = 0.1; + + private final FreeformFinSet finset; private int modID = -1; - private double minX, maxX, maxY; - private double figureWidth = 0; - private double figureHeight = 0; - private double translateX = 0; - private double translateY = 0; - - private AffineTransform transform; - private Rectangle2D.Double[] handles = null; + protected final List listeners = new LinkedList(); + + private Rectangle2D.Double[] finPointHandles = null; + public FinPointFigure(FreeformFinSet finset) { this.finset = finset; + + // useful for debugging -- shows a contrast against un-drawn space. + setBackground(Color.WHITE); + setOpaque(true); + + updateTransform(); } - - + @Override public void paintComponent(Graphics g) { super.paintComponent(g); - Graphics2D g2 = (Graphics2D) g; - - if (modID != finset.getRocket().getAerodynamicModID()) { - modID = finset.getRocket().getAerodynamicModID(); - calculateDimensions(); - } - - - double tx, ty; - // Calculate translation for figure centering - if (figureWidth * scale + 2 * borderPixelsWidth < getWidth()) { - - // Figure fits in the viewport - tx = (getWidth() - figureWidth * scale) / 2 - minX * scale; - - } else { - - // Figure does not fit in viewport - tx = borderPixelsWidth - minX * scale; - - } - - - if (figureHeight * scale + 2 * borderPixelsHeight < getHeight()) { - ty = getHeight() - borderPixelsHeight; - } else { - ty = borderPixelsHeight + figureHeight * scale; - } - - if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) { - // Origin has changed, fire event - translateX = tx; - translateY = ty; - fireChangeEvent(); - } - - - if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) { - // Origin has changed, fire event - translateX = tx; - translateY = ty; - fireChangeEvent(); - } - - - // Calculate and store the transformation used - transform = new AffineTransform(); - transform.translate(translateX, translateY); - transform.scale(scale / EXTRA_SCALE, -scale / EXTRA_SCALE); - - // TODO: HIGH: border Y-scale upwards - - g2.transform(transform); + Graphics2D g2 = (Graphics2D) g.create(); + + if (modID != finset.getRocket().getAerodynamicModID()) { + modID = finset.getRocket().getAerodynamicModID(); + updateTransform(); + } + + g2.transform(projection); // Set rendering hints appropriately g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, @@ -113,128 +90,184 @@ public class FinPointFigure extends AbstractScaleFigure { RenderingHints.VALUE_ANTIALIAS_ON); + // Background grid + paintBackgroundGrid( g2); - Rectangle visible = g2.getClipBounds(); - double x0 = ((double) visible.x - 3) / EXTRA_SCALE; - double x1 = ((double) visible.x + visible.width + 4) / EXTRA_SCALE; - double y0 = ((double) visible.y - 3) / EXTRA_SCALE; - double y1 = ((double) visible.y + visible.height + 4) / EXTRA_SCALE; - - - // Background grid - - g2.setStroke(new BasicStroke((float) (1.0 * EXTRA_SCALE / scale), - BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); - g2.setColor(new Color(0, 0, 255, 30)); - - Unit unit; - if (this.getParent() != null && - this.getParent().getParent() instanceof ScaleScrollPane) { - unit = ((ScaleScrollPane) this.getParent().getParent()).getCurrentUnit(); - } else { - unit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); - } - - // vertical - Tick[] ticks = unit.getTicks(x0, x1, - ScaleScrollPane.MINOR_TICKS / scale, - ScaleScrollPane.MAJOR_TICKS / scale); - Line2D.Double line = new Line2D.Double(); - for (Tick t : ticks) { - if (t.major) { - line.setLine(t.value * EXTRA_SCALE, y0 * EXTRA_SCALE, - t.value * EXTRA_SCALE, y1 * EXTRA_SCALE); - g2.draw(line); - } - } - - // horizontal - ticks = unit.getTicks(y0, y1, - ScaleScrollPane.MINOR_TICKS / scale, - ScaleScrollPane.MAJOR_TICKS / scale); - for (Tick t : ticks) { - if (t.major) { - line.setLine(x0 * EXTRA_SCALE, t.value * EXTRA_SCALE, - x1 * EXTRA_SCALE, t.value * EXTRA_SCALE); - g2.draw(line); - } - } - - - - - - // Base rocket line - g2.setStroke(new BasicStroke((float) (3.0 * EXTRA_SCALE / scale), - BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); - g2.setColor(Color.GRAY); - - g2.drawLine((int) (x0 * EXTRA_SCALE), 0, (int) (x1 * EXTRA_SCALE), 0); - - - // Fin shape - Coordinate[] points = finset.getFinPoints(); - Path2D.Double shape = new Path2D.Double(); - shape.moveTo(0, 0); - for (int i = 1; i < points.length; i++) { - shape.lineTo(points[i].x * EXTRA_SCALE, points[i].y * EXTRA_SCALE); - } - - g2.setStroke(new BasicStroke((float) (1.0 * EXTRA_SCALE / scale), - BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); - g2.setColor(Color.BLACK); - g2.draw(shape); - - - // Fin point boxes - g2.setColor(new Color(150, 0, 0)); - double s = BOX_SIZE * EXTRA_SCALE / scale; - handles = new Rectangle2D.Double[points.length]; - for (int i = 0; i < points.length; i++) { - Coordinate c = points[i]; - handles[i] = new Rectangle2D.Double(c.x * EXTRA_SCALE - s, c.y * EXTRA_SCALE - s, 2 * s, 2 * s); - g2.draw(handles[i]); - } + paintRocketBody(g2); + paintFinShape(g2); + paintFinHandles(g2); } - + public void paintBackgroundGrid( Graphics2D g2){ + Rectangle visible = g2.getClipBounds(); + int x0 = visible.x - 3; + int x1 = visible.x + visible.width + 4; + int y0 = visible.y - 3; + int y1 = visible.y + visible.height + 4; - public int getIndexByPoint(double x, double y) { - if (handles == null) - return -1; - - // Calculate point in shapes' coordinates - Point2D.Double p = new Point2D.Double(x, y); - try { - transform.inverseTransform(p, p); - } catch (NoninvertibleTransformException e) { - return -1; - } - - for (int i = 0; i < handles.length; i++) { - if (handles[i].contains(p)) - return i; - } - return -1; + final float grid_line_width = (float)(FinPointFigure.GRID_LINE_BASE_WIDTH/this.scale); + g2.setStroke(new BasicStroke( grid_line_width, + BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + g2.setColor(FinPointFigure.GRID_LINE_COLOR); + + Unit unit; + if (this.getParent() != null && this.getParent().getParent() instanceof ScaleScrollPane) { + unit = ((ScaleScrollPane) this.getParent().getParent()).getCurrentUnit(); + } else { + unit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); + } + + // vertical + Tick[] verticalTicks = unit.getTicks(x0, x1, MINOR_TICKS, MAJOR_TICKS); + Line2D.Double line = new Line2D.Double(); + for (Tick t : verticalTicks) { + if (t.major) { + line.setLine( t.value, y0, t.value, y1); + g2.draw(line); + } + } + + // horizontal + Tick[] horizontalTicks = unit.getTicks(y0, y1, MINOR_TICKS, MAJOR_TICKS); + for (Tick t : horizontalTicks) { + if (t.major) { + line.setLine( x0, t.value, x1, t.value); + g2.draw(line); + } + } + } + + private void paintRocketBody( Graphics2D g2){ + RocketComponent comp = finset.getParent(); + if( comp instanceof Transition ){ + paintBodyTransition(g2); + }else{ + paintBodyTube(g2); + } + } + + // NOTE: This function drawns relative to the reference point of the BODY component + // In other words: 0,0 == the front, foreRadius of the body component + private void paintBodyTransition( Graphics2D g2){ + + // setup lines + final float bodyLineWidth = (float) ( LINE_WIDTH_PIXELS / scale ); + g2.setStroke(new BasicStroke( bodyLineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + g2.setColor(Color.BLACK); + + Transition body = (Transition) finset.getParent(); + final float xResolution_m = 0.01f; // distance between draw points, in meters + + final double xFinStart = finset.asPositionValue(AxialMethod.TOP); //<< in body frame + + // vv in fin-frame == draw-frame vv + final double xOffset = -xFinStart; + final double yOffset = -body.getRadius(xFinStart); + + Path2D.Double bodyShape = new Path2D.Double(); + // draw front-cap: + bodyShape.moveTo( xOffset, yOffset); + bodyShape.lineTo( xOffset, yOffset + body.getForeRadius()); + + final float length_m = (float)( body.getLength()); + Point2D.Double cur = new Point2D.Double (); + for( double xBody = xResolution_m ; xBody < length_m; xBody += xResolution_m ){ + // xBody is distance from front of parent body + cur.x = xOffset + xBody; // offset from origin (front of fin) + cur.y = yOffset + body.getRadius( xBody); // offset from origin ( fin-front-point ) + + bodyShape.lineTo( cur.x, cur.y); + } + + // draw end-cap + bodyShape.lineTo( xOffset + length_m, yOffset + body.getAftRadius()); + bodyShape.lineTo( xOffset + length_m, yOffset); + + g2.draw(bodyShape); + } + + private void paintBodyTube( Graphics2D g2){ + Rectangle visible = g2.getClipBounds(); + int x0 = visible.x - 3; + int x1 = visible.x + visible.width + 4; + + final float bodyLineWidth = (float) ( LINE_WIDTH_PIXELS / scale ); + g2.setStroke(new BasicStroke( bodyLineWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + g2.setColor(Color.BLACK); + + g2.drawLine((int) x0, 0, (int)x1, 0); + } + + private void paintFinShape(final Graphics2D g2){ + // excludes fin tab points + final Coordinate[] drawPoints = finset.getFinPoints(); + + Path2D.Double shape = new Path2D.Double(); + Coordinate startPoint= drawPoints[0]; + shape.moveTo( startPoint.x, startPoint.y); + for (int i = 1; i < drawPoints.length; i++) { + shape.lineTo( drawPoints[i].x, drawPoints[i].y); + } + + final float finEdgeWidth_m = (float) (LINE_WIDTH_PIXELS / scale ); + g2.setStroke(new BasicStroke( finEdgeWidth_m, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + g2.setColor(Color.BLUE); + g2.draw(shape); } - + private void paintFinHandles(final Graphics2D g2) { + // excludes fin tab points + final Coordinate[] drawPoints = finset.getFinPoints(); + + // Fin point boxes + final float boxWidth = (float) (BOX_WIDTH_PIXELS / scale ); + final float boxEdgeWidth_m = (float) ( LINE_WIDTH_PIXELS / scale ); + g2.setStroke(new BasicStroke( boxEdgeWidth_m, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + g2.setColor(new Color(150, 0, 0)); + final double boxHalfWidth = boxWidth/2; + finPointHandles = new Rectangle2D.Double[ drawPoints.length]; + for (int i = 0; i < drawPoints.length; i++) { + Coordinate c = drawPoints[i]; + finPointHandles[i] = new Rectangle2D.Double(c.x - boxHalfWidth, c.y - boxHalfWidth, boxWidth, boxWidth); + g2.draw(finPointHandles[i]); + } + } + + public int getIndexByPoint(double x, double y) { + if (finPointHandles == null) + return -1; + + // Calculate point in shapes' coordinates + Point2D.Double p = new Point2D.Double(x, y); + try { + projection.inverseTransform(p, p); + } catch (NoninvertibleTransformException e) { + return -1; + } + + for (int i = 0; i < finPointHandles.length; i++) { + if (finPointHandles[i].contains(p)) + return i; + } + return -1; + } + public int getSegmentByPoint(double x, double y) { - if (handles == null) + if (finPointHandles == null) return -1; // Calculate point in shapes' coordinates Point2D.Double p = new Point2D.Double(x, y); try { - transform.inverseTransform(p, p); + projection.inverseTransform(p, p); } catch (NoninvertibleTransformException e) { return -1; } - double x0 = p.x / EXTRA_SCALE; - double y0 = p.y / EXTRA_SCALE; - double delta = BOX_SIZE / scale; + double x0 = p.x; + double y0 = p.y; + double delta = BOX_WIDTH_PIXELS /*/ scale*/; //System.out.println("Point: " + x0 + "," + y0); //System.out.println("delta: " + (BOX_SIZE / scale)); @@ -262,84 +295,60 @@ public class FinPointFigure extends AbstractScaleFigure { public Point2D.Double convertPoint(double x, double y) { Point2D.Double p = new Point2D.Double(x, y); try { - transform.inverseTransform(p, p); + projection.inverseTransform(p, p); } catch (NoninvertibleTransformException e) { assert (false) : "Should not occur"; return new Point2D.Double(0, 0); } - p.setLocation(p.x / EXTRA_SCALE, p.y / EXTRA_SCALE); + p.setLocation(p.x, p.y); return p; } - - - @Override - public Dimension getOrigin() { + public Dimension getSubjectOrigin() { if (modID != finset.getRocket().getAerodynamicModID()) { modID = finset.getRocket().getAerodynamicModID(); - calculateDimensions(); + updateTransform(); } - return new Dimension((int) translateX, (int) translateY); - } - + return new Dimension(originLocation_px.width, originLocation_px.height); + } + @Override - public double getFigureWidth() { - if (modID != finset.getRocket().getAerodynamicModID()) { - modID = finset.getRocket().getAerodynamicModID(); - calculateDimensions(); - } - return figureWidth; - } - - @Override - public double getFigureHeight() { - if (modID != finset.getRocket().getAerodynamicModID()) { - modID = finset.getRocket().getAerodynamicModID(); - calculateDimensions(); - } - return figureHeight; - } - - - private void calculateDimensions() { - minX = 0; - maxX = 0; - maxY = 0; - - for (Coordinate c : finset.getFinPoints()) { - if (c.x < minX) - minX = c.x; - if (c.x > maxX) - maxX = c.x; - if (c.y > maxY) - maxY = c.y; - } - - if (maxX < 0.01) - maxX = 0.01; - - figureWidth = maxX - minX; - figureHeight = maxY; - + protected void updateSubjectDimensions(){ + // update subject bounds + BoundingBox newBounds = new BoundingBox(); + + // subsequent updates can only increase the size of the bounds, so this is the minimum size. + newBounds.update( MINIMUM_CANVAS_SIZE_METERS); + + SymmetricComponent parent = (SymmetricComponent)this.finset.getParent(); - Dimension d = new Dimension((int) (figureWidth * scale + 2 * borderPixelsWidth), - (int) (figureHeight * scale + 2 * borderPixelsHeight)); - - if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) { - setPreferredSize(d); - setMinimumSize(d); - revalidate(); - } + // N.B.: (0,0) is the fin front-- where it meets the parent body. + final double xFinFront = finset.asPositionValue(AxialMethod.TOP); //<< in body frame + + // update to bound the parent body: + final double xParentFront = -xFinFront; + newBounds.update( xParentFront); + final double xParentBack = -xFinFront + parent.getLength(); + newBounds.update( xParentBack ); + final double yParentCenterline = -parent.getRadius(xFinFront); // from parent centerline to fin front. + newBounds.update( yParentCenterline ); + + // in 99% of fins, this bound is redundant, buuuuut just in case. + final double yParentMax = yParentCenterline + Math.max( parent.getForeRadius(), parent.getAftRadius()); + newBounds.update( yParentMax ); + + // update to bounds the fin points: + newBounds.update( finset.getFinPoints()); + + subjectBounds_m = newBounds.toRectangle(); } - - @Override - public void updateFigure() { - repaint(); - } - - + protected void updateCanvasOrigin() { + originLocation_px.width = borderThickness_px.width - (int)(subjectBounds_m.getX()*scale); + originLocation_px.height = borderThickness_px.height + (int)(subjectBounds_m.getY()*scale); + + } } diff --git a/swing/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java b/swing/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java index c3a59d65d..8018d9c14 100644 --- a/swing/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java +++ b/swing/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java @@ -18,8 +18,12 @@ import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import net.sf.openrocket.gui.figureelements.FigureElement; import net.sf.openrocket.gui.rocketfigure.RocketComponentShape; +import net.sf.openrocket.gui.scalefigure.RocketPanel.VIEW_TYPE; import net.sf.openrocket.gui.util.ColorConversion; import net.sf.openrocket.gui.util.SwingPreferences; import net.sf.openrocket.motor.Motor; @@ -30,6 +34,7 @@ import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BoundingBox; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.LineStyle; @@ -46,6 +51,8 @@ import net.sf.openrocket.util.Transformation; */ @SuppressWarnings("serial") public class RocketFigure extends AbstractScaleFigure { + + private final static Logger log = LoggerFactory.getLogger(FinPointFigure.class); private static final String ROCKET_FIGURE_PACKAGE = "net.sf.openrocket.gui.rocketfigure"; private static final String ROCKET_FIGURE_SUFFIX = "Shapes"; @@ -61,28 +68,17 @@ public class RocketFigure extends AbstractScaleFigure { private Rocket rocket; private RocketComponent[] selection = new RocketComponent[0]; - private double figureWidth = 0, figureHeight = 0; - protected int figureWidthPx = 0, figureHeightPx = 0; private RocketPanel.VIEW_TYPE currentViewType = RocketPanel.VIEW_TYPE.SideView; private double rotation; - private Transformation transformation; - - private double translateX, translateY; - - - + private Transformation axialRotation; + /* * figureComponents contains the corresponding RocketComponents of the figureShapes */ private final ArrayList figureShapes = new ArrayList(); - - private double minX = 0, maxX = 0, maxR = 0; - // Figure width and height in SI-units and pixels - - private AffineTransform g2transformation = null; private final ArrayList relativeExtra = new ArrayList(); private final ArrayList absoluteExtra = new ArrayList(); @@ -96,27 +92,11 @@ public class RocketFigure extends AbstractScaleFigure { this.rocket = _rkt; this.rotation = 0.0; - this.transformation = Transformation.rotate_x(0.0); + this.axialRotation = Transformation.rotate_x(0.0); updateFigure(); } - @Override - public Dimension getOrigin() { - return new Dimension((int) translateX, (int) translateY); - } - - @Override - public double getFigureHeight() { - return figureHeight; - } - - @Override - public double getFigureWidth() { - return figureWidth; - } - - public RocketComponent[] getSelection() { return selection; } @@ -136,14 +116,14 @@ public class RocketFigure extends AbstractScaleFigure { } public Transformation getRotateTransformation() { - return transformation; + return axialRotation; } public void setRotation(double rot) { if (MathUtil.equals(rotation, rot)) return; this.rotation = rot; - this.transformation = Transformation.rotate_x(rotation); + this.axialRotation = Transformation.rotate_x(rotation); updateFigure(); } @@ -163,22 +143,6 @@ public class RocketFigure extends AbstractScaleFigure { } - /** - * Updates the figure shapes and figure size. - */ - @Override - public void updateFigure() { - figureShapes.clear(); - - calculateSize(); - - getShapeTree( this.figureShapes, rocket, this.transformation, Coordinate.ZERO); - - repaint(); - fireChangeEvent(); - } - - public void addRelativeExtra(FigureElement p) { relativeExtra.add(p); } @@ -219,49 +183,15 @@ public class RocketFigure extends AbstractScaleFigure { AffineTransform baseTransform = g2.getTransform(); - // Update figure shapes if necessary - if (figureShapes == null) - updateFigure(); + updateSubjectDimensions(); + updateCanvasOrigin(); + updateCanvasSize(); + updateTransform(); + + figureShapes.clear(); + updateShapeTree( this.figureShapes, rocket, this.axialRotation, Coordinate.ZERO); - - double tx, ty; - // Calculate translation for figure centering - if (figureWidthPx + 2 * borderPixelsWidth < getWidth()) { - - // Figure fits in the viewport - if (currentViewType == RocketPanel.VIEW_TYPE.BackView){ - tx = getWidth() / 2; - }else{ - tx = (getWidth() - figureWidthPx) / 2 - minX * scale; - } - } else { - - // Figure does not fit in viewport - if (currentViewType == RocketPanel.VIEW_TYPE.BackView){ - tx = borderPixelsWidth + figureWidthPx / 2; - }else{ - tx = borderPixelsWidth - minX * scale; - } - } - - ty = computeTy(figureHeightPx); - - if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) { - // Origin has changed, fire event - translateX = tx; - translateY = ty; - fireChangeEvent(); - } - - - // Calculate and store the transformation used - // (inverse is used in detecting clicks on objects) - g2transformation = new AffineTransform(); - g2transformation.translate(translateX, translateY); - // Mirror position Y-axis upwards - g2transformation.scale(scale / EXTRA_SCALE, -scale / EXTRA_SCALE); - - g2.transform(g2transformation); + g2.transform(projection); // Set rendering hints appropriately g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, @@ -378,22 +308,11 @@ public class RocketFigure extends AbstractScaleFigure { } - protected double computeTy(int heightPx) { - final double ty; - if (heightPx + 2 * borderPixelsHeight < getHeight()) { - ty = getHeight() / 2; - } else { - ty = borderPixelsHeight + heightPx / 2; - } - return ty; - } - - public RocketComponent[] getComponentsByPoint(double x, double y) { // Calculate point in shapes' coordinates Point2D.Double p = new Point2D.Double(x, y); try { - g2transformation.inverseTransform(p, p); + projection.inverseTransform(p, p); } catch (NoninvertibleTransformException e) { return new RocketComponent[0]; } @@ -408,52 +327,54 @@ public class RocketFigure extends AbstractScaleFigure { return l.toArray(new RocketComponent[0]); } - // NOTE: Recursive function - private void getShapeTree( - ArrayList allShapes, // output parameter - final RocketComponent comp, - final Transformation parentTransform, - final Coordinate parentLocation){ + // NOTE: Recursive function + private ArrayList updateShapeTree( + ArrayList allShapes, // output parameter + final RocketComponent comp, + final Transformation parentTransform, + final Coordinate parentLocation){ - - final int instanceCount = comp.getInstanceCount(); - Coordinate[] instanceLocations = comp.getInstanceLocations(); - instanceLocations = parentTransform.transform( instanceLocations ); - double[] instanceAngles = comp.getInstanceAngles(); - if( instanceLocations.length != instanceAngles.length ){ - throw new ArrayIndexOutOfBoundsException(String.format("lengths of location array (%d) and angle arrays (%d) differs! (in: %s) ", instanceLocations.length, instanceAngles.length, comp.getName())); - } - - // iterate over the aggregated instances *for the whole* tree. - for( int index = 0; instanceCount > index ; ++index ){ - final double currentAngle = instanceAngles[index]; - Transformation currentTransform = parentTransform; - if( 0.00001 < Math.abs( currentAngle )) { - Transformation currentAngleTransform = Transformation.rotate_x( currentAngle ); - currentTransform = currentAngleTransform.applyTransformation( parentTransform ); - } - - Coordinate currentLocation = parentLocation.add( instanceLocations[index] ); - -// System.err.println(String.format("@%s: %s -- inst: [%d/%d]", comp.getClass().getSimpleName(), comp.getName(), index+1, instanceCount)); -// System.err.println(String.format(" -- stage: %d, active: %b, config: (%d) %s", comp.getStageNumber(), this.getConfiguration().isComponentActive(comp), this.getConfiguration().instanceNumber, this.getConfiguration().getId())); -// System.err.println(String.format(" -- %s + %s = %s", parentLocation.toString(), instanceLocations[index].toString(), currentLocation.toString())); -// if( 0.00001 < Math.abs( currentAngle )) { -// System.err.println(String.format(" -- at: %6.4f radians", currentAngle)); -// } - - // generate shape for this component, if active - if( this.rocket.getSelectedConfiguration().isComponentActive( comp )){ - allShapes = addThisShape( allShapes, this.currentViewType, comp, currentLocation, currentTransform); - } - - // recurse into component's children - for( RocketComponent child: comp.getChildren() ){ - // draw a tree for each instance subcomponent - getShapeTree( allShapes, child, currentTransform, currentLocation ); - } - } + final int instanceCount = comp.getInstanceCount(); + Coordinate[] instanceLocations = comp.getInstanceLocations(); + instanceLocations = parentTransform.transform( instanceLocations ); + double[] instanceAngles = comp.getInstanceAngles(); + if( instanceLocations.length != instanceAngles.length ){ + throw new ArrayIndexOutOfBoundsException(String.format("lengths of location array (%d) and angle arrays (%d) differs! (in: %s) ", instanceLocations.length, instanceAngles.length, comp.getName())); + } + + // iterate over the aggregated instances *for the whole* tree. + for( int index = 0; instanceCount > index ; ++index ){ + final double currentAngle = instanceAngles[index]; + + Transformation currentTransform = parentTransform; + if( 0.00001 < Math.abs( currentAngle )) { + Transformation currentAngleTransform = Transformation.rotate_x( currentAngle ); + currentTransform = currentAngleTransform.applyTransformation( parentTransform ); + } + + Coordinate currentLocation = parentLocation.add( instanceLocations[index] ); + + // System.err.println(String.format("@%s: %s -- inst: [%d/%d]", comp.getClass().getSimpleName(), comp.getName(), index+1, instanceCount)); + // System.err.println(String.format(" -- stage: %d, active: %b, config: (%d) %s", comp.getStageNumber(), this.getConfiguration().isComponentActive(comp), this.getConfiguration().instanceNumber, this.getConfiguration().getId())); + // System.err.println(String.format(" -- %s + %s = %s", parentLocation.toString(), instanceLocations[index].toString(), currentLocation.toString())); + // if( 0.00001 < Math.abs( currentAngle )) { + // System.err.println(String.format(" -- at: %6.4f radians", currentAngle)); + // } + + // generate shape for this component, if active + if( this.rocket.getSelectedConfiguration().isComponentActive( comp )){ + allShapes = addThisShape( allShapes, this.currentViewType, comp, currentLocation, currentTransform); + } + + // recurse into component's children + for( RocketComponent child: comp.getChildren() ){ + // draw a tree for each instance subcomponent + updateShapeTree( allShapes, child, currentTransform, currentLocation ); + } + } + + return allShapes; } /** @@ -508,82 +429,50 @@ public class RocketFigure extends AbstractScaleFigure { - /** - * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions. - * The bounds are stored in the variables minX, maxX and maxR. - */ - private void calculateFigureBounds() { - Collection bounds = rocket.getSelectedConfiguration().getBounds(); - - if (bounds.isEmpty()) { - minX = 0; - maxX = 0; - maxR = 0; - return; - } - - minX = Double.MAX_VALUE; - maxX = Double.MIN_VALUE; - maxR = 0; - for (Coordinate c : bounds) { - double x = c.x, r = MathUtil.hypot(c.y, c.z); - if (x < minX) - minX = x; - if (x > maxX) - maxX = x; - if (r > maxR) - maxR = r; - } - } - -// public double getBestZoom(Rectangle2D bounds) { -// double zh = 1, zv = 1; -// if (bounds.getWidth() > 0.0001) -// zh = (getWidth() - 2 * borderPixelsWidth) / bounds.getWidth(); -// if (bounds.getHeight() > 0.0001) -// zv = (getHeight() - 2 * borderPixelsHeight) / bounds.getHeight(); -// return Math.min(zh, zv); -// } -// - - + /** + * Gets the bounds of the drawn subject in Model-Space + * + * i.e. the maximum extents in the selected dimensions. + * The bounds are stored in the variables minX, maxX and maxR. + * + * @return + */ + @Override + protected void updateSubjectDimensions() { + // calculate bounds, and store in class variables + final BoundingBox bounds = rocket.getSelectedConfiguration().getBoundingBox(); + + switch (currentViewType) { + case SideView: + subjectBounds_m = new Rectangle2D.Double(bounds.min.x, bounds.min.y, bounds.span().x, bounds.span().y); + break; + case BackView: + final double maxR = Math.max(Math.hypot(bounds.min.y, bounds.min.z), Math.hypot(bounds.max.y, bounds.max.z)); + subjectBounds_m = new Rectangle2D.Double(-maxR, -maxR, 2 * maxR, 2 * maxR); + break; + default: + throw new BugException("Illegal figure type = " + currentViewType); + } + } + /** * Calculates the necessary size of the figure and set the PreferredSize * property accordingly. */ - private void calculateSize() { - Rectangle2D dimensions = this.getDimensions(); - - figureHeight = dimensions.getHeight(); - figureWidth = dimensions.getWidth(); - - figureWidthPx = (int) (figureWidth * scale); - figureHeightPx = (int) (figureHeight * scale); - - Dimension dpx = new Dimension( - figureWidthPx + 2 * borderPixelsWidth, - figureHeightPx + 2 * borderPixelsHeight); - - if (!dpx.equals(getPreferredSize()) || !dpx.equals(getMinimumSize())) { - setPreferredSize(dpx); - setMinimumSize(dpx); - revalidate(); - } + @Override + protected void updateCanvasOrigin() { + + final Dimension subjectArea = new Dimension((int)(subjectBounds_m.getWidth()*scale), + (int)(subjectBounds_m.getHeight()*scale)); + + final int newOriginY = borderThickness_px.height + (int)(subjectArea.getHeight() / 2); + if (currentViewType == RocketPanel.VIEW_TYPE.BackView){ + int newOriginX = borderThickness_px.width + getWidth() / 2; + originLocation_px = new Dimension(newOriginX, newOriginY); + }else { + int newOriginX = borderThickness_px.width + (getWidth() - subjectArea.width) / 2; + originLocation_px = new Dimension(newOriginX, newOriginY); + } } - - public Rectangle2D getDimensions() { - calculateFigureBounds(); - - switch (currentViewType) { - case SideView: - return new Rectangle2D.Double(minX, -maxR, maxX - minX, 2 * maxR); - - case BackView: - return new Rectangle2D.Double(-maxR, -maxR, 2 * maxR, 2 * maxR); - - default: - throw new BugException("Illegal figure type = " + currentViewType); - } - } - + } diff --git a/swing/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java b/swing/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java deleted file mode 100644 index 48440fe33..000000000 --- a/swing/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java +++ /dev/null @@ -1,82 +0,0 @@ -package net.sf.openrocket.gui.scalefigure; - -import java.awt.Dimension; - -import net.sf.openrocket.util.ChangeSource; - - -public interface ScaleFigure extends ChangeSource { - - /** - * Extra scaling applied to the figure. The f***ing Java JRE doesn't know - * how to draw shapes when using very large scaling factors, so this must - * be manually applied to every single shape used. - *

- * The scaling factor used is divided by this value, and every coordinate used - * in the figures must be multiplied by this factor. - */ - public static final double EXTRA_SCALE = 1000; - - /** - * Shorthand for {@link #EXTRA_SCALE}. - */ - public static final double S = EXTRA_SCALE; - - - /** - * Set the scale level of the figure. A scale value of 1.0 indicates an original - * size when using the current DPI level. - * - * @param scale the scale level. - */ - public void setScaling(double scale); - - - /** - * Set the scale level so that the figure fits into the given bounds. - * - * @param bounds the bounds of the figure. - */ - public void setScaling(Dimension bounds); - - - /** - * Return the scale level of the figure. A scale value of 1.0 indicates an original - * size when using the current DPI level. - * - * @return the current scale level. - */ - public double getScaling(); - - - /** - * Return the scale of the figure on px/m. - * - * @return the current scale value. - */ - public double getAbsoluteScale(); - - - /** - * Return the pixel coordinates of the figure origin. - * - * @return the pixel coordinates of the figure origin. - */ - public Dimension getOrigin(); - - - /** - * Get the amount of blank space left around the figure. - * - * @return the amount of horizontal and vertical space left on both sides of the figure. - */ - public Dimension getBorderPixels(); - - /** - * Set the amount of blank space left around the figure. - * - * @param width the amount of horizontal space left on both sides of the figure. - * @param height the amount of vertical space left on both sides of the figure. - */ - public void setBorderPixels(int width, int height); -} diff --git a/swing/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java b/swing/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java index a45dd51a8..bc973938d 100644 --- a/swing/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java +++ b/swing/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java @@ -17,7 +17,6 @@ import java.util.EventObject; import javax.swing.BorderFactory; import javax.swing.JComponent; -import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.ScrollPaneConstants; import javax.swing.event.ChangeEvent; @@ -29,6 +28,7 @@ import net.sf.openrocket.unit.Tick; import net.sf.openrocket.unit.Unit; import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.StateChangeListener; @@ -44,6 +44,7 @@ import net.sf.openrocket.util.StateChangeListener; * * @author Sampo Niskanen */ +@SuppressWarnings("serial") public class ScaleScrollPane extends JScrollPane implements MouseListener, MouseMotionListener { @@ -51,45 +52,33 @@ public class ScaleScrollPane extends JScrollPane public static final int MINOR_TICKS = 3; public static final int MAJOR_TICKS = 30; + public static final String USER_SCALE_PROPERTY = "UserScale"; - private JComponent component; - private ScaleFigure figure; + private final JComponent component; + private final AbstractScaleFigure figure; private DoubleModel rulerUnit; private Ruler horizontalRuler; private Ruler verticalRuler; - private final boolean allowFit; - + // is the subject *currently* being fitting private boolean fit = false; - - /** - * Create a scale scroll pane that allows fitting. - * - * @param component the component to contain (must implement ScaleFigure) - */ - public ScaleScrollPane(JComponent component) { - this(component, true); - } - /** * Create a scale scroll pane. * * @param component the component to contain (must implement ScaleFigure) * @param allowFit whether automatic fitting of the figure is allowed */ - public ScaleScrollPane(JComponent component, boolean allowFit) { + public ScaleScrollPane(final JComponent component) { super(component); - if (!(component instanceof ScaleFigure)) { + if (!(component instanceof AbstractScaleFigure)) { throw new IllegalArgumentException("component must implement ScaleFigure"); } this.component = component; - this.figure = (ScaleFigure) component; - this.allowFit = allowFit; - + this.figure = (AbstractScaleFigure) component; rulerUnit = new DoubleModel(0.0, UnitGroup.UNITS_LENGTH); rulerUnit.addChangeListener(new ChangeListener() { @@ -106,50 +95,45 @@ public class ScaleScrollPane extends JScrollPane UnitSelector selector = new UnitSelector(rulerUnit); selector.setFont(new Font("SansSerif", Font.PLAIN, 8)); this.setCorner(JScrollPane.UPPER_LEFT_CORNER, selector); - this.setCorner(JScrollPane.UPPER_RIGHT_CORNER, new JPanel()); - this.setCorner(JScrollPane.LOWER_LEFT_CORNER, new JPanel()); - this.setCorner(JScrollPane.LOWER_RIGHT_CORNER, new JPanel()); this.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY)); - + setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + viewport.addMouseListener(this); viewport.addMouseMotionListener(this); figure.addChangeListener(new StateChangeListener() { @Override public void stateChanged(EventObject e) { - horizontalRuler.updateSize(); + horizontalRuler.updateSize(); verticalRuler.updateSize(); - if (fit) { - setFitting(true); - } + if(fit) { + figure.scaleTo(viewport.getExtentSize()); + } } }); viewport.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { - if (fit) { - setFitting(true); - } + if(fit) { + figure.scaleTo(viewport.getExtentSize()); + } + figure.updateFigure(); + + horizontalRuler.updateSize(); + verticalRuler.updateSize(); } }); } - public ScaleFigure getFigure() { + public AbstractScaleFigure getFigure() { return figure; } - - /** - * Return whether automatic fitting of the figure is allowed. - */ - public boolean isFittingAllowed() { - return allowFit; - } - /** * Return whether the figure is currently automatically fitted within the component bounds. */ @@ -159,54 +143,69 @@ public class ScaleScrollPane extends JScrollPane /** * Set whether the figure is automatically fitted within the component bounds. - * - * @throws BugException if automatic fitting is disallowed and fit is true */ - public void setFitting(boolean fit) { - if (fit && !allowFit) { - throw new BugException("Attempting to fit figure not allowing fit."); - } - this.fit = fit; - if (fit) { - setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); - setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); + public void setFitting(final boolean shouldFit) { + this.fit = shouldFit; + if (shouldFit) { validate(); - Dimension view = viewport.getExtentSize(); - figure.setScaling(view); - } else { - setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); - setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + + Dimension view = viewport.getExtentSize(); + figure.scaleTo(view); + this.firePropertyChange( USER_SCALE_PROPERTY, 1.0, figure.getUserScale()); + + revalidate(); } } - - - public double getScaling() { - return figure.getScaling(); + public double getUserScale() { + return figure.getUserScale(); } - public double getScale() { - return figure.getAbsoluteScale(); - } - - public void setScaling(double scale) { - if (fit) { - setFitting(false); - } - figure.setScaling(scale); - horizontalRuler.repaint(); - verticalRuler.repaint(); + public void setScaling(final double newScale) { + // match if closer than 1%: + if( MathUtil.equals(newScale, figure.getUserScale(), 0.01)){ + return; + } + + // if explicitly setting a zoom level, turn off fitting + this.fit = false; + figure.scaleTo(newScale); + + revalidate(); } public Unit getCurrentUnit() { return rulerUnit.getCurrentUnit(); } - + + public String toViewportString(){ + Rectangle view = this.getViewport().getViewRect(); + return ("Viewport::("+view.getWidth()+","+view.getHeight()+")" + +"@("+view.getX()+", "+view.getY()+")"); + } + + @Override + public void revalidate() { + if( null != component ) { + component.revalidate(); + figure.updateFigure(); + } + + if( null != horizontalRuler ){ + horizontalRuler.revalidate(); + horizontalRuler.repaint(); + } + if( null != verticalRuler ){ + verticalRuler.revalidate(); + verticalRuler.repaint(); + } + + super.revalidate(); + } + //////////////// Mouse handlers //////////////// - - private int dragStartX = 0; private int dragStartY = 0; private Rectangle dragRectangle = null; @@ -288,27 +287,25 @@ public class ScaleScrollPane extends JScrollPane repaint(); } - private double fromPx(int px) { - Dimension origin = figure.getOrigin(); - if (orientation == HORIZONTAL) { - px -= origin.width; - } else { - // px = -(px - origin.height); - px -= origin.height; - } - return px / figure.getAbsoluteScale(); + private double fromPx(final int px) { + Dimension origin = figure.getSubjectOrigin(); + double realValue = Double.NaN; + if (orientation == HORIZONTAL) { + realValue = px - origin.width; + } else { + realValue = origin.height - px; + } + return realValue / figure.getAbsoluteScale(); } - private int toPx(double l) { - Dimension origin = figure.getOrigin(); - int px = (int) (l * figure.getAbsoluteScale() + 0.5); + private int toPx(final double value) { + final Dimension origin = figure.getSubjectOrigin(); + final int px = (int) (value * figure.getAbsoluteScale() + 0.5); if (orientation == HORIZONTAL) { - px += origin.width; + return (px + origin.width); } else { - px = px + origin.height; - // px += origin.height; + return (origin.height - px); } - return px; } @@ -322,8 +319,7 @@ public class ScaleScrollPane extends JScrollPane // Fill area with background color g2.setColor(getBackground()); g2.fillRect(area.x, area.y, area.width, area.height + 100); - - + int startpx, endpx; if (orientation == HORIZONTAL) { startpx = area.x; @@ -337,11 +333,19 @@ public class ScaleScrollPane extends JScrollPane double start, end, minor, major; start = fromPx(startpx); end = fromPx(endpx); + minor = MINOR_TICKS / figure.getAbsoluteScale(); major = MAJOR_TICKS / figure.getAbsoluteScale(); - - Tick[] ticks = unit.getTicks(start, end, minor, major); - + + Tick[] ticks = null; + if( VERTICAL == orientation ){ + // the parameters are *intended* to be backwards: because 'getTicks(...)' can only + // create increasing arrays (where the start < end) + ticks = unit.getTicks(end, start, minor, major); + }else if(HORIZONTAL == orientation ){ + // normal parameter order + ticks = unit.getTicks(start, end, minor, major); + } // Set color & hints g2.setColor(Color.BLACK); diff --git a/swing/src/net/sf/openrocket/gui/scalefigure/ScaleSelector.java b/swing/src/net/sf/openrocket/gui/scalefigure/ScaleSelector.java index 7fd68c0d2..71cc55b03 100644 --- a/swing/src/net/sf/openrocket/gui/scalefigure/ScaleSelector.java +++ b/swing/src/net/sf/openrocket/gui/scalefigure/ScaleSelector.java @@ -4,7 +4,6 @@ import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.text.DecimalFormat; -import java.util.Arrays; import java.util.EventObject; import java.util.Locale; @@ -19,6 +18,9 @@ import net.sf.openrocket.util.StateChangeListener; @SuppressWarnings("serial") public class ScaleSelector extends JPanel { + public static final double MINIMUM_ZOOM = 0.01; // == 1 % + public static final double MAXIMUM_ZOOM = 1000.00; // == 10,000 % + // Ready zoom settings private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("0.#%"); @@ -45,19 +47,16 @@ public class ScaleSelector extends JPanel { button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - double scale = scrollPane.getScaling(); - scale = getNextLargerScale(scale); - scrollPane.setScaling(scale); + final double oldScale = scrollPane.getUserScale(); + final double newScale = getNextLargerScale(oldScale); + scrollPane.setScaling(newScale); } }); add(button, "gap"); // Zoom level selector String[] settings = SCALE_LABELS; - if (!scrollPane.isFittingAllowed()) { - settings = Arrays.copyOf(settings, settings.length - 1); - } - + scaleSelector = new JComboBox<>(settings); scaleSelector.setEditable(true); setZoomText(); @@ -68,8 +67,7 @@ public class ScaleSelector extends JPanel { String text = (String) scaleSelector.getSelectedItem(); text = text.replaceAll("%", "").trim(); - if (text.toLowerCase(Locale.getDefault()).startsWith(SCALE_FIT.toLowerCase(Locale.getDefault())) && - scrollPane.isFittingAllowed()) { + if (text.toLowerCase(Locale.getDefault()).startsWith(SCALE_FIT.toLowerCase(Locale.getDefault()))){ scrollPane.setFitting(true); setZoomText(); return; @@ -101,7 +99,7 @@ public class ScaleSelector extends JPanel { button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - double scale = scrollPane.getScaling(); + double scale = scrollPane.getUserScale(); scale = getNextSmallerScale(scale); scrollPane.setScaling(scale); } @@ -111,7 +109,8 @@ public class ScaleSelector extends JPanel { } private void setZoomText() { - String text = PERCENT_FORMAT.format(scrollPane.getScaling()); + final double userScale = scrollPane.getUserScale(); + String text = PERCENT_FORMAT.format(userScale); if (scrollPane.isFitting()) { text = "Fit (" + text + ")"; }