[refactor] Reduce redundant methods in Scalefigures, and harmonize common function names

- removed interface that was only inherited by the single AbstractBaseClass
- harmonizes  the border pixels variables in the scalefigure package
This commit is contained in:
Daniel_M_Williams 2018-06-24 19:01:28 -04:00
parent 9d76ece128
commit 885df6ce04
10 changed files with 625 additions and 741 deletions

View File

@ -367,7 +367,7 @@ public class FreeformFinSetConfig extends FinSetConfig {
private int dragIndex = -1; private int dragIndex = -1;
private FinPointScrollPane( final FinPointFigure _figure) { private FinPointScrollPane( final FinPointFigure _figure) {
super( _figure, true); super( _figure);
} }
@Override @Override

View File

@ -506,7 +506,6 @@ public class GeneralOptimizationDialog extends JDialog {
// // Rocket figure // // Rocket figure
figure = new RocketFigure( getSelectedSimulation().getRocket() ); figure = new RocketFigure( getSelectedSimulation().getRocket() );
figure.setBorderPixels(1, 1);
ScaleScrollPane figureScrollPane = new ScaleScrollPane(figure); ScaleScrollPane figureScrollPane = new ScaleScrollPane(figure);
figureScrollPane.setFitting(true); figureScrollPane.setFitting(true);
panel.add(figureScrollPane, "span, split, height 200lp, grow"); panel.add(figureScrollPane, "span, split, height 200lp, grow");

View File

@ -177,7 +177,7 @@ public class DesignReport {
canvas.beginText(); canvas.beginText();
canvas.setFontAndSize(ITextHelper.getBaseFont(), PrintUtilities.NORMAL_FONT_SIZE); 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))); .toPoints(1)));
final int diagramHeight = pageImageableHeight * 2 - 70 - (figHeightPts); final int diagramHeight = pageImageableHeight * 2 - 70 - (figHeightPts);
canvas.moveText(document.leftMargin() + pageSize.getBorderWidthLeft(), diagramHeight); canvas.moveText(document.leftMargin() + pageSize.getBorderWidthLeft(), diagramHeight);
@ -274,7 +274,7 @@ public class DesignReport {
theFigure.updateFigure(); theFigure.updateFigure();
double scale = double scale =
(thePageImageableWidth * 2.2) / theFigure.getFigureWidth(); (thePageImageableWidth * 2.2) / theFigure.getWidth();
theFigure.setScale(scale); 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 * 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; 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 //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. //to allow for this.
if (theFigure.getDimensions().getY() < 0.0d) { if (theFigure.getHeight() < 0.0d) {
y += (int) halfFigureHeight; y += (int) halfFigureHeight;
} }
g2d.translate(20, y); g2d.translate(20, y);

View File

@ -22,18 +22,12 @@ public class PrintFigure extends RocketFigure {
super(rkt); super(rkt);
} }
@Override
protected double computeTy(int heightPx) {
super.computeTy(heightPx);
return 0;
}
public void setScale(final double theScale) { public void setScale(final double theScale) {
this.scale = theScale; //dpi/0.0254*scaling; this.scale = theScale; //dpi/0.0254*scaling;
updateFigure(); updateFigure();
} }
public double getFigureHeightPx() { public double getFigureHeightPx() {
return this.figureHeightPx; return this.getSize().height;
} }
} }

View File

