All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.pepsoft.worldpainter.painting.DimensionPainter Maven / Gradle / Ivy

There is a newer version: 2.23.2
Show newest version
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package org.pepsoft.worldpainter.painting;

import org.pepsoft.worldpainter.Dimension;
import org.pepsoft.worldpainter.Terrain;
import org.pepsoft.worldpainter.layers.Layer;

import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

import static java.awt.RenderingHints.KEY_TEXT_ANTIALIASING;
import static java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_OFF;
import static org.pepsoft.worldpainter.Constants.TILE_SIZE_BITS;

/**
 * A utility class for painting basic shapes to dimension using any kind of {@link Paint}.
 *
 * 

Note: does not do any event inhibition management. The client should do that by * surrounding use of this class with invocations of {@link Dimension#setEventsInhibited(boolean)}: * *

dimension.setEventsInhibited(true);
 *try {
 *    painter.setDimension(dimension);
 *    // One or more calls to painting methods...
 *} finally {
 *    dimension.setEventsInhibited(false); // Will fire all necessary events
 *}
* * @author SchmitzP */ public final class DimensionPainter { /** * Paint a single impression of the current brush using the current paint. * * @param x The x coordinate at which to paint the impression. * @param y The y coordinate at which to paint the impression. */ public void drawPoint(Dimension dimension, int x, int y) { if (undo) { paint.remove(dimension, x, y, 1.0f); } else { paint.apply(dimension, x, y, 1.0f); } } /** * Paint a single impression of the current brush, attenuated by a dynamic level, using the current paint. * * @param x The x coordinate at which to paint the impression. * @param y The y coordinate at which to paint the impression. * @param dynamicLevel The dynamic level between {@code 0.0f} and {@code 1.0f} (inclusive) with which to * multiply the brush. */ public void drawPoint(Dimension dimension, int x, int y, float dynamicLevel) { if (undo) { paint.remove(dimension, x, y, dynamicLevel); } else { paint.apply(dimension, x, y, dynamicLevel); } } /** * Draw a straight line between two points using the current brush and paint, by applying the brush at each point * along the line. * * @param x1 The x coordinate at which to start the line. * @param y1 The y coordinate at which to start the line. * @param x2 The x coordinate at which to end the line. * @param y2 The y coordinate at which to end the line. */ public void drawLine(Dimension dimension, int x1, int y1, int x2, int y2) { final int dx = Math.abs(x2 - x1); final int dy = Math.abs(y2 - y1); if (dx < dy) { // Mostly vertical; go from top to bottom // Normalise the endpoints if (y2 < y1) { int tmp = y1; y1 = y2; y2 = tmp; tmp = x1; x1 = x2; x2 = tmp; } float x = x1 - 0.5f; final float fDx = (float) (x2 - x1) / dy; for (int y = y1; y <= y2; y++) { drawPoint(dimension, Math.round(x), y); x += fDx; } } else { // Mostly horizontal; go from left to right // Normalise the endpoints if (x2 < x1) { int tmp = y1; y1 = y2; y2 = tmp; tmp = x1; x1 = x2; x2 = tmp; } float y = y1 - 0.5f; final float fDy = (float) (y2 - y1) / dx; for (int x = x1; x <= x2; x++) { drawPoint(dimension, x, Math.round(y)); y += fDy; } } } /** * Draw a straight line between two points using the current brush, attenuated by a dynamic level, and paint, by * applying the brush at each point along the line. * * @param x1 The x coordinate at which to start the line. * @param y1 The y coordinate at which to start the line. * @param x2 The x coordinate at which to end the line. * @param y2 The y coordinate at which to end the line. * @param dynamicLevel The dynamic level between {@code 0.0f} and {@code 1.0f} (inclusive) with which to * multiply the brush. */ public void drawLine(Dimension dimension, int x1, int y1, int x2, int y2, float dynamicLevel) { final int dx = Math.abs(x2 - x1); final int dy = Math.abs(y2 - y1); if (dx < dy) { // Mostly vertical; go from top to bottom // Normalise the endpoints if (y2 < y1) { int tmp = y1; y1 = y2; y2 = tmp; tmp = x1; x1 = x2; x2 = tmp; } float x = x1 - 0.5f; final float fDx = (float) (x2 - x1) / dy; for (int y = y1; y <= y2; y++) { drawPoint(dimension, Math.round(x), y, dynamicLevel); x += fDx; } } else { // Mostly horizontal; go from left to right // Normalise the endpoints if (x2 < x1) { int tmp = y1; y1 = y2; y2 = tmp; tmp = x1; x1 = x2; x2 = tmp; } float y = y1 - 0.5f; final float fDy = (float) (y2 - y1) / dx; for (int x = x1; x <= x2; x++) { drawPoint(dimension, x, Math.round(y), dynamicLevel); y += fDy; } } } /** * Draw one or more lines of text using the current paint, font and rotation. The current brush is ignored. The text * may be rotated at 90 degree angles using {@link #setTextAngle(int)}. * * @param x The x coordinate of the start/top corner of the block of text. * @param y The y coordinate of the start/top corner of the block of text. * @param text The text to paint. May have multiple lines separated by line feed ('\n') characters. */ @SuppressWarnings("SuspiciousNameCombination") public void drawText(Dimension dimension, int x, int y, String text) { final String[] lines = text.split("\\n"); for (String line: lines) { final int lineHeight = drawTextLine(dimension, x, y, line); switch (textAngle) { case 0: y += lineHeight; break; case 1: x += lineHeight; break; case 2: y -= lineHeight; break; case 3: x -= lineHeight; break; } } } /** * Flood fill an area of the dimension with the current paint. The area to be flooded is the area where the type of * paint has the same value as where the fill is started. If the value is already the same as the configured paint, * nothing happens. If the operation takes more than two seconds a modal dialog is shown to the user with an * indeterminate progress bar. * *

The total filled area cannot exceed a square around the given coordinates with a surface area of * {@link Integer#MAX_VALUE}, due to Java limitations. If the boundary of this area has been hit, the method will * return {@code false} and the entire matching area may not have been filled. * * @param x The X coordinate to start the flood fill. * @param y The Y coordinate to start the flood fill. * @param parent The window to use as parent for the modal dialog shown if the operation takes more than two * seconds. * @return {@code true} if the fill operation was completed, or {@code false} if the filled area touched the * maximum bounds and the operation may not have filled the entire matching area. */ public boolean fill(Dimension dimension, final int x, final int y, Window parent) { AbstractDimensionPaintFillMethod fillMethod; if (paint instanceof LayerPaint) { final Layer layer = ((LayerPaint) paint).getLayer(); switch (layer.getDataSize()) { case BIT: case BIT_PER_CHUNK: if (undo) { fillMethod = new UndoDimensionPaintFillMethod("Removing " + layer, dimension, paint) { @Override public boolean isBoundary(int x, int y) { return ! dimension.getBitLayerValueAt(layer, x, y); } }; } else { fillMethod = new DimensionPaintFillMethod("Applying " + layer, dimension, paint) { @Override public boolean isBoundary(int x, int y) { return dimension.getBitLayerValueAt(layer, x, y); } }; } break; case NIBBLE: case BYTE: if (paint instanceof DiscreteLayerPaint) { final int fillValue = dimension.getLayerValueAt(layer, x, y); if (undo) { fillMethod = new UndoDimensionPaintFillMethod("Removing " + layer, dimension, paint) { @Override public boolean isBoundary(int x, int y) { return dimension.getLayerValueAt(layer, x, y) != fillValue; } @Override boolean isFilled(int x, int y) { return dimension.getLayerValueAt(layer, x, y) == layer.getDefaultValue(); } }; } else { fillMethod = new DimensionPaintFillMethod("Applying " + layer, dimension, paint) { @Override public boolean isBoundary(int x, int y) { return dimension.getLayerValueAt(layer, x, y) != fillValue; } @Override boolean isFilled(int x, int y) { return dimension.getLayerValueAt(layer, x, y) == ((DiscreteLayerPaint) paint).getValue(); } }; } } else { if (undo) { fillMethod = new UndoDimensionPaintFillMethod("Removing " + layer, dimension, paint) { @Override public boolean isBoundary(int x, int y) { return dimension.getLayerValueAt(layer, x, y) == 0; } }; } else { fillMethod = new DimensionPaintFillMethod("Applying " + layer, dimension, paint) { @Override public boolean isBoundary(int x, int y) { return dimension.getLayerValueAt(layer, x, y) >= targetValue; } final int targetValue = 1 + Math.round((layer.getDataSize() == Layer.DataSize.NIBBLE) ? (paint.getBrush().getLevel() * 14) : (paint.getBrush().getLevel() * 254)); }; } } break; default: throw new IllegalArgumentException("Don't know how to fill with layer with data size " + layer.getDataSize()); } } else if (paint instanceof TerrainPaint) { final Terrain terrainToFill = dimension.getTerrainAt(x, y); if (undo) { fillMethod = new UndoDimensionPaintFillMethod("Removing " + terrainToFill, dimension, paint) { @Override public boolean isBoundary(int x, int y) { return dimension.getTerrainAt(x, y) != terrainToFill; } @Override boolean isFilled(int x, int y) { return dimension.getTerrainAt(x, y) == ((TerrainPaint) paint).getTerrain(); } }; } else { fillMethod = new DimensionPaintFillMethod("Applying " + ((TerrainPaint) paint).getTerrain(), dimension, paint) { @Override public boolean isBoundary(int x, int y) { return dimension.getTerrainAt(x, y) != terrainToFill; } @Override boolean isFilled(int x, int y) { return dimension.getTerrainAt(x, y) == ((TerrainPaint) paint).getTerrain(); } }; } } else if (paint instanceof PaintFactory.NullPaint) { return true; } else { throw new IllegalArgumentException("Don't know how to fill with paint " + paint); } if (! fillMethod.isFilled(x, y)) { GeneralQueueLinearFloodFiller filler = new GeneralQueueLinearFloodFiller(fillMethod); filler.floodFill(x, y, parent); return ! filler.isBoundsHit(); } else { return true; } } /** * Get the font with which text is painted. * * @return The font with which text is painted. */ public Font getFont() { return font; } /** * Set the font with which text is painted. * * @param font The font with which text is painted. */ public void setFont(Font font) { this.font = font; } /** * Get the angle at which text is painted. * * @return The angle at which text is painted in terms of one of the constants {@link #ANGLE_0_DEGREES}, * {@link #ANGLE_90_DEGREES}, {@link #ANGLE_180_DEGREES} or {@link #ANGLE_270_DEGREES}. */ public int getTextAngle() { return textAngle; } /** * Set the angle at which text is painted. * * @param textAngle The angle at which text is painted in terms of one of the constants {@link #ANGLE_0_DEGREES}, * {@link #ANGLE_90_DEGREES}, {@link #ANGLE_180_DEGREES} or {@link #ANGLE_270_DEGREES}. */ public void setTextAngle(int textAngle) { if ((textAngle < ANGLE_0_DEGREES) || (textAngle > ANGLE_270_DEGREES)) { throw new IllegalArgumentException(); } this.textAngle = textAngle; } /** * Get the paint which the operations apply to the dimension. * * @return The paint which the operations apply to the dimension. */ public Paint getPaint() { return paint; } /** * Set the paint which the operations apply to the dimension. * * @param paint The paint which the operations apply to the dimension. */ public void setPaint(Paint paint) { this.paint = paint; } /** * Configure whether the paint is applied or removed by the operations. What constitutes * "application" or "removal" is defined by the paint. * * @param undo Whether the paint is applied (when {@code false}) or removed (when * {@code true}) by the operations. */ public void setUndo(boolean undo) { this.undo = undo; } /** * Determine whether the paint is applied or removed by the operations. What constitutes * "application" or "removal" is defined by the paint. * * @return Whether the paint is applied (when {@code false}) or removed (when * {@code true}) by the operations. */ public boolean isUndo() { return undo; } private int drawTextLine(Dimension dimension, int x, int y, String text) { BufferedImage image = new BufferedImage(1000, 100, BufferedImage.TYPE_BYTE_BINARY); final Rectangle2D bounds; Graphics2D g2 = image.createGraphics(); g2.setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_OFF); final int textWidth, textHeight; try { g2.setFont(font); final FontRenderContext frc = g2.getFontRenderContext(); bounds = font.getStringBounds(text, frc); textWidth = (int) Math.ceil(bounds.getWidth()); textHeight = (int) Math.ceil(bounds.getHeight()); if ((textWidth < 1) || (textHeight < 1)) { return (int) bounds.getHeight(); } if ((textWidth > 1000) || (textHeight > 100)) { g2.dispose(); image = new BufferedImage(textWidth, textHeight, BufferedImage.TYPE_BYTE_BINARY); g2 = image.createGraphics(); g2.setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_OFF); g2.setFont(font); } g2.drawString(text, (int) (-bounds.getX()), (int) (-bounds.getY())); } finally { g2.dispose(); } if (undo) { for (int xx = 0; xx < textWidth; xx++) { for (int yy = 0; yy < textHeight; yy++) { if ((image.getRGB(xx, yy) & 1) != 0) { switch (textAngle) { case ANGLE_0_DEGREES: paint.removePixel(dimension, x + xx, y + yy); break; case ANGLE_90_DEGREES: paint.removePixel(dimension, x + yy, y - xx); break; case ANGLE_180_DEGREES: paint.removePixel(dimension, x - xx, y - yy); break; case ANGLE_270_DEGREES: paint.removePixel(dimension, x - yy, y + xx); break; } } } } } else { for (int xx = 0; xx < textWidth; xx++) { for (int yy = 0; yy < textHeight; yy++) { if ((image.getRGB(xx, yy) & 1) != 0) { switch (textAngle) { case ANGLE_0_DEGREES: paint.applyPixel(dimension, x + xx, y + yy); break; case ANGLE_90_DEGREES: paint.applyPixel(dimension, x + yy, y - xx); break; case ANGLE_180_DEGREES: paint.applyPixel(dimension, x - xx, y - yy); break; case ANGLE_270_DEGREES: paint.applyPixel(dimension, x - yy, y + xx); break; } } } } } return (int) bounds.getHeight(); } private Paint paint; private int textAngle; private boolean undo; private Font font; public static final int ANGLE_0_DEGREES = 0; public static final int ANGLE_90_DEGREES = 1; public static final int ANGLE_180_DEGREES = 2; public static final int ANGLE_270_DEGREES = 3; static abstract class AbstractDimensionPaintFillMethod implements GeneralQueueLinearFloodFiller.FillMethod { protected AbstractDimensionPaintFillMethod(String description, Dimension dimension, Paint paint) { this.description = description; this.dimension = dimension; this.paint = paint; bounds = new Rectangle(dimension.getLowestX() << TILE_SIZE_BITS, dimension.getLowestY() << TILE_SIZE_BITS, dimension.getWidth() << TILE_SIZE_BITS, dimension.getHeight() << TILE_SIZE_BITS); } @Override public final Rectangle getBounds() { return bounds; } @Override public final String getDescription() { return description; } boolean isFilled(int x, int y) { return isBoundary(x, y); } private final String description; private final Rectangle bounds; protected final Dimension dimension; protected final Paint paint; } static abstract class DimensionPaintFillMethod extends AbstractDimensionPaintFillMethod { DimensionPaintFillMethod(String description, Dimension dimension, Paint paint) { super(description, dimension, paint); } @Override public final void fill(int x, int y) { paint.applyPixel(dimension, x, y); } } static abstract class UndoDimensionPaintFillMethod extends AbstractDimensionPaintFillMethod { UndoDimensionPaintFillMethod(String description, Dimension dimension, Paint paint) { super(description, dimension, paint); } @Override public final void fill(int x, int y) { paint.removePixel(dimension, x, y); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy