package com.kreative.paint.tool; import java.awt.Cursor; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Point2D; import java.util.BitSet; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import com.kreative.paint.Layer; import com.kreative.paint.document.draw.DrawObject; import com.kreative.paint.document.draw.PaintSettings; import com.kreative.paint.document.tile.PaintSurface; import com.kreative.paint.util.Bitmap; import com.kreative.paint.util.CursorUtils; public class FillTool extends AbstractPaintDrawTool { private static final int K = 0xFF000000; // A bit of history: When I was little, I thought this icon/cursor // was a graduation cap. It actually wasn't until I read the // HyperTalk Script Language Guide and learned the same tool in // HyperCard was called the "bucket" tool that I got it. private static final Image icon = ToolUtilities.makeIcon( 16, 16, new int[] { 0,0,0,0,0,K,K,K,0,0,0,0,0,0,0,0, 0,0,0,0,K,0,0,0,K,0,0,0,0,0,0,0, 0,0,0,0,K,0,0,K,K,0,0,0,0,0,0,0, 0,0,0,0,K,0,K,0,K,K,0,0,0,0,0,0, 0,0,0,0,K,K,0,0,K,0,K,K,0,0,0,0, 0,0,0,0,K,0,0,0,K,0,0,K,K,K,0,0, 0,0,0,K,0,0,0,0,K,0,0,0,K,K,K,0, 0,0,K,0,0,0,0,K,0,K,0,0,0,K,K,K, 0,K,0,0,0,0,0,0,K,0,0,0,0,K,K,K, K,0,0,0,0,0,0,0,0,0,0,0,K,K,K,K, K,0,0,0,0,0,0,0,0,0,0,K,0,K,K,K, 0,K,0,0,0,0,0,0,0,0,K,0,0,K,K,K, 0,0,K,0,0,0,0,0,0,K,0,0,0,K,K,K, 0,0,0,K,0,0,0,0,K,0,0,0,0,K,K,K, 0,0,0,0,K,0,0,K,0,0,0,0,0,K,K,0, 0,0,0,0,0,K,K,0,0,0,0,0,0,K,0,0, } ); public ToolCategory getCategory() { return ToolCategory.PAINT; } protected Image getBWIcon() { return icon; } public boolean mousePressed(ToolEvent e) { boolean all = e.isCtrlDown(); Layer layer = e.getLayer(); if (layer == null) { return false; } else if (e.isInDrawMode()) { e.beginTransaction(getName()); Point2D.Float lp = new Point2D.Float(e.getCanvasX()-layer.getX(), e.getCanvasY()-layer.getY()); PaintSettings ps = e.getPaintSettings(); for (int j = layer.size()-1; j >= 0; j--) { DrawObject obj = layer.get(j); if (all || obj.contains(lp)) { if (!obj.isLocked()) { if (e.isAltDown()) { if (e.isShiftDown()) { obj.setPaintSettings(obj.getPaintSettings() .deriveFillPaint(ps.fillPaint) .deriveFillComposite(ps.fillComposite) .deriveDrawPaint(ps.drawPaint) .deriveDrawComposite(ps.drawComposite) .deriveDrawStroke(ps.drawStroke)); } else { obj.setPaintSettings(obj.getPaintSettings() .deriveDrawPaint(ps.drawPaint) .deriveDrawComposite(ps.drawComposite) .deriveDrawStroke(ps.drawStroke)); } } else { obj.setPaintSettings(obj.getPaintSettings() .deriveFillPaint(ps.fillPaint) .deriveFillComposite(ps.fillComposite)); } } if (!all) { e.commitTransaction(); return true; } } } e.commitTransaction(); return true; } else { e.beginTransaction(getName()); Rectangle bounds = new Rectangle(-layer.getX(), -layer.getY(), e.getCanvas().getWidth(), e.getCanvas().getHeight()); int x = (int)Math.floor(e.getCanvasX()-layer.getX()); int y = (int)Math.floor(e.getCanvasY()-layer.getY()); Point seedPoint = new Point(x, y); Graphics2D g2 = layer.getCachedPaintGraphics(); e.getPaintSettings().applyFill(g2); if (all) { allFill(bounds, seedPoint, layer, g2); } else { seedFill(bounds, seedPoint, layer, g2); } e.commitTransaction(); return true; } } public Cursor getCursor(ToolEvent e) { return CursorUtils.CURSOR_BUCKET; } private static boolean colorCmp(int c, int targetcolor) { // in the future this could be refactored to support "tolerance" return c == targetcolor; } private static void allFill(Rectangle bounds, Point seedPoint, PaintSurface p, Graphics2D g2) { int targetcolor = p.getRGB(seedPoint.x, seedPoint.y); int[] rgb = new int[bounds.width*bounds.height]; p.getRGB(bounds.x, bounds.y, bounds.width, bounds.height, rgb, 0, bounds.width); int[] prgb = new int[bounds.width*bounds.height]; int cbx1 = bounds.width, cby1 = bounds.height, cbx2 = -1, cby2 = -1; for (int ay = 0, by = 0; ay < rgb.length; ay += bounds.width, by++) { for (int ax = 0, bx = 0; ax < bounds.width; ax++, bx++) { if (colorCmp(rgb[ay+ax], targetcolor)) { prgb[ay+ax] = 0xFF000000; if (bx < cbx1) cbx1 = bx; if (by < cby1) cby1 = by; if (bx > cbx2) cbx2 = bx; if (by > cby2) cby2 = by; } } } if (cbx2 >= cbx1 && cby2 >= cby1) { int cw = cbx2-cbx1+1; int ch = cby2-cby1+1; int[] nrgb = new int[cw*ch]; for ( int ny = 0, py = cby1*bounds.width; ny < nrgb.length && py < prgb.length; ny += cw, py += bounds.width ) { for ( int nx = 0, px = cbx1; nx < cw && px < bounds.width; nx++, px++ ) { nrgb[ny+nx] = prgb[py+px]; } } new Bitmap(cw, ch, nrgb).paint(g2, bounds.x+cbx1, bounds.y+cby1); } } private static void seedFill(Rectangle bounds, Point node, PaintSurface p, Graphics2D g2) { int xmin = bounds.x, xmax = bounds.x+bounds.width-1, ymin = bounds.y, ymax = bounds.y+bounds.height-1; int bx = bounds.x, by = bounds.y, rc = bounds.width; int targetcolor = p.getRGB(node.x, node.y); int[] rgb = new int[bounds.width*bounds.height]; p.getRGB(bounds.x, bounds.y, bounds.width, bounds.height, rgb, 0, bounds.width); int[] prgb = new int[bounds.width*bounds.height]; int cbx1 = bounds.width, cby1 = bounds.height, cbx2 = -1, cby2 = -1; List<Long> Q = new LinkedList<Long>(); Set<Long> V = new LongBitSet(); Q.add(((node.x & 0xFFFFFFFFL) << 32L) | (node.y & 0xFFFFFFFFL)); while (!Q.isEmpty()) { long n = Q.remove(0); if (!V.contains(n)) { int nx = (int)(n >> 32L); int ny = (int)(n); if (colorCmp(rgb[(ny-by)*rc + (nx-bx)], targetcolor)) { int w = nx; int e = nx; int y = ny; while (w > xmin && colorCmp(rgb[(y-by)*rc + ((w-1)-bx)], targetcolor)) w--; while (e < xmax && colorCmp(rgb[(y-by)*rc + ((e+1)-bx)], targetcolor)) e++; for (int x = w; x <= e; x++) { prgb[(y-by)*rc + (x-bx)] = 0xFF000000; if (x-bx < cbx1) cbx1 = x-bx; if (y-by < cby1) cby1 = y-by; if (x-bx > cbx2) cbx2 = x-bx; if (y-by > cby2) cby2 = y-by; V.add(((x & 0xFFFFFFFFL) << 32L) | (y & 0xFFFFFFFFL)); } if (y > ymin) { for (int x = w; x <= e; x++) { if (colorCmp(rgb[((y-1)-by)*rc + (x-bx)], targetcolor)) { long q = ((x & 0xFFFFFFFFL) << 32L) | ((y-1) & 0xFFFFFFFFL); if (!V.contains(q)) Q.add(q); } } } if (y < ymax) { for (int x = w; x <= e; x++) { if (colorCmp(rgb[((y+1)-by)*rc + (x-bx)], targetcolor)) { long q = ((x & 0xFFFFFFFFL) << 32L) | ((y+1) & 0xFFFFFFFFL); if (!V.contains(q)) Q.add(q); } } } } } } if (cbx2 >= cbx1 && cby2 >= cby1) { int cw = cbx2-cbx1+1; int ch = cby2-cby1+1; int[] nrgb = new int[cw*ch]; for ( int ny = 0, py = cby1*bounds.width; ny < nrgb.length && py < prgb.length; ny += cw, py += bounds.width ) { for ( int nx = 0, px = cbx1; nx < cw && px < bounds.width; nx++, px++ ) { nrgb[ny+nx] = prgb[py+px]; } } new Bitmap(cw, ch, nrgb).paint(g2, bounds.x+cbx1, bounds.y+cby1); } } private static class LongBitSet implements Set<Long> { private Map<Long,BitSet> m = new HashMap<Long,BitSet>(); public boolean add(Long o) { if (m.containsKey(o >> 24)) { m.get(o >> 24).set((int)(o & 0xFFFFFF)); } else { BitSet b = new BitSet(); b.set((int)(o & 0xFFFFFF)); m.put(o >> 24, b); } return true; } public boolean addAll(Collection<? extends Long> c) { boolean ret = false; for (long l : c) { if (add(l)) ret = true; } return ret; } public void clear() { m.clear(); } public boolean contains(Object o) { if (m.containsKey((Long)o >> 24)) { return m.get((Long)o >> 24).get((int)((Long)o & 0xFFFFFF)); } else { return false; } } public boolean containsAll(Collection<?> c) { for (Object o : c) { if (!contains(o)) return false; } return true; } public boolean isEmpty() { for (BitSet b : m.values()) { if (!b.isEmpty()) return false; } return true; } public Iterator<Long> iterator() { throw new UnsupportedOperationException("LongBitSet.iterator"); } public boolean remove(Object o) { if (m.containsKey((Long)o >> 24)) { m.get((Long)o >> 24).clear((int)((Long)o & 0xFFFFFF)); return true; } else { return false; } } public boolean removeAll(Collection<?> c) { boolean ret = false; for (Object o : c) { if (remove(o)) ret = true; } return ret; } public boolean retainAll(Collection<?> c) { throw new UnsupportedOperationException("LongBitSet.retainAll"); } public int size() { int size = 0; for (BitSet b : m.values()) { size += b.cardinality(); } return size; } public Object[] toArray() { throw new UnsupportedOperationException("LongBitSet.toArray"); } public <T> T[] toArray(T[] a) { throw new UnsupportedOperationException("LongBitSet.toArray"); } } }