/*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.osedu.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package tufts.vue;
import tufts.Util;
import java.util.Iterator;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
/**
* This class handles the resizing and/or reposition of both single
* objects, and selected groups of objects. If more than one object is selected,
* the default is to reposition all the objects in the group as the
* the total bounding box of the group is resized, maintaining the relative
* spatial relationship of all the objects in the bounding box.
* As the box is dragged, it uses an ad-hoc algorithm, as
* the control point being moved is not guarnteed to stay exactly under
* the mouse.
*
* @version $Revision: 1.20 $ / $Date: 2007/10/31 08:47:40 $ / $Author: sfraize $
*/
class ResizeControl implements LWSelection.ControlListener, VueConstants
{
// Consider implementing as or optionally as (perhaps depending on shape) a
// point-transforming resize that instead of setting the bounding box & letting
// shape handle it, transforms all the points in the shape manualy. Wouldn't want
// to do this for, say RoundRect, as would throw off corner arcs I think, but,
// polygons > sides 4 and, of course, you'll HAVE to have this if you want to
// support arbitrary polygons!
// TODO: LOCAL_RESIZE still doesn't work for multi-selection on slides (upper offset
// is wrong), and single-reshape of child nodes on the map has now been broken: not
// taking into account scale, nor proper offset of the drag bounds -- e.g., we're
// now using local bounds (local to the parent node of the resizing child node), but
// the drag bounds are relative to the FOCAL, not the parent...
// TODO: LOCAL_RESIZE breaks the resizing of objects that are in groups,
// unless the group happens to be located at 0,0...
// OKAY, here's the problem: originally, EVERYTHING was in map coordinates, so
// we never had to worry about this. Now, everything has local coordinates.
// That is, the coordinates in their parent. And now we also have coordinates
// relative to the FOCAL, which should ultimately replace all reference to
// map coordinates -- maps will just be another focal, except they'd normally
// be a focal without a parent.
// The incoming events can give us the map or focal coordinates.
// The LOCAL coordinates are relative to whatever is moving -- e.g., if the items
// are in a group, then we have a THIRD coordinate system to contend with here.
// Easist is probably to go back to everything tracking map coordinates, and then
// always transform those down to the new mResizeParent coordinates to get the local
// coords. Ultimately we always track the focal coords, and then have generic
// routines that map between any two coordinate systems, but we don't have that
// yet...
// todo future: could create a Coord class for all our coordinates (e.g. Point2D.Float,
// and then we can change impl w/out direct reference to the java API), but
// have every Coord also refer to it's context (parent) so at any time,
// we could request the value of that Coord in any other context (focal, child, etc).
// Would do the same for Rectangles. (maybe VPoint / VRect?) All transient usage
// would use these objects (GUI code path). Non-transient would be a bit overkill as they
// wouldn't need the context/parent pointer.
// only one of these two options should be in use. LOCAL_RESIZE not currently feasable.
private static final boolean LOCAL_RESIZE = false;
private static final boolean MAPPED_RESIZE = false; // untested/broken code, but direction we want
boolean active = false;
LWSelection.Controller[] handles = new LWSelection.Controller[8];
// These are all in MAP coordinates
private Rectangle2D.Float mOriginalGroup_bounds;
//private Rectangle2D.Float mOriginalGroupULC_bounds;
//private Rectangle2D.Float mOriginalGroupLRC_bounds;
private Rectangle2D.Float mNewDraggedBounds;
private Object[] mOriginal_each_bounds; // Rectangle2D.Float for everything but links
private Box2D resize_box = null;
private Point2D mapMouseDown;
private LWComponent mLocalParent;
ResizeControl() {
for (int i = 0; i < handles.length; i++)
handles[i] = new LWSelection.Controller();
}
/** interface ControlListener -- our control's are numbered starting at 0 in the upper left corner,
* and increasing in index value in the clockwise direction.
*/
public LWSelection.Controller[] getControlPoints(double zoom) {
return handles;
}
private boolean isTopCtrl(int i) { return i == 0 || i == 1 || i == 2; }
private boolean isLeftCtrl(int i) { return i == 0 || i == 6 || i == 7; }
private boolean isRightCtrl(int i) { return i == 2 || i == 3 || i == 4; }
private boolean isBottomCtrl(int i) { return i == 4 || i == 5 || i == 6; }
/** this ctrl point can effect only the size, never the location */
private boolean isSizeOnlyCtrl(int i) { return i >= 3 && i <= 5; }
/** interface ControlListener handler -- for handling resize on selection */
public void controlPointPressed(int index, MapMouseEvent e) {
if (DEBUG.LAYOUT||DEBUG.MOUSE) out("resize control point " + index + " pressed");
final LWSelection selection = VUE.getSelection().clone();
//mOriginalGroup_bounds = (Rectangle2D.Float) selection.getShapeBounds();
mLocalParent = selection.first().getParent();
if (LOCAL_RESIZE) {
if (!selection.allHaveSameParent())
Util.printStackTrace(this + "; selection includes multiple parentage -- unpredicatable results");
mOriginalGroup_bounds = LWMap.getLocalBounds(selection);
} else {
mOriginalGroup_bounds = (Rectangle2D.Float) selection.getBounds();
}
if (mOriginalGroup_bounds == null) {
// if some code is written that clears the selection at the wrong time, this might happen.
System.err.println(this + " control point pressed with empty selection " + selection);
if (DEBUG.Enabled) tufts.Util.printStackTrace();
return;
}
if (DEBUG.LAYOUT) System.out.println(this + " originalGroup_bounds " + mOriginalGroup_bounds);
//mOriginalGroupULC_bounds = LWMap.getULCBounds(selection.iterator());
//mOriginalGroupLRC_bounds = LWMap.getLRCBounds(selection.iterator());
resize_box = new Box2D(mOriginalGroup_bounds);
mNewDraggedBounds = resize_box.getRect();
//mNewDraggedBounds = (Rectangle2D.Float) mOriginalGroup_bounds.getBounds2D();
//------------------------------------------------------------------
// save the original locations & sizes of everything in the selection
//------------------------------------------------------------------
mOriginal_each_bounds = new Object[selection.size()];
int idx = 0;
for (LWComponent c : selection) {
//System.out.println("PROCESSING " + c);
if (c.isManagedLocation())
continue;
if (c instanceof LWLink) {
// be sure to clone, as this is changing all the time
mOriginal_each_bounds[idx] = ((LWLink)c).getControlsWithCurrentDragEffect().clone();
//c.out("ResizeControl GOT CONTROLS " + java.util.Arrays.asList(mOriginal_each_bounds[idx]));
} else {
//mOriginal_each_bounds[idx] = c.getShapeBounds();
if (LOCAL_RESIZE)
mOriginal_each_bounds[idx] = c.getLocalBounds();
else
mOriginal_each_bounds[idx] = c.getBounds();
if (DEBUG.LAYOUT) out(c + " bounds " + mOriginal_each_bounds[idx]);
}
idx++;
}
mapMouseDown = e.getMapPoint();
}
void drawDebug(DrawContext dc) { // this only called if viewer or layout debug is on
if (mNewDraggedBounds != null) {
MapViewer viewer = VUE.getActiveViewer();
if (LOCAL_RESIZE) {
Rectangle2D localOriginal = mLocalParent.transformZeroToMapRect(mOriginalGroup_bounds, null);
dc.g.setStroke(STROKE_TWO);
dc.g.setColor(java.awt.Color.blue);
dc.g.draw(viewer.mapToScreenRect(localOriginal));
Rectangle2D localNew = mLocalParent.transformZeroToMapRect(mNewDraggedBounds, null);
dc.g.setStroke(STROKE_ONE);
dc.g.setColor(java.awt.Color.red);
dc.g.draw(viewer.mapToScreenRect(localNew));
} else {
// old/ style
dc.g.setStroke(STROKE_TWO);
dc.g.setColor(java.awt.Color.blue);
dc.g.draw(viewer.mapToScreenRect(mOriginalGroup_bounds));
dc.g.setStroke(STROKE_ONE);
dc.g.setColor(java.awt.Color.red);
dc.g.draw(viewer.mapToScreenRect(mNewDraggedBounds));
}
// if (viewer.getFocal() != viewer.getMap()) {
// // *FOCAL* LOCAL style
// dc.g.setStroke(STROKE_TWO);
// dc.g.setColor(java.awt.Color.orange);
// dc.g.draw(viewer.mapToScreenRect(viewer.getFocal().transformZeroToMapRect(mOriginalGroup_bounds)));
// }
// dc.g.setStroke(STROKE_ONE);
// dc.g.setColor(java.awt.Color.red);
// if (false&&LOCAL_RESIZE)
// dc.g.draw(viewer.mapToScreenRect(mNewDraggedBounds));
// else
// dc.g.draw(viewer.mapToScreenRect(mNewDraggedBounds));
// if (false) {
// dc.g.setColor(java.awt.Color.green);
// dc.g.draw(viewer.mapToScreenRect(mOriginalGroupULC_bounds));
// dc.g.setColor(java.awt.Color.red);
// dc.g.draw(viewer.mapToScreenRect(mOriginalGroupLRC_bounds));
// }
}
}
/** interface ControlListener handler -- for handling resize on selection */
public void controlPointMoved(int i, MapMouseEvent e) {
//System.out.println(this + " resize control point " + i + " moved");
// control points are indexed starting at 0 in the upper left,
// and increasing clockwise ending at 7 at the middle left point.
final Point2D.Float p = LOCAL_RESIZE ? e.getFocalPoint() : e.getMapPoint();
if (isTopCtrl(i)) {
resize_box.setULY(p.y);
} else if (isBottomCtrl(i)) {
resize_box.setLRY(p.y);
}
if (isLeftCtrl(i)) {
resize_box.setULX(p.x);
} else if (isRightCtrl(i)) {
resize_box.setLRX(p.x);
}
if (DEBUG.WORK) out(" point: " + Util.fmt(p));
if (DEBUG.WORK) out("resize_box: " + resize_box);
mNewDraggedBounds = resize_box.getRect();
if (DEBUG.WORK) out("dragBounds: " + Util.fmt(mNewDraggedBounds));
if (VUE.getSelection().size() == 1) {
// only one item in the selection
LWComponent c = VUE.getSelection().first();
// todo: put the selection in the ControlListener interface: don't look it up here.
if (c.supportsUserResize())
dragReshape(i, c, mNewDraggedBounds, e);
} else {
final double scaleX = mNewDraggedBounds.width / mOriginalGroup_bounds.width;
final double scaleY = mNewDraggedBounds.height / mOriginalGroup_bounds.height;
dragReshapeSelection(i,
VUE.getSelection(),
scaleX, scaleY,
e.isAltDown()); // resize if ALT is down, otherwise reposition only
}
}
/**
* reshape a single component
* @param controlPoint - which control handle is being dragged (numbered clockwise from
* @param c - the component to reshape
* @param request - the new requested bounds
*/
private void dragReshape(final int controlPoint, final LWComponent c, Rectangle2D.Float request, MapMouseEvent e)
{
//if (DEBUG.WORK) out("dragReshape; request=" + Util.fmt(request));
boolean lockedLocation = c.isManagedLocation(); // todo: this also checks selection, which we may not want...
if (c instanceof LWImage && c.getParent() instanceof LWSlide && !e.isShiftDown()) {
// TODO: fix
// Total hack for now for at least images on slides to do something
// reasonable: includes knowledge the images resize differently if shift is down...
lockedLocation = true;
}
final float requestWidth, requestHeight;
if (MAPPED_RESIZE) {
// note: this has never worked, but makes sense to move in direction
// of doing this first (and then won't have to transform the location
// x/y below when we actually do a set)
//c.getParent().transformMapToZeroRect(request, request);
request = (Rectangle2D.Float) c.getParent().transformMapToZeroRect(request);
// note that if the object itself is scaled, this actually won't work,
// as the zeroRect in the parent is the net scaled size, whereas
// we want the "actual" size to actually set on the component...
// (and anyway, this appears to be even worse for resizing images on slides)
requestWidth = request.width;
requestHeight = request.height;
} else if (LOCAL_RESIZE) {
requestWidth = request.width;
requestHeight = request.height;
} else {
requestWidth = request.width / c.getMapScaleF();
requestHeight = request.height / c.getMapScaleF();
}
if (lockedLocation || isSizeOnlyCtrl(controlPoint)) {
// this part works fine for local resizes of images on slides (Right, LR,
// and Bottom controls) it's any control that may also change the location
// while the image is enforcing it's aspect that has problems.
c.userSetSize(requestWidth, requestHeight, e);
} else {
// if (c instanceof LWImage) {
// // hack for LWImage's which handle this specially
// c.userSetFrame(request.x, request.y, request.width, request.height, e);
// return;
// }
// an origin control point is any control point that might
// change the location
final float oldWidth, oldHeight, oldX, oldY;
if (LOCAL_RESIZE) {
Rectangle2D.Float lb = c.getLocalBounds();
oldWidth = lb.width;
oldHeight = lb.height;
oldX = lb.x;
oldY = lb.y;
} else {
oldWidth = c.getWidth();
oldHeight = c.getHeight();
oldX = c.getMapX();
oldY = c.getMapY();
}
//----------------------------------------------------------------------------------------
// First set size and find out what size was actually taken before adjusting
// location. Would be better to do this by getting the minimum size first,
// but that's not working at the moment for floating text layout's.
// With images, this is badly broken for UL, UR and LL controls because of
// images constraining the aspect (and somewhat broken for the Top & Left
// controls). The way this code works here is to do the resize, then see
// about the location needing to be changes, but the userSetSize call on an
// aspect constraining image may not actually set it to that size, so our
// further adjustments based on the new size get very confused.
c.userSetSize(requestWidth, requestHeight, e);
//----------------------------------------------------------------------------------------
final float newWidth = c.getWidth();
final float newHeight = c.getHeight();
float newX, newY;
boolean moved = false;
if (newWidth != requestWidth) {
//System.out.println("width stuck at " + newWidth + " can't go to " + requestWidth);
if (isLeftCtrl(controlPoint) == false || newWidth == oldWidth) {
// do NOT move the X coord (tho Y coord might be moving)
newX = oldX;
} else {
float dx = oldWidth - newWidth;
newX = oldX + dx;
//System.out.println("\tdid manage get from " + oldWidth + " to " + newWidth + " dx=" + dx);
moved = true;
}
} else {
newX = request.x;
// may have moved
moved = true;
}
if (newHeight != requestHeight) {
//System.out.println("height stuck at " + newHeight + " can't go to " + requestHeight);
if (isTopCtrl(controlPoint) == false || newHeight == oldHeight) {
// do NOT move the Y coord (tho X coord might be moving)
newY = oldY;
} else {
float dy = oldHeight - newHeight;
newY = oldY + dy;
//System.out.println("\tdid manage get from " + oldHeight + " to " + newHeight + " dy=" + dy);
moved = true;
}
} else {
newY = request.y;
// may have moved
moved = true;
}
if (moved) {
if (LOCAL_RESIZE == false) {
if (DEBUG.WORK) System.out.format("RC: new absolute loc: %6.1f,%-6.1f; %s\n", newX, newY, c);
final Point2D.Float p = new Point2D.Float();
c.getParent().transformMapToZeroPoint(new Point2D.Float(newX, newY), p);
newX = p.x;
newY = p.y;
//newX -= c.getParent().getMapX();
//newY -= c.getParent().getMapY();
if (DEBUG.WORK) System.out.format("RC: new relative loc: %6.1f,%-6.1f; %s\n", newX, newY, c);
}
c.setLocation(newX, newY);
}
// TODO: get rid of getMinumumSize unless we fix layout floating_text
// to really return minimum
}
}
// Todo: make static methods that can operate on any collection of
// lw components (not just selection) so could generically use
// this for LWGroup resize also. (or, if groups really just put
// everything in the selection, it would automatically work).
/** @param cpi - control point index (which ctrl point is being moved) */
// todo: consider moving this code to LWGroup so that they can resize
// Note: this method will still work with just one item in the iterator, but
// it doesn't prevent moving the object when it should, which is why we have
// dragReshape above.
private void dragReshapeSelection(final int cpi,
final LWSelection selection,
final double dScaleX,
final double dScaleY,
final boolean reshapeObjects)
{
// TODO: pre-process selection to remove any managed location items (as now),
// but ALSO ignore any items that don't share the top-level parent of
// the entire selection (as due to LOCAL_RESIZE, we can no longer support
// reshaping an entire selection unless all contents have the same parent)
int idx = 0;
//System.out.println("scaleX="+scaleX);System.out.println("scaleY="+scaleY);
LWLink currentLink;
for (LWComponent c : selection) {
if (c.isManagedLocation()) // must match conditinal aboice where we collect mOriginal_each_bounds[] -- OVERKILL, allow reshaping of child (managed loc) objects
continue;
if (false && c.getParent().isSelected()) // skip if our parent also being resized -- race conditions possible -- todo: deeper nesting???
continue;
if (c instanceof LWLink) {
int controlIndex = -1;
Point2D.Float result = new Point2D.Float();
for (Point2D.Float originalPoint : ((Point2D.Float[]) mOriginal_each_bounds[idx++])) {
controlIndex++;
if (originalPoint == null)
continue;
//c.out("HANDLING CPI " + controlIndex);
Point2D.Float newPoint = translatePoint(originalPoint, result);
((LWLink)c).setControllerLocation(controlIndex, newPoint);
}
continue;
}
// TODO: need to change entire method to at least move object on-center, and
// possible to support four different movement aspects: one for each
// direction the selection edge is moving in (left/right/up/down), as what
// we really want is for objects to never exceed the mo1ving edge. If the
// selection gets to small, they may exceed the non-moving edge of the
// original group, where we'll start getting errors, tho we could also force
// a stop a that point.
// turn on DEBUG.LAYOUT to see the red box that everything is actually
// being laid-out inside...
Rectangle2D.Float c_original_bounds = (Rectangle2D.Float) mOriginal_each_bounds[idx++];
boolean resized = false;
boolean repositioned = false;
float c_new_width = 0;
float c_new_height = 0;
float c_new_x = 0;
float c_new_y = 0;
if (c.supportsUserResize() && reshapeObjects) {
//-------------------------------------------------------
// Resize -- must be done before any repositioning
//-------------------------------------------------------
if (DEBUG.LAYOUT) System.out.println("dScaleX=" + dScaleX);
if (DEBUG.LAYOUT) System.out.println("dScaleY=" + dScaleY);
c_new_width = (float) (c_original_bounds.width * dScaleX);
c_new_height = (float) (c_original_bounds.height * dScaleY);
resized = true;
}
// TODO: even if managed location, if reshaping, should allow a child
// of an object being reshaped to also be reshaped.
//-------------------------------------------------------
// Don't try to reposition child nodes -- their parents
// handle their layout (todo: flag for this -- e.g. isManagedLocation)
//-------------------------------------------------------
//if ((c.getParent() instanceof LWNode) == false) {
Point2D.Float centerPoint = null;
if (true) { // if "reposition allowed"
//-------------------------------------------------------
// Reposition (todo: needs work in the case of not resizing)
//-------------------------------------------------------
float scaleX = (float) dScaleX;
float scaleY = (float) dScaleY;
if (reshapeObjects) {
// Note: if we're handling a single selected object, reshapeObjects is always true.
// When reshaping, we can adjust the component origin smoothly with the scale
// because their lower right edge is also growing with the scale.
c_new_x = mNewDraggedBounds.x + (c_original_bounds.x - mOriginalGroup_bounds.x) * scaleX;
c_new_y = mNewDraggedBounds.y + (c_original_bounds.y - mOriginalGroup_bounds.y) * scaleY;
} else {
// when just repositioning, we have to compute the new component positions
// based on their lower right corner. (? is this still true?)
// dx/dy are the CUMULATIVE delta's from the position at the start of
// the drag operation
// CRAP, was this ever right? This doesn't look normalized....
float dx = (c_original_bounds.x - mOriginalGroup_bounds.x) * scaleX;
float dy = (c_original_bounds.y - mOriginalGroup_bounds.y) * scaleY;
c_new_x = mNewDraggedBounds.x + dx;
c_new_y = mNewDraggedBounds.y + dy;
// This is better, in that everything gets squshed the same now matter from what direction, tho then
// we can get empty curves, which are blowing our bounds to infinity (i think) and
// the selection dissappearing... fix that before enabling this.
/*
centerPoint = translatePoint(new Point2D.Float((float)c_original_bounds.getCenterX(),
(float)c_original_bounds.getCenterY()));
*/
}
if (reshapeObjects) {
if (isLeftCtrl(cpi)) {
float c_width = resized ? c_new_width * c.getMapScaleF() : c.getWidth();
if (c_new_x + c_width > resize_box.lr.x)
c_new_x = (float) resize_box.lr.x - c_width;
}
if (isTopCtrl(cpi)) {
float c_height = resized ? c_new_height * c.getMapScaleF() : c.getHeight();
if (c_new_y + c_height > resize_box.lr.y)
c_new_y = (float) resize_box.lr.y - c_height;
}
}
repositioned = true;
}
//if (repositioned && !c.hasAbsoluteMapLocation()) {
if (repositioned) {
final LWComponent parent = c.getParent();
if (DEBUG.WORK) System.out.format("new absolute loc: %6.1f,%6.1f for %s\n", c_new_x, c_new_y, c);
if (c.atTopLevel()) {
// no adjustment needed
} else {
// TODO: not working...
//Point2D.Float p = c.getParent().transformMapToZeroPoint(new Point2D.Float(c_new_x, c_new_y));
//c_new_x -= p.x;
//c_new_y -= p.y;
c_new_x -= parent.getMapX();
c_new_y -= parent.getMapY();
if (DEBUG.WORK) System.out.format("new relative loc: %6.1f,%6.1f for %s\n", c_new_x, c_new_y, c);
}
}
if (resized && repositioned) {
c.userSetFrame(c_new_x, c_new_y,
c_new_width / c.getMapScaleF(),
c_new_height / c.getMapScaleF());
} else if (resized) {
c.userSetSize(c_new_width / c.getMapScaleF(),
c_new_height / c.getMapScaleF());
} else if (repositioned) {
if (centerPoint != null)
c.setCenterAt(centerPoint);
else
c.userSetLocation(c_new_x, c_new_y);
} else
throw new IllegalStateException("Unhandled dragResizeReshape");
}
}
private Point2D.Float translatePoint(Point2D.Float originalPoint)
{
return translatePoint(originalPoint, originalPoint);
}
private Point2D.Float translatePoint(Point2D.Float originalPoint, Point2D.Float result)
{
float normal_x = (originalPoint.x - mOriginalGroup_bounds.x);
float normal_y = (originalPoint.y - mOriginalGroup_bounds.y);
float ratio_x = normal_x / mOriginalGroup_bounds.width;
float ratio_y = normal_y / mOriginalGroup_bounds.height;
final Rectangle2D.Float newBounds = mNewDraggedBounds;
//final Rectangle2D.Float newBounds = (Rectangle2D.Float) selection.getBounds(); // grows continually on it's own... (rounding error?)
float new_normal_x = newBounds.width * ratio_x;
float new_normal_y = newBounds.height * ratio_y;
float new_x = newBounds.x + new_normal_x;
float new_y = newBounds.y + new_normal_y;
result.x = new_x;
result.y = new_y;
return result;
/*
System.out.format("RATIOX %.2f orig-x %.2f normal-x %.1f orig-width %.1f new-width %.1f new-normal-x %.1f\n",
ratio_x,
originalPoint.x,
normal_x,
mOriginalGroup_bounds.width,
mNewDraggedBounds.width,
new_normal_x
);
*/
}
/** interface ControlListener handler -- for handling resize on selection */
public void controlPointDropped(int index, MapMouseEvent e) {
//System.out.println("MapViewer: resize control point " + index + " dropped");
mNewDraggedBounds = null;
Actions.NodeMakeAutoSized.checkEnabled();
}
private void out(String s) {
System.out.println("ResizeControl: " + s);
}
static class Box2D {
// We need double precision to make sure our computed
// width in getRect agrees with that of the given rectangle.
Point2D.Double ul = new Point2D.Double(); // upper left corner
Point2D.Double lr = new Point2D.Double(); // lower right corner
public Box2D(Rectangle2D r) {
ul.x = r.getX();
ul.y = r.getY();
lr.x = ul.x + r.getWidth();
lr.y = ul.y + r.getHeight();
}
Rectangle2D.Float getRect() {
Rectangle2D.Float r = new Rectangle2D.Float();
r.setRect(ul.x, ul.y, lr.x - ul.x, lr.y - ul.y);
return r;
}
// These set methods never let the box take negative width or height
void setULX(float x) { ul.x = (x > lr.x) ? lr.x : x; }
void setULY(float y) { ul.y = (y > lr.y) ? lr.y : y; }
void setLRX(float x) { lr.x = (x < ul.x) ? ul.x : x; }
void setLRY(float y) { lr.y = (y < ul.y) ? ul.y : y; }
public String toString() {
return "Box2D[" + Util.fmt(ul) + " -> " + Util.fmt(lr) + "]";
}
}
}