@ -2,6 +2,8 @@ package net.sf.openrocket.gui.scalefigure;
import java.awt.Color; import java.awt.Color;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.EventListener; import java.util.EventListener;
import java.util.EventObject; import java.util.EventObject;
import java.util.LinkedList; import java.util.LinkedList;
@ -9,124 +11,194 @@ import java.util.List;
import javax.swing.JPanel; import javax.swing.JPanel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.sf.openrocket.gui.util.GUIUtil; import net.sf.openrocket.gui.util.GUIUtil;
import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.StateChangeListener; import net.sf.openrocket.util.StateChangeListener;
@SuppressWarnings("serial") @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.
* <p>
* 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 // 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_WIDTH = 30;
private static final int DEFAULT_BORDER_PIXELS_HEIGHT = 20; 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 static final Dimension borderThickness_px = new Dimension(DEFAULT_BORDER_PIXELS_WIDTH, DEFAULT_BORDER_PIXELS_HEIGHT);
protected Dimension originLocation_px = new Dimension(0,0);
protected double scale = 1.0;
protected double scaling = 1.0;
protected int borderPixelsWidth = DEFAULT_BORDER_PIXELS_WIDTH;
protected int borderPixelsHeight = DEFAULT_BORDER_PIXELS_HEIGHT;
// ======= 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<EventListener> listeners = new LinkedList<EventListener>(); protected final List<EventListener> listeners = new LinkedList<EventListener>();
public AbstractScaleFigure() { public AbstractScaleFigure() {
this.dpi = GUIUtil.getDPI(); // produces a pixels-per-meter scale factor
this.scaling = 1.0; //
this.scale = dpi / 0.0254 * scaling; // 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); setBackground(Color.WHITE);
setOpaque(true); setOpaque(true);
} }
public double getUserScale(){
return userScale;
public abstract void updateFigure();
public abstract double getFigureWidth();
public abstract double getFigureHeight();
@Override
public double getScaling() {
return scaling;
} }
@Override public double getAbsoluteScale() {
public double getAbsoluteScale() { return scale;
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) { * Set the scale level to display newBounds
if (Double.isInfinite(scaling) || Double.isNaN(scaling)) *
scaling = 1.0; * @param bounds the bounds of the figure.
if (scaling < 0.001) */
scaling = 0.001; public void scaleTo(Dimension newBounds) {
if (scaling > 1000) if( 0 == newBounds.getWidth() || 0 == newBounds.getHeight())
scaling = 1000; return;
if (Math.abs(this.scaling - scaling) < 0.01)
return; updateSubjectDimensions();
this.scaling = scaling; updateCanvasOrigin();
this.scale = dpi / 0.0254 * scaling; updateCanvasSize();
updateFigure(); updateTransform();
}
// dimensions within the viewable area, which are available to draw
@Override final int drawable_width_px = newBounds.width - 2 * borderThickness_px.width;
public void setScaling(Dimension bounds) { final int drawable_height_px = newBounds.height - 2 * borderThickness_px.height;
double zh = 1, zv = 1;
int w = bounds.width - 2 * borderPixelsWidth - 20; if(( 0 < drawable_width_px ) && ( 0 < drawable_height_px)) {
int h = bounds.height - 2 * borderPixelsHeight - 20; final double width_scale = (drawable_width_px) / ( subjectBounds_m.getWidth() * baseScale);
final double height_scale = (drawable_height_px) / ( subjectBounds_m.getHeight() * baseScale);
if (w < 10) final double minScale = Math.min(height_scale, width_scale);
w = 10;
if (h < 10) scaleTo(minScale);
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;
} }
}
/**
* 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) { public void addChangeListener(StateChangeListener listener) {
listeners.add(0, listener); listeners.add(0, listener);
} }
@Override
public void removeChangeListener(StateChangeListener listener) { public void removeChangeListener(StateChangeListener listener) {
listeners.remove(listener); listeners.remove(listener);
} }
private EventObject changeEvent = null;
protected void fireChangeEvent() { protected void fireChangeEvent() {
if (changeEvent == null) final EventObject changeEvent = new EventObject(this);
changeEvent = new EventObject(this);
// Copy the list before iterating to prevent concurrent modification exceptions. // Copy the list before iterating to prevent concurrent modification exceptions.
EventListener[] list = listeners.toArray(new EventListener[0]); EventListener[] list = listeners.toArray(new EventListener[0]);
for (EventListener l : list) { for (EventListener l : list) {
@ -135,5 +207,5 @@ public abstract class AbstractScaleFigure extends JPanel implements ScaleFigure
} }
} }
} }
} }

View File

@ -13,96 +13,73 @@ import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Path2D; import java.awt.geom.Path2D;
import java.awt.geom.Point2D; import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D; 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.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.Tick;
import net.sf.openrocket.unit.Unit; import net.sf.openrocket.unit.Unit;
import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.BoundingBox;
import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.StateChangeListener;
// TODO: MEDIUM: the figure jumps and bugs when using automatic fitting
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class FinPointFigure extends AbstractScaleFigure { public class FinPointFigure extends AbstractScaleFigure {
private static final int BOX_SIZE = 4; private final static Logger log = LoggerFactory.getLogger(FinPointFigure.class);
private final FreeformFinSet finset;
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 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<StateChangeListener> listeners = new LinkedList<StateChangeListener>();
private Rectangle2D.Double[] finPointHandles = null;
public FinPointFigure(FreeformFinSet finset) { public FinPointFigure(FreeformFinSet finset) {
this.finset = finset; this.finset = finset;
// useful for debugging -- shows a contrast against un-drawn space.
setBackground(Color.WHITE);
setOpaque(true);
updateTransform();
} }
@Override @Override
public void paintComponent(Graphics g) { public void paintComponent(Graphics g) {
super.paintComponent(g); super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g; Graphics2D g2 = (Graphics2D) g.create();
if (modID != finset.getRocket().getAerodynamicModID()) { if (modID != finset.getRocket().getAerodynamicModID()) {
modID = finset.getRocket().getAerodynamicModID(); modID = finset.getRocket().getAerodynamicModID();
calculateDimensions(); updateTransform();
} }
g2.transform(projection);
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);
// Set rendering hints appropriately // Set rendering hints appropriately
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
@ -113,128 +90,184 @@ public class FinPointFigure extends AbstractScaleFigure {
RenderingHints.VALUE_ANTIALIAS_ON); RenderingHints.VALUE_ANTIALIAS_ON);
// Background grid
paintBackgroundGrid( g2);
Rectangle visible = g2.getClipBounds(); paintRocketBody(g2);
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]);
}
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) { final float grid_line_width = (float)(FinPointFigure.GRID_LINE_BASE_WIDTH/this.scale);
if (handles == null) g2.setStroke(new BasicStroke( grid_line_width,
return -1; BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
g2.setColor(FinPointFigure.GRID_LINE_COLOR);
// Calculate point in shapes' coordinates
Point2D.Double p = new Point2D.Double(x, y); Unit unit;
try { if (this.getParent() != null && this.getParent().getParent() instanceof ScaleScrollPane) {
transform.inverseTransform(p, p); unit = ((ScaleScrollPane) this.getParent().getParent()).getCurrentUnit();
} catch (NoninvertibleTransformException e) { } else {
return -1; unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
} }
for (int i = 0; i < handles.length; i++) { // vertical
if (handles[i].contains(p)) Tick[] verticalTicks = unit.getTicks(x0, x1, MINOR_TICKS, MAJOR_TICKS);
return i; Line2D.Double line = new Line2D.Double();
} for (Tick t : verticalTicks) {
return -1; 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) { public int getSegmentByPoint(double x, double y) {
if (handles == null) if (finPointHandles == null)
return -1; return -1;
// Calculate point in shapes' coordinates // Calculate point in shapes' coordinates
Point2D.Double p = new Point2D.Double(x, y); Point2D.Double p = new Point2D.Double(x, y);
try { try {
transform.inverseTransform(p, p); projection.inverseTransform(p, p);
} catch (NoninvertibleTransformException e) { } catch (NoninvertibleTransformException e) {
return -1; return -1;
} }
double x0 = p.x / EXTRA_SCALE; double x0 = p.x;
double y0 = p.y / EXTRA_SCALE; double y0 = p.y;
double delta = BOX_SIZE / scale; double delta = BOX_WIDTH_PIXELS /*/ scale*/;
//System.out.println("Point: " + x0 + "," + y0); //System.out.println("Point: " + x0 + "," + y0);
//System.out.println("delta: " + (BOX_SIZE / scale)); //System.out.println("delta: " + (BOX_SIZE / scale));
@ -262,84 +295,60 @@ public class FinPointFigure extends AbstractScaleFigure {
public Point2D.Double convertPoint(double x, double y) { public Point2D.Double convertPoint(double x, double y) {
Point2D.Double p = new Point2D.Double(x, y); Point2D.Double p = new Point2D.Double(x, y);
try { try {
transform.inverseTransform(p, p); projection.inverseTransform(p, p);
} catch (NoninvertibleTransformException e) { } catch (NoninvertibleTransformException e) {
assert (false) : "Should not occur"; assert (false) : "Should not occur";
return new Point2D.Double(0, 0); return new Point2D.Double(0, 0);
} }
p.setLocation(p.x / EXTRA_SCALE, p.y / EXTRA_SCALE); p.setLocation(p.x, p.y);
return p; return p;
} }
@Override public Dimension getSubjectOrigin() {
public Dimension getOrigin() {
if (modID != finset.getRocket().getAerodynamicModID()) { if (modID != finset.getRocket().getAerodynamicModID()) {
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 @Override
public double getFigureWidth() { protected void updateSubjectDimensions(){
if (modID != finset.getRocket().getAerodynamicModID()) { // update subject bounds
modID = finset.getRocket().getAerodynamicModID(); BoundingBox newBounds = new BoundingBox();
calculateDimensions();
} // subsequent updates can only increase the size of the bounds, so this is the minimum size.
return figureWidth; newBounds.update( MINIMUM_CANVAS_SIZE_METERS);
}
SymmetricComponent parent = (SymmetricComponent)this.finset.getParent();
@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;
Dimension d = new Dimension((int) (figureWidth * scale + 2 * borderPixelsWidth), // N.B.: (0,0) is the fin front-- where it meets the parent body.
(int) (figureHeight * scale + 2 * borderPixelsHeight)); final double xFinFront = finset.asPositionValue(AxialMethod.TOP); //<< in body frame
if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) { // update to bound the parent body:
setPreferredSize(d); final double xParentFront = -xFinFront;
setMinimumSize(d); newBounds.update( xParentFront);
revalidate(); 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 @Override
public void updateFigure() { protected void updateCanvasOrigin() {
repaint(); originLocation_px.width = borderThickness_px.width - (int)(subjectBounds_m.getX()*scale);
} originLocation_px.height = borderThickness_px.height + (int)(subjectBounds_m.getY()*scale);
}
} }

View File

@ -18,8 +18,12 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.sf.openrocket.gui.figureelements.FigureElement; import net.sf.openrocket.gui.figureelements.FigureElement;
import net.sf.openrocket.gui.rocketfigure.RocketComponentShape; 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.ColorConversion;
import net.sf.openrocket.gui.util.SwingPreferences; import net.sf.openrocket.gui.util.SwingPreferences;
import net.sf.openrocket.motor.Motor; 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.Rocket;
import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.startup.Application; import net.sf.openrocket.startup.Application;
import net.sf.openrocket.util.BoundingBox;
import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.Coordinate;
import net.sf.openrocket.util.LineStyle; import net.sf.openrocket.util.LineStyle;
@ -46,6 +51,8 @@ import net.sf.openrocket.util.Transformation;
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class RocketFigure extends AbstractScaleFigure { 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_PACKAGE = "net.sf.openrocket.gui.rocketfigure";
private static final String ROCKET_FIGURE_SUFFIX = "Shapes"; private static final String ROCKET_FIGURE_SUFFIX = "Shapes";
@ -61,28 +68,17 @@ public class RocketFigure extends AbstractScaleFigure {
private Rocket rocket; private Rocket rocket;
private RocketComponent[] selection = new RocketComponent[0]; 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 RocketPanel.VIEW_TYPE currentViewType = RocketPanel.VIEW_TYPE.SideView;
private double rotation; private double rotation;
private Transformation transformation; private Transformation axialRotation;
private double translateX, translateY;
/* /*
* figureComponents contains the corresponding RocketComponents of the figureShapes * figureComponents contains the corresponding RocketComponents of the figureShapes
*/ */
private final ArrayList<RocketComponentShape> figureShapes = new ArrayList<RocketComponentShape>(); private final ArrayList<RocketComponentShape> figureShapes = new ArrayList<RocketComponentShape>();
private double minX = 0, maxX = 0, maxR = 0;
// Figure width and height in SI-units and pixels
private AffineTransform g2transformation = null;
private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>(); private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>();
private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>(); private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>();
@ -96,27 +92,11 @@ public class RocketFigure extends AbstractScaleFigure {
this.rocket = _rkt; this.rocket = _rkt;
this.rotation = 0.0; this.rotation = 0.0;
this.transformation = Transformation.rotate_x(0.0); this.axialRotation = Transformation.rotate_x(0.0);
updateFigure(); 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() { public RocketComponent[] getSelection() {
return selection; return selection;
} }
@ -136,14 +116,14 @@ public class RocketFigure extends AbstractScaleFigure {
} }
public Transformation getRotateTransformation() { public Transformation getRotateTransformation() {
return transformation; return axialRotation;
} }
public void setRotation(double rot) { public void setRotation(double rot) {
if (MathUtil.equals(rotation, rot)) if (MathUtil.equals(rotation, rot))
return; return;
this.rotation = rot; this.rotation = rot;
this.transformation = Transformation.rotate_x(rotation); this.axialRotation = Transformation.rotate_x(rotation);
updateFigure(); 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) { public void addRelativeExtra(FigureElement p) {
relativeExtra.add(p); relativeExtra.add(p);
} }
@ -219,49 +183,15 @@ public class RocketFigure extends AbstractScaleFigure {
AffineTransform baseTransform = g2.getTransform(); AffineTransform baseTransform = g2.getTransform();
// Update figure shapes if necessary updateSubjectDimensions();
if (figureShapes == null) updateCanvasOrigin();
updateFigure(); updateCanvasSize();
updateTransform();
figureShapes.clear();
updateShapeTree( this.figureShapes, rocket, this.axialRotation, Coordinate.ZERO);
g2.transform(projection);
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);
// Set rendering hints appropriately // Set rendering hints appropriately
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 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) { public RocketComponent[] getComponentsByPoint(double x, double y) {
// Calculate point in shapes' coordinates // Calculate point in shapes' coordinates
Point2D.Double p = new Point2D.Double(x, y); Point2D.Double p = new Point2D.Double(x, y);
try { try {
g2transformation.inverseTransform(p, p); projection.inverseTransform(p, p);
} catch (NoninvertibleTransformException e) { } catch (NoninvertibleTransformException e) {
return new RocketComponent[0]; return new RocketComponent[0];
} }
@ -408,52 +327,54 @@ public class RocketFigure extends AbstractScaleFigure {
return l.toArray(new RocketComponent[0]); return l.toArray(new RocketComponent[0]);
} }
// NOTE: Recursive function // NOTE: Recursive function
private void getShapeTree( private ArrayList<RocketComponentShape> updateShapeTree(
ArrayList<RocketComponentShape> allShapes, // output parameter ArrayList<RocketComponentShape> allShapes, // output parameter
final RocketComponent comp, final RocketComponent comp,
final Transformation parentTransform, final Transformation parentTransform,
final Coordinate parentLocation){ 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; final int instanceCount = comp.getInstanceCount();
if( 0.00001 < Math.abs( currentAngle )) { Coordinate[] instanceLocations = comp.getInstanceLocations();
Transformation currentAngleTransform = Transformation.rotate_x( currentAngle ); instanceLocations = parentTransform.transform( instanceLocations );
currentTransform = currentAngleTransform.applyTransformation( parentTransform ); 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()));
Coordinate currentLocation = parentLocation.add( instanceLocations[index] ); }
// System.err.println(String.format("@%s: %s -- inst: [%d/%d]", comp.getClass().getSimpleName(), comp.getName(), index+1, instanceCount)); // iterate over the aggregated instances *for the whole* tree.
// System.err.println(String.format(" -- stage: %d, active: %b, config: (%d) %s", comp.getStageNumber(), this.getConfiguration().isComponentActive(comp), this.getConfiguration().instanceNumber, this.getConfiguration().getId())); for( int index = 0; instanceCount > index ; ++index ){
// System.err.println(String.format(" -- %s + %s = %s", parentLocation.toString(), instanceLocations[index].toString(), currentLocation.toString())); final double currentAngle = instanceAngles[index];
// if( 0.00001 < Math.abs( currentAngle )) {
// System.err.println(String.format(" -- at: %6.4f radians", currentAngle)); Transformation currentTransform = parentTransform;
// } if( 0.00001 < Math.abs( currentAngle )) {
Transformation currentAngleTransform = Transformation.rotate_x( currentAngle );
// generate shape for this component, if active currentTransform = currentAngleTransform.applyTransformation( parentTransform );
if( this.rocket.getSelectedConfiguration().isComponentActive( comp )){ }
allShapes = addThisShape( allShapes, this.currentViewType, comp, currentLocation, currentTransform);
} Coordinate currentLocation = parentLocation.add( instanceLocations[index] );
// recurse into component's children // System.err.println(String.format("@%s: %s -- inst: [%d/%d]", comp.getClass().getSimpleName(), comp.getName(), index+1, instanceCount));
for( RocketComponent child: comp.getChildren() ){ // System.err.println(String.format(" -- stage: %d, active: %b, config: (%d) %s", comp.getStageNumber(), this.getConfiguration().isComponentActive(comp), this.getConfiguration().instanceNumber, this.getConfiguration().getId()));
// draw a tree for each instance subcomponent // System.err.println(String.format(" -- %s + %s = %s", parentLocation.toString(), instanceLocations[index].toString(), currentLocation.toString()));
getShapeTree( allShapes, child, currentTransform, currentLocation ); // 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. * Gets the bounds of the drawn subject in Model-Space
* The bounds are stored in the variables minX, maxX and maxR. *
*/ * i.e. the maximum extents in the selected dimensions.
private void calculateFigureBounds() { * The bounds are stored in the variables minX, maxX and maxR.
Collection<Coordinate> bounds = rocket.getSelectedConfiguration().getBounds(); *
* @return
if (bounds.isEmpty()) { */
minX = 0; @Override
maxX = 0; protected void updateSubjectDimensions() {
maxR = 0; // calculate bounds, and store in class variables
return; final BoundingBox bounds = rocket.getSelectedConfiguration().getBoundingBox();
}
switch (currentViewType) {
minX = Double.MAX_VALUE; case SideView:
maxX = Double.MIN_VALUE; subjectBounds_m = new Rectangle2D.Double(bounds.min.x, bounds.min.y, bounds.span().x, bounds.span().y);
maxR = 0; break;
for (Coordinate c : bounds) { case BackView:
double x = c.x, r = MathUtil.hypot(c.y, c.z); final double maxR = Math.max(Math.hypot(bounds.min.y, bounds.min.z), Math.hypot(bounds.max.y, bounds.max.z));
if (x < minX) subjectBounds_m = new Rectangle2D.Double(-maxR, -maxR, 2 * maxR, 2 * maxR);
minX = x; break;
if (x > maxX) default:
maxX = x; throw new BugException("Illegal figure type = " + currentViewType);
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);
// }
//
/** /**
* Calculates the necessary size of the figure and set the PreferredSize * Calculates the necessary size of the figure and set the PreferredSize
* property accordingly. * property accordingly.
*/ */
private void calculateSize() { @Override
Rectangle2D dimensions = this.getDimensions(); protected void updateCanvasOrigin() {
figureHeight = dimensions.getHeight(); final Dimension subjectArea = new Dimension((int)(subjectBounds_m.getWidth()*scale),
figureWidth = dimensions.getWidth(); (int)(subjectBounds_m.getHeight()*scale));
figureWidthPx = (int) (figureWidth * scale); final int newOriginY = borderThickness_px.height + (int)(subjectArea.getHeight() / 2);
figureHeightPx = (int) (figureHeight * scale); if (currentViewType == RocketPanel.VIEW_TYPE.BackView){
int newOriginX = borderThickness_px.width + getWidth() / 2;
Dimension dpx = new Dimension( originLocation_px = new Dimension(newOriginX, newOriginY);
figureWidthPx + 2 * borderPixelsWidth, }else {
figureHeightPx + 2 * borderPixelsHeight); int newOriginX = borderThickness_px.width + (getWidth() - subjectArea.width) / 2;
originLocation_px = new Dimension(newOriginX, newOriginY);
if (!dpx.equals(getPreferredSize()) || !dpx.equals(getMinimumSize())) { }
setPreferredSize(dpx);
setMinimumSize(dpx);
revalidate();
}
} }
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);
}
}
} }

View File

@ -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.
* <p>
* 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);
}

View File

@ -17,7 +17,6 @@ import java.util.EventObject;
import javax.swing.BorderFactory; import javax.swing.BorderFactory;
import javax.swing.JComponent; import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JScrollPane; import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants; import javax.swing.ScrollPaneConstants;
import javax.swing.event.ChangeEvent; 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.Unit;
import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.StateChangeListener; import net.sf.openrocket.util.StateChangeListener;
@ -44,6 +44,7 @@ import net.sf.openrocket.util.StateChangeListener;
* *
* @author Sampo Niskanen <sampo.niskanen@iki.fi> * @author Sampo Niskanen <sampo.niskanen@iki.fi>
*/ */
@SuppressWarnings("serial")
public class ScaleScrollPane extends JScrollPane public class ScaleScrollPane extends JScrollPane
implements MouseListener, MouseMotionListener { implements MouseListener, MouseMotionListener {
@ -51,45 +52,33 @@ public class ScaleScrollPane extends JScrollPane
public static final int MINOR_TICKS = 3; public static final int MINOR_TICKS = 3;
public static final int MAJOR_TICKS = 30; public static final int MAJOR_TICKS = 30;
public static final String USER_SCALE_PROPERTY = "UserScale";
private JComponent component; private final JComponent component;
private ScaleFigure figure; private final AbstractScaleFigure figure;
private DoubleModel rulerUnit; private DoubleModel rulerUnit;
private Ruler horizontalRuler; private Ruler horizontalRuler;
private Ruler verticalRuler; private Ruler verticalRuler;
private final boolean allowFit; // is the subject *currently* being fitting
private boolean fit = false; 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. * Create a scale scroll pane.
* *
* @param component the component to contain (must implement ScaleFigure) * @param component the component to contain (must implement ScaleFigure)
* @param allowFit whether automatic fitting of the figure is allowed * @param allowFit whether automatic fitting of the figure is allowed
*/ */
public ScaleScrollPane(JComponent component, boolean allowFit) { public ScaleScrollPane(final JComponent component) {
super(component); super(component);
if (!(component instanceof ScaleFigure)) { if (!(component instanceof AbstractScaleFigure)) {
throw new IllegalArgumentException("component must implement ScaleFigure"); throw new IllegalArgumentException("component must implement ScaleFigure");
} }
this.component = component; this.component = component;
this.figure = (ScaleFigure) component; this.figure = (AbstractScaleFigure) component;
this.allowFit = allowFit;
rulerUnit = new DoubleModel(0.0, UnitGroup.UNITS_LENGTH); rulerUnit = new DoubleModel(0.0, UnitGroup.UNITS_LENGTH);
rulerUnit.addChangeListener(new ChangeListener() { rulerUnit.addChangeListener(new ChangeListener() {
@ -106,50 +95,45 @@ public class ScaleScrollPane extends JScrollPane
UnitSelector selector = new UnitSelector(rulerUnit); UnitSelector selector = new UnitSelector(rulerUnit);
selector.setFont(new Font("SansSerif", Font.PLAIN, 8)); selector.setFont(new Font("SansSerif", Font.PLAIN, 8));
this.setCorner(JScrollPane.UPPER_LEFT_CORNER, selector); 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)); this.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY));
setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
viewport.addMouseListener(this); viewport.addMouseListener(this);
viewport.addMouseMotionListener(this); viewport.addMouseMotionListener(this);
figure.addChangeListener(new StateChangeListener() { figure.addChangeListener(new StateChangeListener() {
@Override @Override
public void stateChanged(EventObject e) { public void stateChanged(EventObject e) {
horizontalRuler.updateSize(); horizontalRuler.updateSize();
verticalRuler.updateSize(); verticalRuler.updateSize();
if (fit) { if(fit) {
setFitting(true); figure.scaleTo(viewport.getExtentSize());
} }
} }
}); });
viewport.addComponentListener(new ComponentAdapter() { viewport.addComponentListener(new ComponentAdapter() {
@Override @Override
public void componentResized(ComponentEvent e) { public void componentResized(ComponentEvent e) {
if (fit) { if(fit) {
setFitting(true); figure.scaleTo(viewport.getExtentSize());
} }
figure.updateFigure();
horizontalRuler.updateSize();
verticalRuler.updateSize();
} }
}); });
} }
public ScaleFigure getFigure() { public AbstractScaleFigure getFigure() {
return figure; 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. * 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. * Set whether the figure is automatically fitted within the component bounds.
*
* @throws BugException if automatic fitting is disallowed and <code>fit</code> is <code>true</code>
*/ */
public void setFitting(boolean fit) { public void setFitting(final boolean shouldFit) {
if (fit && !allowFit) { this.fit = shouldFit;
throw new BugException("Attempting to fit figure not allowing fit."); if (shouldFit) {
}
this.fit = fit;
if (fit) {
setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
validate(); validate();
Dimension view = viewport.getExtentSize();
figure.setScaling(view); Dimension view = viewport.getExtentSize();
} else { figure.scaleTo(view);
setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); this.firePropertyChange( USER_SCALE_PROPERTY, 1.0, figure.getUserScale());
setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
revalidate();
} }
} }
public double getUserScale() {
return figure.getUserScale();
public double getScaling() {
return figure.getScaling();
} }
public double getScale() { public void setScaling(final double newScale) {
return figure.getAbsoluteScale(); // match if closer than 1%:
} if( MathUtil.equals(newScale, figure.getUserScale(), 0.01)){
return;
public void setScaling(double scale) { }
if (fit) {
setFitting(false); // if explicitly setting a zoom level, turn off fitting
} this.fit = false;
figure.setScaling(scale); figure.scaleTo(newScale);
horizontalRuler.repaint();
verticalRuler.repaint(); revalidate();
} }
public Unit getCurrentUnit() { public Unit getCurrentUnit() {
return rulerUnit.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 //////////////// //////////////// Mouse handlers ////////////////
private int dragStartX = 0; private int dragStartX = 0;
private int dragStartY = 0; private int dragStartY = 0;
private Rectangle dragRectangle = null; private Rectangle dragRectangle = null;
@ -288,27 +287,25 @@ public class ScaleScrollPane extends JScrollPane
repaint(); repaint();
} }
private double fromPx(int px) { private double fromPx(final int px) {
Dimension origin = figure.getOrigin(); Dimension origin = figure.getSubjectOrigin();
if (orientation == HORIZONTAL) { double realValue = Double.NaN;
px -= origin.width; if (orientation == HORIZONTAL) {
} else { realValue = px - origin.width;
// px = -(px - origin.height); } else {
px -= origin.height; realValue = origin.height - px;
} }
return px / figure.getAbsoluteScale(); return realValue / figure.getAbsoluteScale();
} }
private int toPx(double l) { private int toPx(final double value) {
Dimension origin = figure.getOrigin(); final Dimension origin = figure.getSubjectOrigin();
int px = (int) (l * figure.getAbsoluteScale() + 0.5); final int px = (int) (value * figure.getAbsoluteScale() + 0.5);
if (orientation == HORIZONTAL) { if (orientation == HORIZONTAL) {
px += origin.width; return (px + origin.width);
} else { } else {
px = px + origin.height; return (origin.height - px);
// px += origin.height;
} }
return px;
} }
@ -322,8 +319,7 @@ public class ScaleScrollPane extends JScrollPane
// Fill area with background color // Fill area with background color
g2.setColor(getBackground()); g2.setColor(getBackground());
g2.fillRect(area.x, area.y, area.width, area.height + 100); g2.fillRect(area.x, area.y, area.width, area.height + 100);
int startpx, endpx; int startpx, endpx;
if (orientation == HORIZONTAL) { if (orientation == HORIZONTAL) {
startpx = area.x; startpx = area.x;
@ -337,11 +333,19 @@ public class ScaleScrollPane extends JScrollPane
double start, end, minor, major; double start, end, minor, major;
start = fromPx(startpx); start = fromPx(startpx);
end = fromPx(endpx); end = fromPx(endpx);
minor = MINOR_TICKS / figure.getAbsoluteScale(); minor = MINOR_TICKS / figure.getAbsoluteScale();
major = MAJOR_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 // Set color & hints
g2.setColor(Color.BLACK); g2.setColor(Color.BLACK);

View File

@ -4,7 +4,6 @@ import java.awt.Component;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener; import java.awt.event.ActionListener;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.EventObject; import java.util.EventObject;
import java.util.Locale; import java.util.Locale;
@ -19,6 +18,9 @@ import net.sf.openrocket.util.StateChangeListener;
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class ScaleSelector extends JPanel { 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 // Ready zoom settings
private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("0.#%"); private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("0.#%");
@ -45,19 +47,16 @@ public class ScaleSelector extends JPanel {
button.addActionListener(new ActionListener() { button.addActionListener(new ActionListener() {
@Override @Override
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
double scale = scrollPane.getScaling(); final double oldScale = scrollPane.getUserScale();
scale = getNextLargerScale(scale); final double newScale = getNextLargerScale(oldScale);
scrollPane.setScaling(scale); scrollPane.setScaling(newScale);
} }
}); });
add(button, "gap"); add(button, "gap");
// Zoom level selector // Zoom level selector
String[] settings = SCALE_LABELS; String[] settings = SCALE_LABELS;
if (!scrollPane.isFittingAllowed()) {
settings = Arrays.copyOf(settings, settings.length - 1);
}
scaleSelector = new JComboBox<>(settings); scaleSelector = new JComboBox<>(settings);
scaleSelector.setEditable(true); scaleSelector.setEditable(true);
setZoomText(); setZoomText();
@ -68,8 +67,7 @@ public class ScaleSelector extends JPanel {
String text = (String) scaleSelector.getSelectedItem(); String text = (String) scaleSelector.getSelectedItem();
text = text.replaceAll("%", "").trim(); text = text.replaceAll("%", "").trim();
if (text.toLowerCase(Locale.getDefault()).startsWith(SCALE_FIT.toLowerCase(Locale.getDefault())) && if (text.toLowerCase(Locale.getDefault()).startsWith(SCALE_FIT.toLowerCase(Locale.getDefault()))){
scrollPane.isFittingAllowed()) {
scrollPane.setFitting(true); scrollPane.setFitting(true);
setZoomText(); setZoomText();
return; return;
@ -101,7 +99,7 @@ public class ScaleSelector extends JPanel {
button.addActionListener(new ActionListener() { button.addActionListener(new ActionListener() {
@Override @Override
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
double scale = scrollPane.getScaling(); double scale = scrollPane.getUserScale();
scale = getNextSmallerScale(scale); scale = getNextSmallerScale(scale);
scrollPane.setScaling(scale); scrollPane.setScaling(scale);
} }
@ -111,7 +109,8 @@ public class ScaleSelector extends JPanel {
} }
private void setZoomText() { private void setZoomText() {
String text = PERCENT_FORMAT.format(scrollPane.getScaling()); final double userScale = scrollPane.getUserScale();
String text = PERCENT_FORMAT.format(userScale);
if (scrollPane.isFitting()) { if (scrollPane.isFitting()) {
text = "Fit (" + text + ")"; text = "Fit (" + text + ")";
} }