AffineTransformMode notifies all affected Displayables on apply by
[trakem2.git] / ini / trakem2 / display / AffineTransformMode.java
blobd85bc810d18026f6d9954aa7377156389e400f52
1 package ini.trakem2.display;
3 import ini.trakem2.display.Display;
4 import ini.trakem2.display.DisplayCanvas;
5 import ini.trakem2.display.Displayable;
6 import ini.trakem2.display.Paintable;
7 import ini.trakem2.display.Selection;
8 import ini.trakem2.display.Layer;
9 import ini.trakem2.display.LayerSet;
10 import ini.trakem2.display.graphics.GraphicsSource;
11 import ini.trakem2.display.TransformationStep;
12 import ini.trakem2.utils.ProjectToolbar;
13 import java.util.Collection;
14 import java.awt.Rectangle;
15 import java.awt.Graphics2D;
16 import java.awt.event.MouseEvent;
17 import java.awt.Rectangle;
18 import java.awt.Color;
19 import java.awt.Graphics;
20 import java.awt.Point;
21 import java.util.Collections;
22 import java.util.Collection;
23 import java.util.LinkedList;
24 import java.util.HashSet;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.TreeMap;
28 import java.util.Iterator;
29 import java.util.ArrayList;
30 import java.util.Map;
31 import java.util.Set;
32 import java.awt.Stroke;
33 import java.awt.BasicStroke;
34 import java.awt.AlphaComposite;
35 import java.awt.Composite;
36 import java.awt.Graphics2D;
37 import java.awt.geom.AffineTransform;
38 import java.awt.geom.Area;
39 import java.awt.image.ColorModel;
40 import java.awt.event.MouseEvent;
42 import ini.trakem2.utils.M;
43 import ini.trakem2.utils.Utils;
44 import ini.trakem2.utils.IJError;
45 import ini.trakem2.display.YesNoDialog;
46 import ini.trakem2.utils.History;
47 import ini.trakem2.ControlWindow;
48 import ini.trakem2.Project;
50 public class AffineTransformMode implements Mode {
52 private final Display display;
53 private final History history;
54 private final ATGS atgs = new AffineTransformMode.ATGS();
56 public AffineTransformMode(final Display display) {
57 this.display = display;
58 ProjectToolbar.setTool(ProjectToolbar.SELECT);
59 // Init:
60 resetBox();
61 floater.center();
62 this.handles = new Handle[]{NW, N, NE, E, SE, S, SW, W, RO, floater};
63 accum_affine = new AffineTransform();
64 history = new History(); // unlimited steps
65 history.add(new TransformationStep(getTransformationsCopy()));
66 display.getCanvas().repaint(false);
69 /** 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. */
70 private HashMap<Displayable,AffineTransform> getTransformationsCopy() {
71 final HashMap<Displayable,AffineTransform> ht_copy = new HashMap<Displayable,AffineTransform>();
72 for (final Displayable d : display.getSelection().getAffected()) {
73 ht_copy.put(d, d.getAffineTransformCopy());
75 return ht_copy;
78 /** Add an undo step to the internal history. */
79 private void addUndoStep() {
80 if (mouse_dragged || display.getSelection().isEmpty()) return;
81 if (null == history) return;
82 if (history.indexAtStart() || (history.indexAtEnd() && -1 != history.index())) {
83 history.add(new TransformationStep(getTransformationsCopy()));
84 } else {
85 // remove history elements from index+1 to end
86 history.clip();
90 synchronized public void undoOneStep() {
91 LayerSet layerset = display.getLayer().getParent();
92 if (null == history) return;
93 // store the current state if at end:
94 Utils.log2("index at end: " + history.indexAtEnd());
96 Map.Entry<Displayable,AffineTransform> Be = ((TransformationStep)history.getCurrent()).ht.entrySet().iterator().next();
98 if (history.indexAtEnd()) {
99 HashMap<Displayable,AffineTransform> m = getTransformationsCopy();
100 history.append(new TransformationStep(m));
101 Be = m.entrySet().iterator().next(); // must set again, for the other one was the last step, not the current state.
104 // disable application to other layers (too big a headache)
105 accum_affine = null;
106 // undo one step
107 TransformationStep step = (TransformationStep)history.undoOneStep();
108 if (null == step) return; // no more steps
109 LayerSet.applyTransforms(step.ht);
110 resetBox();
112 // call fixAffinePoints with the diff affine transform, as computed from first selected object
114 try {
115 // t0 t1
116 // CA = B
117 // C = BA^(-1)
118 AffineTransform A = step.ht.get(Be.getKey()); // the t0
119 AffineTransform C = new AffineTransform(Be.getValue());
120 C.concatenate(A.createInverse());
121 fixAffinePoints(C);
122 } catch (Exception e) {
123 IJError.print(e);
127 synchronized public void redoOneStep() {
128 if (null == history) return;
130 Map.Entry<Displayable,AffineTransform> Ae = ((TransformationStep)history.getCurrent()).ht.entrySet().iterator().next();
132 TransformationStep step = (TransformationStep)history.redoOneStep();
133 if (null == step) return; // no more steps
134 LayerSet.applyTransforms(step.ht);
135 resetBox();
137 // call fixAffinePoints with the diff affine transform, as computed from first selected object
138 // t0 t1
139 // A = CB
140 // AB^(-1) = C
141 AffineTransform B = step.ht.get(Ae.getKey());
142 AffineTransform C = new AffineTransform(Ae.getValue());
143 try {
144 C.concatenate(B.createInverse());
145 fixAffinePoints(C);
146 } catch (Exception e) {
147 IJError.print(e);
151 public boolean isDragging() {
152 return dragging;
155 private class ATGS implements GraphicsSource {
156 public List<? extends Paintable> asPaintable(final List<? extends Paintable> ds) {
157 return ds;
159 /** Paints the transformation handles and a bounding box around all selected. */
160 public void paintOnTop(final Graphics2D g, final Display display, final Rectangle srcRect, final double magnification) {
161 final Stroke original_stroke = g.getStroke();
162 AffineTransform original = g.getTransform();
163 g.setTransform(new AffineTransform());
164 if (!rotating) {
165 //Utils.log("box painting: " + box);
167 // 30 pixel line, 10 pixel gap, 10 pixel line, 10 pixel gap
168 float mag = (float)magnification;
169 float[] dashPattern = { 30, 10, 10, 10 };
170 g.setStroke(new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10, dashPattern, 0));
171 g.setColor(Color.yellow);
172 // paint box
173 //g.drawRect(box.x, box.y, box.width, box.height);
174 g.draw(original.createTransformedShape(box));
175 g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
176 // paint handles for scaling (boxes) and rotating (circles), and floater
177 for (int i=0; i<handles.length; i++) {
178 handles[i].paint(g, srcRect, magnification);
180 } else {
181 g.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
182 RO.paint(g, srcRect, magnification);
183 ((RotationHandle)RO).paintMoving(g, srcRect, magnification, display.getCanvas().getCursorLoc());
186 if (null != affine_handles) {
187 for (final AffinePoint ap : affine_handles) {
188 ap.paint(g);
192 g.setTransform(original);
193 g.setStroke(original_stroke);
197 public GraphicsSource getGraphicsSource() {
198 return atgs;
201 public boolean canChangeLayer() { return false; }
202 public boolean canZoom() { return true; }
203 public boolean canPan() { return true; }
208 /* From former Selection class: the affine transformation GUI */
210 private final int iNW = 0;
211 private final int iN = 1;
212 private final int iNE = 2;
213 private final int iE = 3;
214 private final int iSE = 4;
215 private final int iS = 5;
216 private final int iSW = 6;
217 private final int iW = 7;
218 private final int rNW = 8;
219 private final int rNE = 9;
220 private final int rSE = 10;
221 private final int rSW = 11;
222 private final int ROTATION = 12;
223 private final int FLOATER = 13;
224 private final Handle NW = new BoxHandle(0,0, iNW);
225 private final Handle N = new BoxHandle(0,0, iN);
226 private final Handle NE = new BoxHandle(0,0, iNE);
227 private final Handle E = new BoxHandle(0,0, iE);
228 private final Handle SE = new BoxHandle(0,0, iSE);
229 private final Handle S = new BoxHandle(0,0, iS);
230 private final Handle SW = new BoxHandle(0,0, iSW);
231 private final Handle W = new BoxHandle(0,0, iW);
232 private final Handle RO = new RotationHandle(0,0, ROTATION);
233 /** Pivot of rotation. Always checked first on mouse pressed, before other handles. */
234 private final Floater floater = new Floater(0, 0, FLOATER);
235 private final Handle[] handles;
236 private Handle grabbed = null;
237 private boolean dragging = false; // means: dragging the whole transformation box
238 private boolean rotating = false;
239 private boolean mouse_dragged = false;
240 private Rectangle box;
242 private int x_d_old, y_d_old, x_d, y_d;
244 /** Handles have screen coordinates. */
245 private abstract class Handle {
246 public int x, y;
247 public final int id;
248 Handle(int x, int y, int id) {
249 this.x = x;
250 this.y = y;
251 this.id = id;
253 abstract public void paint(Graphics2D g, Rectangle srcRect, double mag);
254 /** Radius is the dectection "radius" around the handle x,y. */
255 public boolean contains(int x_p, int y_p, double radius) {
256 if (x - radius <= x_p && x + radius >= x_p
257 && y - radius <= y_p && y + radius >= y_p) return true;
258 return false;
260 public void set(int x, int y) {
261 this.x = x;
262 this.y = y;
264 abstract void drag(MouseEvent me, int dx, int dy);
267 private class BoxHandle extends Handle {
268 BoxHandle(int x, int y, int id) {
269 super(x,y,id);
271 public void paint(final Graphics2D g, final Rectangle srcRect, final double mag) {
272 final int x = (int)((this.x - srcRect.x)*mag);
273 final int y = (int)((this.y - srcRect.y)*mag);
274 DisplayCanvas.drawHandle(g, x, y, 1.0); // ignoring magnification for the sizes, since Selection is painted differently
276 public void drag(MouseEvent me, int dx, int dy) {
277 Rectangle box_old = (Rectangle)box.clone();
278 //Utils.log2("dx,dy: " + dx + "," + dy + " before mod");
279 double res = dx / 2.0;
280 res -= Math.floor(res);
281 res *= 2;
282 int extra = (int)res;
283 int anchor_x = 0,
284 anchor_y = 0;
285 switch (this.id) { // java sucks to such an extent, I don't even bother
286 case iNW:
287 if (x + dx >= E.x) return;
288 if (y + dy >= S.y) return;
289 box.x += dx;
290 box.y += dy;
291 box.width -= dx;
292 box.height -= dy;
293 anchor_x = SE.x;
294 anchor_y = SE.y;
295 break;
296 case iN:
297 if (y + dy >= S.y) return;
298 box.y += dy;
299 box.height -= dy;
300 anchor_x = S.x;
301 anchor_y = S.y;
302 break;
303 case iNE:
304 if (x + dx <= W.x) return;
305 if (y + dy >= S.y) return;
306 box.y += dy;
307 box.width += dx;
308 box.height -= dy;
309 anchor_x = SW.x;
310 anchor_y = SW.y;
311 break;
312 case iE:
313 if (x + dx <= W.x) return;
314 box.width += dx;
315 anchor_x = W.x;
316 anchor_y = W.y;
317 break;
318 case iSE:
319 if (x + dx <= W.x) return;
320 if (y + dy <= N.y) return;
321 box.width += dx;
322 box.height += dy;
323 anchor_x = NW.x;
324 anchor_y = NW.y;
325 break;
326 case iS:
327 if (y + dy <= N.y) return;
328 box.height += dy;
329 anchor_x = N.x;
330 anchor_y = N.y;
331 break;
332 case iSW:
333 if (x + dx >= E.x) return;
334 if (y + dy <= N.y) return;
335 box.x += dx;
336 box.width -= dx;
337 box.height += dy;
338 anchor_x = NE.x;
339 anchor_y = NE.y;
340 break;
341 case iW:
342 if (x + dx >= E.x) return;
343 box.x += dx;
344 box.width -= dx;
345 anchor_x = E.x;
346 anchor_y = E.y;
347 break;
349 // proportion:
350 double px = (double)box.width / (double)box_old.width;
351 double py = (double)box.height / (double)box_old.height;
352 // displacement: specific of each element of the selection and their links, depending on where they are.
354 final AffineTransform at = new AffineTransform();
355 at.translate( anchor_x, anchor_y );
356 at.scale( px, py );
357 at.translate( -anchor_x, -anchor_y );
359 addUndoStep();
361 if (null != accum_affine) accum_affine.preConcatenate(at);
363 for (final Displayable d : display.getSelection().getAffected()) {
364 d.preTransform(at, false);
366 fixAffinePoints(at);
368 // finally:
369 setHandles(box); // overkill. As Graham said, most newly available chip resources are going to be wasted. They are already.
373 private final double rotate(MouseEvent me) {
374 // center of rotation is the floater
375 double cos = Utils.getCos(x_d_old - floater.x, y_d_old - floater.y, x_d - floater.x, y_d - floater.y);
376 //double sin = Math.sqrt(1 - cos*cos);
377 //double delta = M.getAngle(cos, sin);
378 double delta = Math.acos(cos); // same thing as the two lines above
379 // need to compute the sign of rotation as well: the cross-product!
380 // cross-product:
381 // a = (3,0,0) and b = (0,2,0)
382 // 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).
384 if (Utils.isControlDown(me)) {
385 delta = Math.toDegrees(delta);
386 if (me.isShiftDown()) {
387 // 1 degree angle increments
388 delta = (int)(delta + 0.5);
389 } else {
390 // 10 degrees angle increments: snap to closest
391 delta = (int)((delta + 5.5 * (delta < 0 ? -1 : 1)) / 10) * 10;
393 Utils.showStatus("Angle: " + delta + " degrees");
394 delta = Math.toRadians(delta);
396 // 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.
399 if (Double.isNaN(delta)) {
400 Utils.log2("Selection rotation handle: ignoring NaN angle");
401 return Double.NaN;
404 double zc = (x_d_old - floater.x) * (y_d - floater.y) - (x_d - floater.x) * (y_d_old - floater.y);
405 // correction:
406 if (zc < 0) {
407 delta = -delta;
409 rotate(Math.toDegrees(delta), floater.x, floater.y);
410 return delta;
413 private class RotationHandle extends Handle {
414 final int shift = 50;
415 RotationHandle(int x, int y, int id) {
416 super(x, y, id);
418 public void paint(final Graphics2D g, final Rectangle srcRect, final double mag) {
419 final int x = (int)((this.x - srcRect.x)*mag) + shift;
420 final int y = (int)((this.y - srcRect.y)*mag);
421 final int fx = (int)((floater.x - srcRect.x)*mag);
422 final int fy = (int)((floater.y - srcRect.y)*mag);
423 draw(g, fx, fy, x, y);
425 private void draw(final Graphics2D g, int fx, int fy, int x, int y) {
426 g.setColor(Color.white);
427 g.drawLine(fx, fy, x, y);
428 g.fillOval(x -4, y -4, 9, 9);
429 g.setColor(Color.black);
430 g.drawOval(x -2, y -2, 5, 5);
432 public void paintMoving(final Graphics2D g, final Rectangle srcRect, final double mag, final Point mouse) {
433 // mouse as xMouse,yMouse from ImageCanvas: world coordinates, not screen!
434 final int fx = (int)((floater.x - srcRect.x)*mag);
435 final int fy = (int)((floater.y - srcRect.y)*mag);
436 // vector
437 double vx = (mouse.x - srcRect.x)*mag - fx;
438 double vy = (mouse.y - srcRect.y)*mag - fy;
439 //double len = Math.sqrt(vx*vx + vy*vy);
440 //vx = (vx / len) * 50;
441 //vy = (vy / len) * 50;
442 draw(g, fx, fy, fx + (int)vx, fy + (int)vy);
444 public boolean contains(int x_p, int y_p, double radius) {
445 final double mag = display.getCanvas().getMagnification();
446 final double x = this.x + shift / mag;
447 final double y = this.y;
448 if (x - radius <= x_p && x + radius >= x_p
449 && y - radius <= y_p && y + radius >= y_p) return true;
450 return false;
452 public void drag(MouseEvent me, int dx, int dy) {
453 /// Bad design, I know, I'm ignoring the dx,dy
454 // how:
455 // center is the floater
457 rotate(me);
461 private class Floater extends Handle {
462 Floater(int x, int y, int id) {
463 super(x,y, id);
465 public void paint(Graphics2D g, Rectangle srcRect, double mag) {
466 int x = (int)((this.x - srcRect.x)*mag);
467 int y = (int)((this.y - srcRect.y)*mag);
468 Composite co = g.getComposite();
469 g.setXORMode(Color.white);
470 g.drawOval(x -10, y -10, 21, 21);
471 g.drawRect(x -1, y -15, 3, 31);
472 g.drawRect(x -15, y -1, 31, 3);
473 g.setComposite(co); // undo XOR paint
475 public Rectangle getBoundingBox(Rectangle b) {
476 b.x = this.x - 15;
477 b.y = this.y - 15;
478 b.width = this.x + 31;
479 b.height = this.y + 31;
480 return b;
482 public void drag(MouseEvent me, int dx, int dy) {
483 this.x += dx;
484 this.y += dy;
485 RO.x = this.x;
486 RO.y = this.y;
488 public void center() {
489 this.x = RO.x = box.x + box.width/2;
490 this.y = RO.y = box.y + box.height/2;
492 public boolean contains(int x_p, int y_p, double radius) {
493 return super.contains(x_p, y_p, radius*3.5);
497 public void centerFloater() {
498 floater.center();
501 /** No display bounds are checked, the floater can be placed wherever you want. */
502 public void setFloater(int x, int y) {
503 floater.x = x;
504 floater.y = y;
507 public int getFloaterX() { return floater.x; }
508 public int getFloaterY() { return floater.y; }
510 private void setHandles(final Rectangle b) {
511 final int tx = b.x;
512 final int ty = b.y;
513 final int tw = b.width;
514 final int th = b.height;
515 NW.set(tx, ty);
516 N.set(tx + tw/2, ty);
517 NE.set(tx + tw, ty);
518 E.set(tx + tw, ty + th/2);
519 SE.set(tx + tw, ty + th);
520 S.set(tx + tw/2, ty + th);
521 SW.set(tx, ty + th);
522 W.set(tx, ty + th/2);
525 private AffineTransform accum_affine = null;
527 /** Skips current layer, since its done already. */
528 synchronized protected void applyAndPropagate(final Set<Layer> sublist) {
529 if (null == accum_affine) {
530 Utils.log2("Cannot apply to other layers: undo/redo was used.");
531 return;
533 if (0 == sublist.size()) {
534 Utils.logAll("No layers to apply to!");
535 return;
537 // Check if there are links across affected layers
538 if (Displayable.areThereLayerCrossLinks(sublist, false)) {
539 if (ControlWindow.isGUIEnabled()) {
540 YesNoDialog yn = ControlWindow.makeYesNoDialog("Warning!", "Some objects are linked!\nThe transformation would alter interrelationships.\nProceed anyway?");
541 if ( ! yn.yesPressed()) return;
542 } else {
543 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.");
544 return;
547 // Add undo step
548 final ArrayList<Displayable> al = new ArrayList<Displayable>();
549 for (final Layer l : sublist) {
550 al.addAll(l.getDisplayables());
552 display.getLayer().getParent().addTransformStep(al);
555 // Must capture last step of free affine when using affine points:
556 if (null != free_affine && null != model) {
557 accum_affine.preConcatenate(free_affine);
558 accum_affine.preConcatenate(model.createAffine());
561 // Apply!
562 for (final Layer l : sublist) {
563 if (display.getLayer() == l) continue; // already applied
564 l.apply(Displayable.class, accum_affine);
567 // Record current state as last step in undo queue
568 display.getLayer().getParent().addTransformStep(al);
571 public boolean apply() {
572 // Notify each Displayable that any set of temporary transformations are over.
573 // The transform is the same, has not changed. This is just sending an event.
574 for (final Displayable d : display.getSelection().getAffected()) {
575 d.setAffineTransform( d.getAffineTransform() );
577 return true;
580 public boolean cancel() {
581 if (null != history) {
582 // apply first
583 display.getLayer().getParent().applyTransforms(((TransformationStep)history.get(0)).ht);
585 return true;
588 private class AffinePoint {
589 int x, y;
590 AffinePoint(int x, int y) {
591 this.x = x;
592 this.y = y;
594 public boolean equals(Object ob) {
595 //if (!ob.getClass().equals(AffinePoint.class)) return false;
596 AffinePoint ap = (AffinePoint) ob;
597 double mag = display.getCanvas().getMagnification();
598 double dx = mag * ( ap.x - this.x );
599 double dy = mag * ( ap.y - this.y );
600 double d = dx * dx + dy * dy;
601 return d < 64.0;
603 void translate(int dx, int dy) {
604 x += dx;
605 y += dy;
607 private void paint(Graphics2D g) {
608 int x = display.getCanvas().screenX(this.x);
609 int y = display.getCanvas().screenY(this.y);
610 Utils.drawPoint(g, x, y);
614 private ArrayList<AffinePoint> affine_handles = null;
615 private ArrayList< mpicbg.models.PointMatch > matches = null;
616 private mpicbg.models.Point[] p = null;
617 private mpicbg.models.Point[] q = null;
618 private mpicbg.models.AbstractAffineModel2D<?> model = null;
619 private AffineTransform free_affine = null;
620 private HashMap initial_affines = null;
622 private void forgetAffine() {
623 affine_handles = null;
624 matches = null;
625 p = q = null;
626 model = null;
627 free_affine = null;
628 initial_affines = null;
631 private void initializeModel() {
632 // Store current "initial" state in the accumulated affine
633 if (null != free_affine && null != model && null != accum_affine) {
634 accum_affine.preConcatenate(free_affine);
635 accum_affine.preConcatenate(model.createAffine());
638 free_affine = new AffineTransform();
639 initial_affines = getTransformationsCopy();
641 int size = affine_handles.size();
643 switch (size) {
644 case 0:
645 model = null;
646 q = p = null;
647 matches = null;
648 return;
649 case 1:
650 model = new mpicbg.models.TranslationModel2D();
651 break;
652 case 2:
653 model = new mpicbg.models.SimilarityModel2D();
654 break;
655 case 3:
656 model = new mpicbg.models.AffineModel2D();
657 break;
659 p = new mpicbg.models.Point[size];
660 q = new mpicbg.models.Point[size];
661 matches = new ArrayList< mpicbg.models.PointMatch >();
662 int i = 0;
663 for (final AffinePoint ap : affine_handles) {
664 p[i] = new mpicbg.models.Point(new float[]{ap.x, ap.y});
665 q[i] = p[i].clone();
666 matches.add(new mpicbg.models.PointMatch(p[i], q[i]));
667 i++;
671 private void freeAffine(AffinePoint affp) {
672 // The selected point
673 final float[] w = q[affine_handles.indexOf(affp)].getW();
674 w[0] = affp.x;
675 w[1] = affp.y;
677 try {
678 model.fit(matches);
679 } catch (Exception e) {}
681 final AffineTransform model_affine = model.createAffine();
682 for (final Iterator it = initial_affines.entrySet().iterator(); it.hasNext(); ) {
683 final Map.Entry e = (Map.Entry)it.next();
684 final Displayable d = (Displayable)e.getKey();
685 final AffineTransform at = new AffineTransform((AffineTransform)e.getValue());
686 at.preConcatenate(free_affine);
687 at.preConcatenate(model_affine);
688 d.setAffineTransform(at);
692 private void fixAffinePoints(final AffineTransform at) {
693 if (null != matches) {
694 float[] po = new float[2];
695 for (final AffinePoint affp : affine_handles) {
696 po[0] = affp.x;
697 po[1] = affp.y;
698 at.transform(po, 0, po, 0, 1);
699 affp.x = (int)po[0];
700 affp.y = (int)po[1];
702 // Model will be reinitialized when needed
703 free_affine.setToIdentity();
704 model = null;
708 private AffinePoint affp = null;
711 public void mousePressed(MouseEvent me, int x_p, int y_p, double magnification) {
712 grabbed = null; // reset
713 if (me.isShiftDown()) {
714 if (Utils.isControlDown(me) && null != affine_handles) {
715 if (affine_handles.remove(new AffinePoint(x_p, y_p))) {
716 if (0 == affine_handles.size()) affine_handles = null;
717 else initializeModel();
719 return;
721 if (null == affine_handles) {
722 affine_handles = new ArrayList<AffinePoint>();
724 if (affine_handles.size() < 3) {
725 affine_handles.add(new AffinePoint(x_p, y_p));
726 if (1 == affine_handles.size()) {
727 free_affine = new AffineTransform();
728 initial_affines = getTransformationsCopy();
730 initializeModel();
732 return;
733 } else if (null != affine_handles) {
734 int index = affine_handles.indexOf(new AffinePoint(x_p, y_p));
735 if (-1 != index) {
736 affp = affine_handles.get(index);
737 return;
741 // find scale handle
742 double radius = 4 / magnification;
743 if (radius < 1) radius = 1;
744 // start with floater (the last)
745 for (int i=handles.length -1; i>-1; i--) {
746 if (handles[i].contains(x_p, y_p, radius)) {
747 grabbed = handles[i];
748 if (grabbed.id >= rNW && grabbed.id <= ROTATION) rotating = true;
749 return;
753 // if none grabbed, then drag the whole thing
754 dragging = false; //reset
755 if (box.x <= x_p && box.y <= y_p && box.x + box.width >= x_p && box.y + box.height >= y_p) {
756 dragging = true;
759 public void mouseDragged(MouseEvent me, int x_p, int y_p, int x_d, int y_d, int x_d_old, int y_d_old) {
760 // Store old for rotation handle:
761 this.x_d = x_d;
762 this.y_d = y_d;
763 this.x_d_old = x_d_old;
764 this.y_d_old = y_d_old;
766 // compute translation
767 int dx = x_d - x_d_old;
768 int dy = y_d - y_d_old;
770 execDrag(me, dx, dy);
772 mouse_dragged = true; // after execDrag, so the first undo step is added.
774 private void execDrag(MouseEvent me, int dx, int dy) {
775 if (0 == dx && 0 == dy) return;
776 if (null != affp) {
777 affp.translate(dx, dy);
778 if (null == model) {
779 // Model has been canceled by a transformation from the other handles
780 initializeModel();
782 // Passing on the translation from start
783 freeAffine(affp);
784 return;
787 if (null != grabbed) {
788 // drag the handle and perform whatever task it has assigned
789 grabbed.drag(me, dx, dy);
790 } else if (dragging) {
791 // drag all selected and linked
792 translate(dx, dy);
793 //and the box!
794 box.x += dx;
795 box.y += dy;
796 // and the handles!
797 setHandles(box);
801 public void mouseReleased(MouseEvent me, int x_p, int y_p, int x_d, int y_d, int x_r, int y_r) {
803 // Record current state for selected Displayable set, if there was any change:
804 final int dx = x_r - x_p;
805 final int dy = y_r - y_p;
806 if (0 != dx || 0 != dy) {
807 display.getLayerSet().addTransformStep(display.getSelection().getAffected()); // all selected and their links: i.e. all that will change
810 // me is null when calling from Display, because of popup interfering with mouseReleased
811 if (null != me) execDrag(me, x_r - x_d, y_r - y_d);
813 // recalculate box
814 resetBox();
816 //reset
817 if ((null != grabbed && grabbed.id <= iW) || dragging) {
818 floater.center();
820 grabbed = null;
821 dragging = false;
822 rotating = false;
823 affp = null;
824 mouse_dragged = false;
827 /** Recalculate box and reset handles. */
828 public void resetBox() {
829 box = null;
830 Rectangle b = new Rectangle();
831 for (final Displayable d : display.getSelection().getSelected()) {
832 b = d.getBoundingBox(b);
833 if (null == box) box = (Rectangle)b.clone();
834 box.add(b);
836 if (null != box) setHandles(box);
839 /** Rotate the objects in the current selection by the given angle, in degrees, relative to the x_o, y_o origin. */
840 public void rotate(final double angle, final int xo, final int yo) {
841 final AffineTransform at = new AffineTransform();
842 at.rotate(Math.toRadians(angle), xo, yo);
844 addUndoStep();
846 if (null != accum_affine) accum_affine.preConcatenate(at);
848 for (final Displayable d : display.getSelection().getAffected()) {
849 d.preTransform(at, false); // all linked ones included in the hashset
851 fixAffinePoints(at);
852 resetBox();
855 /** Translate all selected objects and their links by the given differentials. The floater position is unaffected; if you want to update it call centerFloater() */
856 public void translate(final double dx, final double dy) {
857 final AffineTransform at = new AffineTransform();
858 at.translate(dx, dy);
860 addUndoStep();
862 if (null != accum_affine) accum_affine.preConcatenate(at);
864 for (final Displayable d : display.getSelection().getAffected()) {
865 d.preTransform(at, false); // all linked ones already included in the hashset
867 fixAffinePoints(at);
868 resetBox();
871 /** Scale all selected objects and their links by by the given scales, relative to the floater position. . */
872 public void scale(final double sx, final double sy) {
873 if (0 == sx || 0 == sy) {
874 Utils.showMessage("Cannot scale to 0.");
875 return;
878 final AffineTransform at = new AffineTransform();
879 at.translate(floater.x, floater.y);
880 at.scale(sx, sy);
881 at.translate(-floater.x, -floater.y);
883 addUndoStep();
885 if (null != accum_affine) accum_affine.preConcatenate(at);
887 for (final Displayable d : display.getSelection().getAffected()) {
888 d.preTransform(at, false); // all linked ones already included in the hashset
890 fixAffinePoints(at);
891 resetBox();
894 public Rectangle getRepaintBounds() {
895 Rectangle b = display.getSelection().getLinkedBox();
896 b.add(floater.getBoundingBox(new Rectangle()));
897 return b;
900 public void srcRectUpdated(Rectangle srcRect, double magnification) {}
901 public void magnificationUpdated(Rectangle srcRect, double magnification) {}