package ini.trakem2.display; import ij.gui.GenericDialog; import ij.gui.OvalRoi; import ij.gui.PolygonRoi; import ij.gui.Roi; import ij.gui.ShapeRoi; import ij.process.FloatPolygon; import ini.trakem2.imaging.Segmentation; import ini.trakem2.utils.IJError; import ini.trakem2.utils.M; import ini.trakem2.utils.OptionPanel; import ini.trakem2.utils.ProjectToolbar; import ini.trakem2.utils.Utils; import ini.trakem2.vector.VectorString3D; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; public class AreaWrapper { private final Area area; private Painter painter = null; private Rectangle r_old = null; private Displayable source = null; public AreaWrapper(final Displayable source, final Area area) { this.source = source; this.area = area; } public AreaWrapper() { this(null, new Area()); } public AreaWrapper(final Area area) { this(null, area); } public void setSource(final Displayable source) { this.source = source; } public Displayable getSource() { return source; } public Area getArea() { return area; } /** Does not set the @param area, but copies its internal data. */ public void putData(final Area a) { if (this.area == a) return; this.area.reset(); this.area.add(a); } /** Add an area in world coordinates. */ public void add(final Area wa, final Layer layer) { try { this.area.add(wa.createTransformedArea(source.getAffineTransform().createInverse())); ((AreaContainer)source).calculateBoundingBox(layer); } catch (NoninvertibleTransformException nite) { IJError.print(nite); } } /** Subtract an area in world coordinates. */ public void subtract(final Area wa, final Layer layer) { try { this.area.subtract(wa.createTransformedArea(source.getAffineTransform().createInverse())); ((AreaContainer)source).calculateBoundingBox(layer); } catch (NoninvertibleTransformException nite) { IJError.print(nite); } } /** Add an area that needs to be transformed by tmp first to bring it to world coordinates; * will MODIFY the to_world AffineTransform object. */ public void add(final Area a, final AffineTransform to_world) { try { to_world.preConcatenate(source.getAffineTransform().createInverse()); this.area.add(a.createTransformedArea(to_world)); } catch (NoninvertibleTransformException nite) { IJError.print(nite); } } public void paint(final Graphics2D g, final AffineTransform aff, final boolean fill, final Color color) { g.setColor(color); if (!area.isEmpty()) { if (fill) g.fill(area.createTransformedArea(aff)); else g.draw(area.createTransformedArea(aff)); } if (null != this.painter) { try { this.painter.paint(g, aff, fill); } catch (Exception e) {} } } private final class Painter extends Thread { /** The area to paint into when done or when removing. */ final private Area target_area; /** The temporary area to paint to, which is only different than target_area when adding. */ final private Area area; /** The list of all painted points. */ private final LinkedList<Point> points = new LinkedList<Point>(); /** The last point on which a paint event was done. */ private volatile Point previous_p = null; private final int brush_size; // the diameter private final Area brush; final private int leftClick = InputEvent.BUTTON1_MASK; final private int alt = (InputEvent.ALT_MASK|InputEvent.SHIFT_MASK); final private DisplayCanvas dc = Display.getFront().getCanvas(); final private int flags; final private boolean adding; private final Layer la; private final Displayable source; private final Object arealock = new Object(), pointslock = new Object(); private final ExecutorService accumulator; private final ScheduledExecutorService composer; private final ScheduledFuture<?> composition; private final Runnable interpolator; Painter(final Area area, final double mag, final Layer la, final Displayable source, final int flags) throws Exception { super("AreaWrapper.Painter"); setPriority(Thread.NORM_PRIORITY); this.la = la; this.source = source; this.flags = flags; this.adding = (0 == (flags & alt)); // if adding areas, make it be a copy, to be added on mouse release // (In this way, the receiving Area is small and can be operated on fast) if (adding) { this.target_area = area; this.area = new Area(); } else { this.target_area = area; this.area = area; } brush_size = ProjectToolbar.getBrushSize(); brush = makeBrush(brush_size, mag); if (null == brush) throw new RuntimeException("Can't paint with brush of size 0."); accumulator = Utils.newFixedThreadPool(1, "AreaWrapper-accumulator"); composer = Executors.newScheduledThreadPool(1); this.interpolator = new Runnable() { public void run() { final ArrayList<Point> ps; final int n_points; synchronized (pointslock) { n_points = points.size(); if (0 == n_points) return; ps = new ArrayList<Point>(points); points.clear(); points.add(ps.get(n_points -1)); // to start the next spline from the last point } final AffineTransform at_inv; try { at_inv = source.getAffineTransform().createInverse(); } catch (NoninvertibleTransformException nite) { IJError.print(nite); return; } if (n_points < 2) { // No interpolation required final AffineTransform atb = new AffineTransform(1, 0, 0, 1, ps.get(0).x, ps.get(0).y); atb.preConcatenate(at_inv); Area chunk = slashInInts(brush.createTransformedArea(atb)); synchronized (arealock) { if (adding) Painter.this.area.add(chunk); else Painter.this.area.subtract(chunk); } return; } try { // paint the regions between points, using spline interpolation // A cheap way would be to just make a rectangle between both points, with thickess radius. // A better, expensive way is to fit a spline first, then add each one as a circle. // The spline way is wasteful, but way more precise and beautiful. Since there's only one repaint, it's not excessively slow. int[] xp = new int[ps.size()]; int[] yp = new int[xp.length]; int j = 0; for (final Point p : ps) { xp[j] = p.x; yp[j] = p.y; j++; } PolygonRoi proi = new PolygonRoi(xp, yp, xp.length, Roi.POLYLINE); proi.fitSpline(); FloatPolygon fp = proi.getFloatPolygon(); proi = null; double[] xpd = new double[fp.npoints]; double[] ypd = new double[fp.npoints]; // Fails: fp contains float[], which for some reason cannot be copied into double[] //System.arraycopy(fp.xpoints, 0, xpd, 0, xpd.length); //System.arraycopy(fp.ypoints, 0, ypd, 0, ypd.length); for (int i=0; i<xpd.length; i++) { xpd[i] = fp.xpoints[i]; ypd[i] = fp.ypoints[i]; } fp = null; // VectorString2D resampling doesn't work VectorString3D vs = new VectorString3D(xpd, ypd, new double[xpd.length], false); double delta = ((double)brush_size) / 10; if (delta < 1) delta = 1; vs.resample(delta); xpd = vs.getPoints(0); ypd = vs.getPoints(1); vs = null; // adjust first and last points back to integer precision Point po = ps.get(0); xpd[0] = po.x; ypd[0] = po.y; po = ps.get(ps.size()-1); xpd[xpd.length-1] = po.x; ypd[ypd.length-1] = po.y; final Area chunk = new Area(); final AffineTransform atb = new AffineTransform(); for (int i=0; i<xpd.length; i++) { atb.setToTranslation((int)xpd[i], (int)ypd[i]); // always integers atb.preConcatenate(at_inv); chunk.add(slashInInts(brush.createTransformedArea(atb))); } synchronized (arealock) { if (adding) Painter.this.area.add(chunk); else Painter.this.area.subtract(chunk); } Display.repaint(Painter.this.la, 3, r_old, false, false); } catch (Exception e) { IJError.print(e); } } }; composition = composer.scheduleWithFixedDelay(interpolator, 200, 500, TimeUnit.MILLISECONDS); start(); } /** Paint only if area is not the target_area. */ private final void paint(final Graphics2D g, final AffineTransform aff, final boolean fill) { if (area == target_area) return; if (null != area) { synchronized (arealock) { if (null == area) return; if (fill) g.fill(area.createTransformedArea(aff)); else g.draw(area.createTransformedArea(aff)); // won't be perfect except on mouse release } } } final AtomicBoolean quitted = new AtomicBoolean(false); /** This method must be called exactly and only once.*/ final void quit() { //if (!this.paint) return; // already quit //this.paint = false; if (isInterrupted()) return; interrupt(); // isInterrupted() is not enough of a check because it's not synchronized (or for other reasons), so use: if (quitted.getAndSet(true)) { return; } // Make interpolated points affect add or subtract operations try { accumulator.shutdownNow(); composition.cancel(true); composer.shutdown(); try { composer.awaitTermination(30, TimeUnit.SECONDS); accumulator.awaitTermination(30, TimeUnit.SECONDS); // must wait until the threads have actually died } catch (InterruptedException ie) {} // proceed in any case // From now on, synchronizing on arealock and pointslock should not be necessary: // no other threads are operating on the areas. // But paranoid programming requires that I used it anyway. final int psize; synchronized (pointslock) { psize = points.size(); } if (psize > 1) { // one last time: interpolator.run(); } else { // Just one point: no interpolation needed // merge the temporary Area, if any, with the general one if (adding) { synchronized (arealock) { this.target_area.add(area); } } else { // If subtracting, it was already done return; } } if (adding) { final Area added; synchronized (arealock) { added = new Area(this.target_area); added.add(area); } // now, depending on paint mode, alter the new target area: if (PAINT_OVERLAP == PP.paint_mode) { // Nothing happens with PAINT_OVERLAP, default mode. } else { final Map<Displayable,List<Area>> other_areas = la.getParent().findAreas(la, added.createTransformedArea(source.getAffineTransform()).getBounds(), true); // prepare undo step: final HashMap<Displayable,Runnable> ops = PAINT_ERODE == PP.paint_mode ? new HashMap<Displayable,Runnable>() : null; for (final Map.Entry<Displayable,List<Area>> e : other_areas.entrySet()) { final Displayable d = e.getKey(); if (source == d) continue; for (final Area a : e.getValue()) { if (this.area == a) continue; AffineTransform aff; switch (PP.paint_mode) { case PAINT_ERODE: // subtract this target_area from any other Area that overlaps with it aff = new AffineTransform(this.source.getAffineTransform()); aff.preConcatenate(d.at.createInverse()); final Area ta = added.createTransformedArea(aff); final Rectangle ta_bounds = ta.getBounds(); if (a.getBounds().intersects(ta_bounds)) { ops.put(d, new Runnable() { public void run() { a.subtract(ta); }}); } break; case PAINT_EXCLUDE: // subtract all other overlapping Area from the target_area aff = new AffineTransform(d.at); aff.preConcatenate(this.source.getAffineTransform().createInverse()); final Area q = a.createTransformedArea(aff); if (q.getBounds().intersects(added.getBounds())) { added.subtract(q); } break; default: Utils.log2("Can't handle paint mode " + PP.paint_mode); break; } } } if (null != ops && ops.size() > 0) { source.getLayerSet().addDataEditStep(ops.keySet()); for (final Runnable r : ops.values()) { r.run(); } something_eroded = true; } } synchronized (arealock) { this.target_area.reset(); this.target_area.add(added); } } // else do nothing, the subtract is already done } catch (Exception ee) { IJError.print(ee); } } /** For best smoothness, each mouse dragged event should be captured!*/ public void run() { final AffineTransform at_inv; try { at_inv = source.getAffineTransform().createInverse(); } catch (NoninvertibleTransformException nite) { IJError.print(nite); return; } // create brush while (!isInterrupted()) { // detect mouse up (don't use 'flags': was recorded on starting up) if (0 == (dc.getModifiers() & leftClick)) { //Utils.log2("--------->> Quit brushing from inside loop"); quit(); return; } final Point p = dc.getCursorLoc(); // as offscreen coords if (p.equals(previous_p) /*|| (null != previous_p && p.distance(previous_p) < brush_size/5) */) { try { Thread.sleep(3); } catch (InterruptedException ie) {} continue; } if (!dc.getSrcRect().contains(p.x, p.y)) { // Ignoring point off srcRect continue; } final Runnable task = new Runnable() { public void run() { final AffineTransform aff = new AffineTransform(1, 0, 0, 1, p.x, p.y); aff.preConcatenate(at_inv); final Area slash = slashInInts(brush.createTransformedArea(aff)); synchronized (arealock) { if (0 == (flags & alt)) { // no modifiers, just add area.add(slash); } else { // with alt down, subtract area.subtract(slash); } } synchronized (pointslock) { points.add(p); } final Rectangle copy = new Rectangle(p.x - brush_size/2, p.y - brush_size/2, brush_size, brush_size); // repaint only the last added slash Display.repaint(la, 3, copy, false, false); // accumulate rectangle for repainting out the brush circle synchronized (arealock) { if (null != r_old) copy.add(r_old); r_old = copy; } } }; try { accumulator.submit(task); } catch (Throwable t) { // will happen when quit() calls accumulator.shutdown(), // which will then refuse to run any task. // Only the jobs completed up to this time point // will be finished, so the trace may finish earlier // than the mouse release point in slow computers. return; } previous_p = p; } } /** Sets the bounds of the created slash, in offscreen coords, to r if r is not null. */ /* private Area createSlash(final AffineTransform atb, final Rectangle r) { Area slash = brush.createTransformedArea(atb); // + int transform, no problem if (null != r) r.setRect(slash.getBounds()); // bring to the current transform, if any if (!at.isIdentity()) { try { slash = slash.createTransformedArea(at.createInverse()); } catch (NoninvertibleTransformException nite) { IJError.print(nite); return null; } } // avoid problems with floating-point points, for example inability to properly fill areas or delete them. return slashInInts(slash); } */ private final Area slashInInts(final Area area) { int[] x = new int[400]; int[] y = new int[400]; int next = 0; for (PathIterator pit = area.getPathIterator(null); !pit.isDone(); ) { if (x.length == next) { int[] x2 = new int[x.length + 200]; int[] y2 = new int[y.length + 200]; System.arraycopy(x, 0, x2, 0, x.length); System.arraycopy(y, 0, y2, 0, y.length); x = x2; y = y2; } final float[] coords = new float[6]; int seg_type = pit.currentSegment(coords); switch (seg_type) { case PathIterator.SEG_MOVETO: case PathIterator.SEG_LINETO: x[next] = (int)coords[0]; y[next] = (int)coords[1]; break; case PathIterator.SEG_CLOSE: break; default: Utils.log2("WARNING: slashInInts unhandled seg type."); break; } pit.next(); if (pit.isDone()) break; // the loop next++; } // resize back (now next is the length): if (x.length == next) { int[] x2 = new int[next]; int[] y2 = new int[next]; System.arraycopy(x, 0, x2, 0, next); System.arraycopy(y, 0, y2, 0, next); x = x2; y = y2; } return new Area(new Polygon(x, y, next)); } } static public Area makeMouseBrush(int diameter, double mag) { Display front = Display.getFront(); Area brush = makeBrush(diameter, mag); if (null == front) return brush; Point p = front.getCanvas().getCursorLoc(); return brush.createTransformedArea(new AffineTransform(1, 0, 0, 1, p.x, p.y)); } /** This method could get tones of improvement, which should be pumped upstream into ImageJ's RoiBrush class which is creating it at every while(true) {} iteration!!! * The returned area has its coordinates centered around 0,0 */ static public Area makeBrush(int diameter, double mag) { if (diameter < 1) return null; if (mag >= 1) return new Area(new OvalRoi(-diameter/2, -diameter/2, diameter, diameter).getPolygon()); // else, create a smaller brush and transform it up, i.e. less precise, less points to store -but precision matches what the eye sees, and allows for much better storage -less points. int screen_diameter = (int)(diameter * mag); if (0 == screen_diameter) return null; // can't paint at this mag with this diameter Area brush = new Area(new OvalRoi(-screen_diameter/2, -screen_diameter/2, screen_diameter, screen_diameter).getPolygon()); // scale to world coordinates AffineTransform at = new AffineTransform(); at.scale(1/mag, 1/mag); return brush.createTransformedArea(at); // smooth out edges /* Polygon pol = new OvalRoi(-diameter/2, -diameter/2, diameter, diameter).getPolygon(); Polygon pol2 = new Polygon(); // cheap and fast: skip every other point, since all will be square angles for (int i=0; i<pol.npoints; i+=2) { pol2.addPoint(pol.xpoints[i], pol.ypoints[i]); } return new Area(pol2); // the above works nice, but then the fill and fill-remove don't work properly (there are traces in the edges) // Needs a workround: before adding/substracting, enlarge the polygon to have square edges */ } private boolean something_eroded = false; private Segmentation.BlowCommander blowcommander = null; private List<Runnable> post_mouseReleased_tasks = null; // shared, and thus made null at every mouse release static private Integer controller_key = null; public void mousePressed(final MouseEvent me, Layer la, final int x_p_w, final int y_p_w, final double mag) { mousePressed(me, la, x_p_w, y_p_w, mag, null); } public void mousePressed(final MouseEvent me, Layer la, final int x_p_w, final int y_p_w, final double mag, final List<Runnable> post_tasks) { this.post_mouseReleased_tasks = post_tasks; final int tool = ProjectToolbar.getToolId(); if (ProjectToolbar.BRUSH == tool) { if (null != AreaWrapper.controller_key) { return; } if (me.isShiftDown()) { // fill/erase a hole/area if the clicked point lays within one // An area in world coords: Area bmin = null; Area bmax = null; final ArrayList<Area> intersecting = new ArrayList<Area>(); // a list of areas in world coords // Try to find a hole in this or another visible Area, but fill it this int min_area = Integer.MAX_VALUE; int max_area = 0; final Map<Displayable,List<Area>> other_areas = la.getParent().findAreas(la, new Rectangle(x_p_w, y_p_w, 1, 1), true); for (final Map.Entry<Displayable,List<Area>> e : other_areas.entrySet()) { final Displayable d = e.getKey(); for (final Area a : e.getValue()) { // bring point to zd space final Point2D.Double p = d.inverseTransformPoint(x_p_w, y_p_w); final Polygon polygon = M.findPath(a, (int)p.x, (int)p.y); if (null != polygon) { Area bw = new Area(polygon).createTransformedArea(d.at); Rectangle bounds = bw.getBounds(); int pol_area = bounds.width * bounds.height; if (pol_area < min_area) { bmin = bw; min_area = pol_area; } if (pol_area > max_area) { bmax = bw; max_area = pol_area; } intersecting.add(bw); } } } // Take the largest area and subtract from it all other visible areas if (intersecting.size() > 1) { Area compound = new Area(bmax); for (Area a : intersecting) { if (bmax == a) continue; compound.intersect(a); } if (!compound.isSingular()) { Polygon polygon = M.findPath(compound, x_p_w, y_p_w); if (null != polygon) { compound = new Area(polygon); } } Rectangle cbounds = compound.getBounds(); int carea = cbounds.width * cbounds.height; if (carea < min_area) { min_area = carea; bmin = compound; } } // Also try to merge all visible areas in the current field of view of the current layer and find a hole there final Area all = new Area(); // in world coords for (final Map.Entry<Displayable,List<Area>> e : la.getParent().findAreas(la, Display.getFront().getCanvas().getSrcRect(), true).entrySet()) { for (final Area ar : e.getValue()) { all.add(ar.createTransformedArea(e.getKey().at)); } } Polygon polygon = M.findPath(all, x_p_w, y_p_w); // in world coords if (null == polygon && source.getProject().getBooleanProperty("flood_fill_to_image_edge")) { Area patch_area = la.getPatchArea(true); // in world coords Rectangle bounds = patch_area.getBounds(); if (0 != bounds.width && 0 != bounds.height) { patch_area.subtract(all); polygon = M.findPath(patch_area, x_p_w, y_p_w); } } if (null != polygon) { Rectangle bounds = polygon.getBounds(); int pol_area = bounds.width * bounds.height; if (pol_area < min_area) { min_area = pol_area; bmin = new Area(polygon); } } if (null != bmin) { try { // Add b as local to this Area Area blocal = bmin.createTransformedArea(source.getAffineTransform().createInverse()); if (me.isAltDown()) { area.subtract(blocal); } else { area.add(blocal); } ((AreaContainer)source).calculateBoundingBox(la); Display.repaint(la, bmin.getBounds(), 1); // use b, in world coords } catch (NoninvertibleTransformException nite) { IJError.print(nite); } } } else { if (null != this.painter) { this.painter.quit(); // in case there was a mouse release outside the canvas--may not be detected } try { this.painter = new Painter(area, mag, la, source, me.getModifiers()); } catch (Exception e) { Utils.log2("Oops: " + e); } } } else { final ArrayList<Runnable> ptasks = new ArrayList<Runnable>(); if (null != post_mouseReleased_tasks) { ptasks.addAll(post_mouseReleased_tasks); post_mouseReleased_tasks = null; // avoid running them twice } final Displayable src = this.source; ptasks.add(new Runnable() { public void run() { // Add data edit step when done for undo/redo src.getLayerSet().addDataEditStep(src); } }); if (ProjectToolbar.PENCIL == tool) { ptasks.add(new Runnable() { public void run() { // Add data edit step when done for undo/redo src.getLayerSet().addDataEditStep(src); } }); if (Utils.isControlDown(me)) { // Grow with blow tool try { blowcommander = Segmentation.blowRoi(this, la, Display.getFront().getCanvas().getSrcRect(), x_p_w, y_p_w, ptasks); } catch (Exception e) { IJError.print(e); } } else { // Grow with fast marching Segmentation.fastMarching(this, la, Display.getFront().getCanvas().getSrcRect(), x_p_w, y_p_w, ptasks); } } else if (ProjectToolbar.WAND == tool) { Segmentation.magicWand(this, la, Display.getFront().getCanvas().getSrcRect(), x_p_w, y_p_w, ptasks, me.isShiftDown(), me.isAltDown()); } } } public void mouseDragged(MouseEvent me, Layer la, int x_p, int y_p, int x_d, int y_d, int x_d_old, int y_d_old) { // nothing, the BrushThread handles it if (null != AreaWrapper.controller_key && KeyEvent.VK_M == AreaWrapper.controller_key.intValue() && ProjectToolbar.getToolId() == ProjectToolbar.BRUSH) { // "move" the area Rectangle r = area.getBounds(); area.transform(new AffineTransform(1, 0, 0, 1, x_d - x_d_old, y_d - y_d_old)); r.add(new Rectangle(r.x + (x_d_old - x_d), r.y + (y_d_old - y_d), r.width, r.height)); Display.getFront().getCanvas().repaint(source.at.createTransformedShape(r).getBounds(), 1); return; } if (null != blowcommander) { blowcommander.mouseDragged(me, la, x_p, y_p, x_d, y_d, x_d_old, y_d_old); } } public void mouseReleased(MouseEvent me, Layer la, int x_p, int y_p, int x_d, int y_d, int x_r, int y_r) { // No matter what tool, ensure that moving and brushing operations are always terminated: if (null != this.painter) { this.painter.quit(); this.painter = null; } if (null != AreaWrapper.controller_key) { // finish AreaWrapper.controller_key = null; return; } if (null != blowcommander) { blowcommander.mouseReleased(me, la, x_p, y_p, x_d, y_d, x_r, y_r); blowcommander = null; } if (null != post_mouseReleased_tasks) { for (Runnable task : post_mouseReleased_tasks) task.run(); } if (something_eroded) { Display.repaint(source.getLayerSet()); something_eroded = false; } // Repaint instead the last rectangle, to erase the circle if (null != r_old) { Display.repaint(la, r_old, 3, false); r_old = null; } // repaint the navigator and snapshot Display.repaint(la, source); } static public final int PAINT_OVERLAP = 0; static public final int PAINT_EXCLUDE = 1; static public final int PAINT_ERODE = 2; static public class PaintParameters { public float default_alpha = 0.4f; public int paint_mode = AreaWrapper.PAINT_OVERLAP; public boolean setup() { GenericDialog gd = new GenericDialog("Paint parameters"); gd.addSlider("Default_alpha", 0, 100, default_alpha * 100); final String[] modes = {"Allow overlap", "Exclude others", "Erode others"}; gd.addChoice("Paint mode", modes, modes[paint_mode]); gd.showDialog(); if (gd.wasCanceled()) return false; this.default_alpha = (float) gd.getNextNumber(); if (this.default_alpha > 1) this.default_alpha = 1f; else if (this.default_alpha < 0) this.default_alpha = 0.4f; // back to default's default value this.paint_mode = gd.getNextChoiceIndex(); // trigger update of GUI radio buttons on all displays: Display.toolChanged(ProjectToolbar.BRUSH); return true; } public OptionPanel asOptionPanel() { OptionPanel op = new OptionPanel(); final String[] modes = {"Allow overlap", "Exclude others", "Erode others"}; op.addChoice("Area paint mode:", modes, paint_mode, new OptionPanel.ChoiceIntSetter(this, "paint_mode")); return op; } } static public final PaintParameters PP = new PaintParameters(); public void keyPressed(KeyEvent ke, DisplayCanvas dc, Layer la) { final int keyCode = ke.getKeyCode(); if (null != AreaWrapper.controller_key && KeyEvent.VK_ENTER == keyCode) { AreaWrapper.controller_key = null; ke.consume(); return; } if (KeyEvent.VK_M == keyCode && ProjectToolbar.getToolId() == ProjectToolbar.BRUSH) { AreaWrapper.controller_key = keyCode; ke.consume(); return; } try { switch (keyCode) { case KeyEvent.VK_C: // COPY DisplayCanvas.setCopyBuffer(source.getClass(), area.createTransformedArea(source.getAffineTransform())); ke.consume(); return; case KeyEvent.VK_V: // PASTE // Casting a null is fine, and addArea survives a null. Area wa = (Area) DisplayCanvas.getCopyBuffer(source.getClass()); if (null != wa) { source.getLayerSet().addDataEditStep(source); add(wa, la); // wa is in world coordinates ((AreaContainer)source).calculateBoundingBox(la); source.getLayerSet().addDataEditStep(source); } ke.consume(); return; case KeyEvent.VK_F: // fill all holes source.getLayerSet().addDataEditStep(source); fillHoles(); source.getLayerSet().addDataEditStep(source); ke.consume(); return; case KeyEvent.VK_X: // remove area from current layer, if any if (area.isEmpty()) return; source.getLayerSet().addDataEditStep(source); area.reset(); ((AreaContainer)source).calculateBoundingBox(la); source.getLayerSet().addDataEditStep(source); ke.consume(); return; } } catch (Exception e) { IJError.print(e); } finally { if (ke.isConsumed()) { Display.repaint(la, source.getBoundingBox(), 5); source.linkPatches(); return; } } Roi roi = dc.getFakeImagePlus().getRoi(); if (null == roi) return; // Check ROI switch (keyCode) { case KeyEvent.VK_A: case KeyEvent.VK_D: case KeyEvent.VK_K: if (!M.isAreaROI(roi)) { Utils.log("Only accepts region ROIs, not lines."); return; } break; } ShapeRoi sroi = new ShapeRoi(roi); try { switch (keyCode) { case KeyEvent.VK_A: add(M.getArea(sroi), la); ke.consume(); break; case KeyEvent.VK_D: // VK_S is for 'save' always subtract(M.getArea(sroi), la); ke.consume(); break; } if (ke.isConsumed()) { Display.repaint(la, source.getBoundingBox(), 5); source.linkPatches(); } } catch (Exception e) { Utils.log("Could not add ROI to area at layer " + dc.getDisplay().getLayer() + " : " + e); } } public void fillHoles() { Polygon pol = new Polygon(); for (PathIterator pit = area.getPathIterator(null); !pit.isDone(); ) { float[] coords = new float[6]; int seg_type = pit.currentSegment(coords); switch (seg_type) { case PathIterator.SEG_MOVETO: case PathIterator.SEG_LINETO: pol.addPoint((int)coords[0], (int)coords[1]); break; case PathIterator.SEG_CLOSE: area.add(new Area(pol)); // prepare next: pol = new Polygon(); break; default: Utils.log2("WARNING: unhandled seg type."); break; } pit.next(); if (pit.isDone()) { break; } } } }