package ini.trakem2.display; import ini.trakem2.ControlWindow; import ini.trakem2.display.graphics.GraphicsSource; import ini.trakem2.utils.History; import ini.trakem2.utils.IJError; import ini.trakem2.utils.ProjectToolbar; import ini.trakem2.utils.Utils; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Composite; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.Stroke; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; public class AffineTransformMode implements Mode { private final Display display; private final History history; private final ATGS atgs = new AffineTransformMode.ATGS(); public AffineTransformMode(final Display display) { this.display = display; ProjectToolbar.setTool(ProjectToolbar.SELECT); // Init: resetBox(); floater.center(); this.handles = new Handle[]{NW, N, NE, E, SE, S, SW, W, RO, floater}; accum_affine = new AffineTransform(); history = new History(); // unlimited steps history.add(new TransformationStep(getTransformationsCopy())); display.getCanvas().repaint(false); } /** Returns a hash table with all selected Displayables as keys, and a copy of their affine transform as value. This is useful to easily create undo steps. */ private HashMap<Displayable,AffineTransform> getTransformationsCopy() { final HashMap<Displayable,AffineTransform> ht_copy = new HashMap<Displayable,AffineTransform>(); for (final Displayable d : display.getSelection().getAffected()) { ht_copy.put(d, d.getAffineTransformCopy()); } return ht_copy; } /** Add an undo step to the internal history. */ private void addUndoStep() { if (mouse_dragged || display.getSelection().isEmpty()) return; if (null == history) return; if (history.indexAtStart() || (history.indexAtEnd() && -1 != history.index())) { history.add(new TransformationStep(getTransformationsCopy())); } else { // remove history elements from index+1 to end history.clip(); } } @Override synchronized public void undoOneStep() { if (null == history) return; // store the current state if at end: Utils.log2("index at end: " + history.indexAtEnd()); Map.Entry<Displayable,AffineTransform> Be = ((TransformationStep)history.getCurrent()).ht.entrySet().iterator().next(); if (history.indexAtEnd()) { final HashMap<Displayable,AffineTransform> m = getTransformationsCopy(); history.append(new TransformationStep(m)); Be = m.entrySet().iterator().next(); // must set again, for the other one was the last step, not the current state. } // disable application to other layers (too big a headache) accum_affine = null; // undo one step final TransformationStep step = (TransformationStep)history.undoOneStep(); if (null == step) return; // no more steps LayerSet.applyTransforms(step.ht); resetBox(); // call fixAffinePoints with the diff affine transform, as computed from first selected object try { // t0 t1 // CA = B // C = BA^(-1) final AffineTransform A = step.ht.get(Be.getKey()); // the t0 final AffineTransform C = new AffineTransform(Be.getValue()); C.concatenate(A.createInverse()); fixAffinePoints(C); } catch (final Exception e) { IJError.print(e); } } @Override synchronized public void redoOneStep() { if (null == history) return; final Map.Entry<Displayable,AffineTransform> Ae = ((TransformationStep)history.getCurrent()).ht.entrySet().iterator().next(); final TransformationStep step = (TransformationStep)history.redoOneStep(); if (null == step) return; // no more steps LayerSet.applyTransforms(step.ht); resetBox(); // call fixAffinePoints with the diff affine transform, as computed from first selected object // t0 t1 // A = CB // AB^(-1) = C final AffineTransform B = step.ht.get(Ae.getKey()); final AffineTransform C = new AffineTransform(Ae.getValue()); try { C.concatenate(B.createInverse()); fixAffinePoints(C); } catch (final Exception e) { IJError.print(e); } } @Override public boolean isDragging() { return dragging; } private class ATGS implements GraphicsSource { @Override public List<? extends Paintable> asPaintable(final List<? extends Paintable> ds) { return ds; } /** Paints the transformation handles and a bounding box around all selected. */ @Override public void paintOnTop(final Graphics2D g, final Display display, final Rectangle srcRect, final double magnification) { final Stroke original_stroke = g.getStroke(); final AffineTransform original = g.getTransform(); g.setTransform(new AffineTransform()); if (!rotating) { //Utils.log("box painting: " + box); // 30 pixel line, 10 pixel gap, 10 pixel line, 10 pixel gap //float mag = (float)magnification; final float[] dashPattern = { 30, 10, 10, 10 }; g.setStroke(new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10, dashPattern, 0)); g.setColor(Color.yellow); // paint box //g.drawRect(box.x, box.y, box.width, box.height); g.draw(original.createTransformedShape(box)); g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER)); // paint handles for scaling (boxes) and rotating (circles), and floater for (int i=0; i<handles.length; i++) { handles[i].paint(g, srcRect, magnification); } } else { g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER)); RO.paint(g, srcRect, magnification); ((RotationHandle)RO).paintMoving(g, srcRect, magnification, display.getCanvas().getCursorLoc()); } if (null != affine_handles) { for (final AffinePoint ap : affine_handles) { ap.paint(g); } } g.setTransform(original); g.setStroke(original_stroke); } } @Override public GraphicsSource getGraphicsSource() { return atgs; } @Override public boolean canChangeLayer() { return false; } @Override public boolean canZoom() { return true; } @Override public boolean canPan() { return true; } /* From former Selection class: the affine transformation GUI */ private final int iNW = 0; private final int iN = 1; private final int iNE = 2; private final int iE = 3; private final int iSE = 4; private final int iS = 5; private final int iSW = 6; private final int iW = 7; private final int ROTATION = 12; private final int FLOATER = 13; private final Handle NW = new BoxHandle(0,0, iNW); private final Handle N = new BoxHandle(0,0, iN); private final Handle NE = new BoxHandle(0,0, iNE); private final Handle E = new BoxHandle(0,0, iE); private final Handle SE = new BoxHandle(0,0, iSE); private final Handle S = new BoxHandle(0,0, iS); private final Handle SW = new BoxHandle(0,0, iSW); private final Handle W = new BoxHandle(0,0, iW); private final Handle RO = new RotationHandle(0,0, ROTATION); /** Pivot of rotation. Always checked first on mouse pressed, before other handles. */ private final Floater floater = new Floater(0, 0, FLOATER); private final Handle[] handles; private Handle grabbed = null; private boolean dragging = false; // means: dragging the whole transformation box private boolean rotating = false; private boolean mouse_dragged = false; private Rectangle box; private int x_d_old, y_d_old, x_d, y_d; /** Handles have screen coordinates. */ private abstract class Handle { public int x, y; public final int id; Handle(final int x, final int y, final int id) { this.x = x; this.y = y; this.id = id; } abstract public void paint(Graphics2D g, Rectangle srcRect, double mag); /** Radius is the dectection "radius" around the handle x,y. */ public boolean contains(final int x_p, final int y_p, final double radius) { if (x - radius <= x_p && x + radius >= x_p && y - radius <= y_p && y + radius >= y_p) return true; return false; } public void set(final int x, final int y) { this.x = x; this.y = y; } abstract void drag(MouseEvent me, int dx, int dy); } private class BoxHandle extends Handle { BoxHandle(final int x, final int y, final int id) { super(x,y,id); } @Override public void paint(final Graphics2D g, final Rectangle srcRect, final double mag) { final int x = (int)((this.x - srcRect.x)*mag); final int y = (int)((this.y - srcRect.y)*mag); DisplayCanvas.drawHandle(g, x, y, 1.0); // ignoring magnification for the sizes, since Selection is painted differently } @Override public void drag(final MouseEvent me, final int dx, final int dy) { final Rectangle box_old = (Rectangle)box.clone(); //Utils.log2("dx,dy: " + dx + "," + dy + " before mod"); double res = dx / 2.0; res -= Math.floor(res); res *= 2; int anchor_x = 0, anchor_y = 0; switch (this.id) { // java sucks to such an extent, I don't even bother case iNW: if (x + dx >= E.x) return; if (y + dy >= S.y) return; box.x += dx; box.y += dy; box.width -= dx; box.height -= dy; anchor_x = SE.x; anchor_y = SE.y; break; case iN: if (y + dy >= S.y) return; box.y += dy; box.height -= dy; anchor_x = S.x; anchor_y = S.y; break; case iNE: if (x + dx <= W.x) return; if (y + dy >= S.y) return; box.y += dy; box.width += dx; box.height -= dy; anchor_x = SW.x; anchor_y = SW.y; break; case iE: if (x + dx <= W.x) return; box.width += dx; anchor_x = W.x; anchor_y = W.y; break; case iSE: if (x + dx <= W.x) return; if (y + dy <= N.y) return; box.width += dx; box.height += dy; anchor_x = NW.x; anchor_y = NW.y; break; case iS: if (y + dy <= N.y) return; box.height += dy; anchor_x = N.x; anchor_y = N.y; break; case iSW: if (x + dx >= E.x) return; if (y + dy <= N.y) return; box.x += dx; box.width -= dx; box.height += dy; anchor_x = NE.x; anchor_y = NE.y; break; case iW: if (x + dx >= E.x) return; box.x += dx; box.width -= dx; anchor_x = E.x; anchor_y = E.y; break; } // proportion: final double px = (double)box.width / (double)box_old.width; final double py = (double)box.height / (double)box_old.height; // displacement: specific of each element of the selection and their links, depending on where they are. final AffineTransform at = new AffineTransform(); at.translate( anchor_x, anchor_y ); at.scale( px, py ); at.translate( -anchor_x, -anchor_y ); addUndoStep(); if (null != accum_affine) accum_affine.preConcatenate(at); Displayable.preConcatenate(at, display.getSelection().getAffected()); fixAffinePoints(at); // finally: setHandles(box); // overkill. As Graham said, most newly available chip resources are going to be wasted. They are already. } } private final double rotate(final MouseEvent me) { // center of rotation is the floater final double cos = Utils.getCos(x_d_old - floater.x, y_d_old - floater.y, x_d - floater.x, y_d - floater.y); //double sin = Math.sqrt(1 - cos*cos); //double delta = M.getAngle(cos, sin); double delta = Math.acos(cos); // same thing as the two lines above // need to compute the sign of rotation as well: the cross-product! // cross-product: // a = (3,0,0) and b = (0,2,0) // a x b = (3,0,0) x (0,2,0) = ((0 x 0 - 2 x 0), -(3 x 0 - 0 x 0), (3 x 2 - 0 x 0)) = (0,0,6). if (Utils.isControlDown(me)) { delta = Math.toDegrees(delta); if (me.isShiftDown()) { // 1 degree angle increments delta = (int)(delta + 0.5); } else { // 10 degrees angle increments: snap to closest delta = (int)((delta + 5.5 * (delta < 0 ? -1 : 1)) / 10) * 10; } Utils.showStatus("Angle: " + delta + " degrees"); delta = Math.toRadians(delta); // TODO: the angle above is just the last increment on mouse drag, not the total amount of angle accumulated since starting this mousePressed-mouseDragged-mouseReleased cycle, neither the actual angle of the selected elements. So we need to store the accumulated angle and diff from it to do the above roundings. } if (Double.isNaN(delta)) { Utils.log2("Selection rotation handle: ignoring NaN angle"); return Double.NaN; } final double zc = (x_d_old - floater.x) * (y_d - floater.y) - (x_d - floater.x) * (y_d_old - floater.y); // correction: if (zc < 0) { delta = -delta; } rotate(Math.toDegrees(delta), floater.x, floater.y); return delta; } private class RotationHandle extends Handle { final int shift = 50; RotationHandle(final int x, final int y, final int id) { super(x, y, id); } @Override public void paint(final Graphics2D g, final Rectangle srcRect, final double mag) { final int x = (int)((this.x - srcRect.x)*mag) + shift; final int y = (int)((this.y - srcRect.y)*mag); final int fx = (int)((floater.x - srcRect.x)*mag); final int fy = (int)((floater.y - srcRect.y)*mag); draw(g, fx, fy, x, y); } private void draw(final Graphics2D g, final int fx, final int fy, final int x, final int y) { g.setColor(Color.white); g.drawLine(fx, fy, x, y); g.fillOval(x -4, y -4, 9, 9); g.setColor(Color.black); g.drawOval(x -2, y -2, 5, 5); } public void paintMoving(final Graphics2D g, final Rectangle srcRect, final double mag, final Point mouse) { // mouse as xMouse,yMouse from ImageCanvas: world coordinates, not screen! final int fx = (int)((floater.x - srcRect.x)*mag); final int fy = (int)((floater.y - srcRect.y)*mag); // vector final double vx = (mouse.x - srcRect.x)*mag - fx; final double vy = (mouse.y - srcRect.y)*mag - fy; //double len = Math.sqrt(vx*vx + vy*vy); //vx = (vx / len) * 50; //vy = (vy / len) * 50; draw(g, fx, fy, fx + (int)vx, fy + (int)vy); } @Override public boolean contains(final int x_p, final int y_p, final double radius) { final double mag = display.getCanvas().getMagnification(); final double x = this.x + shift / mag; final double y = this.y; if (x - radius <= x_p && x + radius >= x_p && y - radius <= y_p && y + radius >= y_p) return true; return false; } @Override public void drag(final MouseEvent me, final int dx, final int dy) { /// Bad design, I know, I'm ignoring the dx,dy // how: // center is the floater rotate(me); } } private class Floater extends Handle { Floater(final int x, final int y, final int id) { super(x,y, id); } @Override public void paint(final Graphics2D g, final Rectangle srcRect, final double mag) { final int x = (int)((this.x - srcRect.x)*mag); final int y = (int)((this.y - srcRect.y)*mag); final Composite co = g.getComposite(); g.setXORMode(Color.white); g.drawOval(x -10, y -10, 21, 21); g.drawRect(x -1, y -15, 3, 31); g.drawRect(x -15, y -1, 31, 3); g.setComposite(co); // undo XOR paint } public Rectangle getBoundingBox(final Rectangle b) { b.x = this.x - 15; b.y = this.y - 15; b.width = this.x + 31; b.height = this.y + 31; return b; } @Override public void drag(final MouseEvent me, final int dx, final int dy) { this.x += dx; this.y += dy; RO.x = this.x; RO.y = this.y; } public void center() { this.x = RO.x = box.x + box.width/2; this.y = RO.y = box.y + box.height/2; } @Override public boolean contains(final int x_p, final int y_p, final double radius) { return super.contains(x_p, y_p, radius*3.5); } } public void centerFloater() { floater.center(); } /** No display bounds are checked, the floater can be placed wherever you want. */ public void setFloater(final int x, final int y) { floater.x = x; floater.y = y; } public int getFloaterX() { return floater.x; } public int getFloaterY() { return floater.y; } private void setHandles(final Rectangle b) { final int tx = b.x; final int ty = b.y; final int tw = b.width; final int th = b.height; NW.set(tx, ty); N.set(tx + tw/2, ty); NE.set(tx + tw, ty); E.set(tx + tw, ty + th/2); SE.set(tx + tw, ty + th); S.set(tx + tw/2, ty + th); SW.set(tx, ty + th); W.set(tx, ty + th/2); } private AffineTransform accum_affine = null; /** Skips current layer, since its done already. */ synchronized protected void applyAndPropagate(final Set<Layer> sublist) { if (null == accum_affine) { Utils.log2("Cannot apply to other layers: undo/redo was used."); return; } if (0 == sublist.size()) { Utils.logAll("No layers to apply to!"); return; } // Check if there are links across affected layers if (Displayable.areThereLayerCrossLinks(sublist, false)) { if (ControlWindow.isGUIEnabled()) { final YesNoDialog yn = ControlWindow.makeYesNoDialog("Warning!", "Some objects are linked!\nThe transformation would alter interrelationships.\nProceed anyway?"); if ( ! yn.yesPressed()) return; } else { Utils.log("Can't apply: some images may be linked across layers.\n Unlink them by removing segmentation objects like arealists, pipes, profiles, etc. that cross these layers."); return; } } // Add undo step final ArrayList<Displayable> al = new ArrayList<Displayable>(); for (final Layer l : sublist) { al.addAll(l.getDisplayables()); } display.getLayer().getParent().addTransformStep(al); // Must capture last step of free affine when using affine points: if (null != free_affine && null != model) { accum_affine.preConcatenate(free_affine); accum_affine.preConcatenate(model.createAffine()); } // Apply! for (final Layer l : sublist) { if (display.getLayer() == l) continue; // already applied l.apply(Displayable.class, accum_affine); } // Record current state as last step in undo queue display.getLayer().getParent().addTransformStep(al); } @Override public boolean apply() { // Notify each Displayable that any set of temporary transformations are over. // The transform is the same, has not changed. This is just sending an event. for (final Displayable d : display.getSelection().getAffected()) { d.setAffineTransform( d.getAffineTransform() ); } return true; } @Override public boolean cancel() { if (null != history) { // apply first LayerSet.applyTransforms(((TransformationStep)history.get(0)).ht); } return true; } private class AffinePoint { int x, y; AffinePoint(final int x, final int y) { this.x = x; this.y = y; } @Override public boolean equals(final Object ob) { //if (!ob.getClass().equals(AffinePoint.class)) return false; final AffinePoint ap = (AffinePoint) ob; final double mag = display.getCanvas().getMagnification(); final double dx = mag * ( ap.x - this.x ); final double dy = mag * ( ap.y - this.y ); final double d = dx * dx + dy * dy; return d < 64.0; } void translate(final int dx, final int dy) { x += dx; y += dy; } private void paint(final Graphics2D g) { final int x = display.getCanvas().screenX(this.x); final int y = display.getCanvas().screenY(this.y); Utils.drawPoint(g, x, y); } } private ArrayList<AffinePoint> affine_handles = null; private ArrayList< mpicbg.models.PointMatch > matches = null; private mpicbg.models.Point[] p = null; private mpicbg.models.Point[] q = null; private mpicbg.models.AbstractAffineModel2D<?> model = null; private AffineTransform free_affine = null; private HashMap<Displayable,AffineTransform> initial_affines = null; /* private void forgetAffine() { affine_handles = null; matches = null; p = q = null; model = null; free_affine = null; initial_affines = null; } */ private void initializeModel() { // Store current "initial" state in the accumulated affine if (null != free_affine && null != model && null != accum_affine) { accum_affine.preConcatenate(free_affine); accum_affine.preConcatenate(model.createAffine()); } free_affine = new AffineTransform(); initial_affines = getTransformationsCopy(); final int size = affine_handles.size(); switch (size) { case 0: model = null; q = p = null; matches = null; return; case 1: model = new mpicbg.models.TranslationModel2D(); break; case 2: model = new mpicbg.models.SimilarityModel2D(); break; case 3: model = new mpicbg.models.AffineModel2D(); break; } p = new mpicbg.models.Point[size]; q = new mpicbg.models.Point[size]; matches = new ArrayList< mpicbg.models.PointMatch >(); int i = 0; for (final AffinePoint ap : affine_handles) { p[i] = new mpicbg.models.Point(new double[]{ap.x, ap.y}); q[i] = p[i].clone(); matches.add(new mpicbg.models.PointMatch(p[i], q[i])); i++; } } private void freeAffine(final AffinePoint affp) { // The selected point final double[] w = q[affine_handles.indexOf(affp)].getW(); w[0] = affp.x; w[1] = affp.y; try { model.fit(matches); } catch (final Exception e) {} final AffineTransform model_affine = model.createAffine(); for (final Map.Entry<Displayable,AffineTransform> e : initial_affines.entrySet()) { final AffineTransform at = new AffineTransform(e.getValue()); at.preConcatenate(free_affine); at.preConcatenate(model_affine); e.getKey().setAffineTransform(at); } } private void fixAffinePoints(final AffineTransform at) { if (null != matches) { final float[] po = new float[2]; for (final AffinePoint affp : affine_handles) { po[0] = affp.x; po[1] = affp.y; at.transform(po, 0, po, 0, 1); affp.x = (int)po[0]; affp.y = (int)po[1]; } // Model will be reinitialized when needed free_affine.setToIdentity(); model = null; } } private AffinePoint affp = null; @Override public void mousePressed(final MouseEvent me, final int x_p, final int y_p, final double magnification) { grabbed = null; // reset if (me.isShiftDown()) { if (Utils.isControlDown(me) && null != affine_handles) { if (affine_handles.remove(new AffinePoint(x_p, y_p))) { if (0 == affine_handles.size()) affine_handles = null; else initializeModel(); } return; } if (null == affine_handles) { affine_handles = new ArrayList<AffinePoint>(); } if (affine_handles.size() < 3) { affine_handles.add(new AffinePoint(x_p, y_p)); if (1 == affine_handles.size()) { free_affine = new AffineTransform(); initial_affines = getTransformationsCopy(); } initializeModel(); } return; } else if (null != affine_handles) { final int index = affine_handles.indexOf(new AffinePoint(x_p, y_p)); if (-1 != index) { affp = affine_handles.get(index); return; } } // find scale handle double radius = 4 / magnification; if (radius < 1) radius = 1; // start with floater (the last) for (int i=handles.length -1; i>-1; i--) { if (handles[i].contains(x_p, y_p, radius)) { grabbed = handles[i]; if (grabbed.id > iW && grabbed.id <= ROTATION) rotating = true; return; } } // if none grabbed, then drag the whole thing dragging = false; //reset if (box.x <= x_p && box.y <= y_p && box.x + box.width >= x_p && box.y + box.height >= y_p) { dragging = true; } } @Override public void mouseDragged(final MouseEvent me, final int x_p, final int y_p, final int x_d, final int y_d, final int x_d_old, final int y_d_old) { // Store old for rotation handle: this.x_d = x_d; this.y_d = y_d; this.x_d_old = x_d_old; this.y_d_old = y_d_old; // compute translation final int dx = x_d - x_d_old; final int dy = y_d - y_d_old; execDrag(me, dx, dy); display.getCanvas().repaint(true); mouse_dragged = true; // after execDrag, so the first undo step is added. } private void execDrag(final MouseEvent me, final int dx, final int dy) { if (0 == dx && 0 == dy) return; if (null != affp) { affp.translate(dx, dy); if (null == model) { // Model has been canceled by a transformation from the other handles initializeModel(); } // Passing on the translation from start freeAffine(affp); return; } if (null != grabbed) { // drag the handle and perform whatever task it has assigned grabbed.drag(me, dx, dy); } else if (dragging) { // drag all selected and linked translate(dx, dy); //and the box! box.x += dx; box.y += dy; // and the handles! setHandles(box); } } @Override public void mouseReleased(final MouseEvent me, final int x_p, final int y_p, final int x_d, final int y_d, final int x_r, final int y_r) { // Record current state for selected Displayable set, if there was any change: final int dx = x_r - x_p; final int dy = y_r - y_p; if (0 != dx || 0 != dy) { display.getLayerSet().addTransformStep(display.getSelection().getAffected()); // all selected and their links: i.e. all that will change } // me is null when calling from Display, because of popup interfering with mouseReleased if (null != me) { execDrag(me, x_r - x_d, y_r - y_d); display.getCanvas().repaint(true); } // recalculate box resetBox(); //reset if ((null != grabbed && grabbed.id <= iW) || dragging) { floater.center(); } grabbed = null; dragging = false; rotating = false; affp = null; mouse_dragged = false; } /** Recalculate box and reset handles. */ public void resetBox() { box = null; Rectangle b = new Rectangle(); for (final Displayable d : display.getSelection().getSelected()) { b = d.getBoundingBox(b); if (null == box) box = (Rectangle)b.clone(); box.add(b); } if (null != box) setHandles(box); } /** Rotate the objects in the current selection by the given angle, in degrees, relative to the x_o, y_o origin. */ public void rotate(final double angle, final int xo, final int yo) { final AffineTransform at = new AffineTransform(); at.rotate(Math.toRadians(angle), xo, yo); addUndoStep(); if (null != accum_affine) accum_affine.preConcatenate(at); Displayable.preConcatenate(at, display.getSelection().getAffected()); fixAffinePoints(at); resetBox(); } /** Translate all selected objects and their links by the given differentials. The floater position is unaffected; if you want to update it call centerFloater() */ public void translate(final double dx, final double dy) { final AffineTransform at = new AffineTransform(); at.translate(dx, dy); addUndoStep(); if (null != accum_affine) accum_affine.preConcatenate(at); Displayable.preConcatenate(at, display.getSelection().getAffected()); fixAffinePoints(at); resetBox(); } /** Scale all selected objects and their links by by the given scales, relative to the floater position. . */ public void scale(final double sx, final double sy) { if (0 == sx || 0 == sy) { Utils.showMessage("Cannot scale to 0."); return; } final AffineTransform at = new AffineTransform(); at.translate(floater.x, floater.y); at.scale(sx, sy); at.translate(-floater.x, -floater.y); addUndoStep(); if (null != accum_affine) accum_affine.preConcatenate(at); Displayable.preConcatenate(at, display.getSelection().getAffected()); fixAffinePoints(at); resetBox(); } @Override public Rectangle getRepaintBounds() { final Rectangle b = display.getSelection().getLinkedBox(); b.add(floater.getBoundingBox(new Rectangle())); return b; } @Override public void srcRectUpdated(final Rectangle srcRect, final double magnification) {} @Override public void magnificationUpdated(final Rectangle srcRect, final double magnification) {} }