es.eucm.ead.editor.view.builders.scene.draw.MeshHelper.java Source code

Java tutorial

Introduction

Here is the source code for es.eucm.ead.editor.view.builders.scene.draw.MeshHelper.java

Source

/**
 * eAdventure is a research project of the
 *    e-UCM research group.
 *
 *    Copyright 2005-2014 e-UCM research group.
 *
 *    You can access a list of all the contributors to eAdventure at:
 *          http://e-adventure.e-ucm.es/contributors
 *
 *    e-UCM is a research group of the Department of Software Engineering
 *          and Artificial Intelligence at the Complutense University of Madrid
 *          (School of Computer Science).
 *
 *          CL Profesor Jose Garcia Santesmases 9,
 *          28040 Madrid (Madrid), Spain.
 *
 *          For more info please visit:  <http://e-adventure.e-ucm.es> or
 *          <http://www.e-ucm.es>
 *
 * ****************************************************************************
 *
 *  This file is part of eAdventure
 *
 *      eAdventure is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU Lesser General Public License as published by
 *      the Free Software Foundation, either version 3 of the License, or
 *      (at your option) any later version.
 *
 *      eAdventure is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU Lesser General Public License for more details.
 *
 *      You should have received a copy of the GNU Lesser General Public License
 *      along with eAdventure.  If not, see <http://www.gnu.org/licenses/>.
 */
package es.eucm.ead.editor.view.builders.scene.draw;

import java.io.IOException;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Mesh;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Blending;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.PixmapIO.PNG;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.VertexAttribute;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ScreenUtils;

import es.eucm.ead.editor.control.Actions;
import es.eucm.ead.editor.control.Controller;
import es.eucm.ead.editor.control.actions.editor.Redo;
import es.eucm.ead.editor.control.actions.editor.Undo;
import es.eucm.ead.editor.control.commands.Command;
import es.eucm.ead.editor.model.events.ModelEvent;
import es.eucm.ead.editor.view.listeners.ActionListener;
import es.eucm.ead.engine.systems.effects.transitions.Region;

/**
 * Handles all the necessary data required to draw brush strokes, undo/redo and
 * delete them.
 */
public class MeshHelper {

    /**
     * The maximum number of triangles. Increasing this value increases the
     * length of the line avoiding the user to touchUp and start a new input
     * process. But this will increase the memory usage and decrease the
     * performance if the number of triangles is too high.
     */
    private static final int MAX_TRIANGLES = 1500;
    /**
     * Used to decide when to draw a dot or to start drawing the brush stroke.
     */
    private static final int MIN_VERTICES = 0;
    /**
     * Defines the quality of the dot. The current amount is calculated via
     * MAX_DOT_TRIANGLES * currentRadius / maxRadius.
     */
    private static final int MAX_DOT_TRIANGLES = 28;
    /**
     * Auxiliary constants cached in order to avoid per-frame calculation.
     */
    private static final int MAX_VERTICES = MAX_TRIANGLES * 2;

    /**
     * The lower this value is the higher is the accuracy of the brush stroke
     * but the length of the line will also decrease requiring the user to
     * touchUp and start a new input process.
     */
    private static final float DASH_ACCURACY = 50f;

    /**
     * Used to convert from local to {@link Stage} coordinates and vice versa.
     */
    private final Vector2 unprojectedVertex = new Vector2();
    /**
     * Performs the undo/redo encapsulation while drawing.
     */
    private final DrawLineCommand drawLine = new DrawLineCommand();
    /**
     * Used to convert from LocalToStageCoordinates and vice versa.
     */
    private final Vector2 temp = new Vector2();
    /**
     * This defines the local coordinates, and the bounds to clamp via
     * {@link #clampTotalBounds()}
     */
    private final Actor scaledView;
    /**
     * Used to clear the {@link #frameBuffer} contents, also used while
     * undo/redo is performed.
     */
    private final PixmapRegion flusher = new PixmapRegion(null, 0, 0);
    /**
     * {@link Camera#combined} matrix passed to the {@link #meshShader}.
     */
    private final Matrix4 combinedMatrix = new Matrix4();
    private final float[] lineVertices;
    /**
     * Used to correctly perform the commands.
     */
    private final Controller controller;

    /**
     * The number of vertices from {@link #lineVertices} passed to the
     * {@link #mesh}.
     */
    private int vertexIndex = 0;
    /**
     * Used to decide if we should render via {@link GL20#GL_TRIANGLE_STRIP} for
     * a brush stroke or via {@link GL20#GL_TRIANGLE_FAN} for a dot (if the user
     * doesn't drag enough to render a line).
     */
    private int primitiveType;
    private float drawRadius = 20f, maxDrawRadius = drawRadius * 2f;
    /**
     * Used to define the {@link Color} of the brush stroke.
     */
    private float r = 1f, g = 1f, b = 0f, a = 1f;
    /**
     * Used to decide the boundaries of the final saved image.
     */
    private float minX, minY, maxX, maxY;
    /**
     * Those values define the bounds of {@link #minX}, {@link #minY},
     * {@link #maxX} and {@link #maxY}. Used in {@link #clampTotalBounds()}.
     */
    private float clampMinX, clampMinY, clampMaxX, clampMaxY;
    /**
     * Last input position, used to calculate distance-based optimizations.
     */
    private float lastX, lastY;
    /**
     * If true, the {@link #combinedMatrix} will be recalculated next time is
     * needed.
     */
    private boolean recalculateMatrix;

    /**
     * Used to know which {@link PixmapRegion} is the most recent. Useful to
     * save and perform undo/redo commands.
     */
    private PixmapRegion currModifiedPixmap;
    /**
     * The {@link TextureRegion} that holds the {@link Texture} to whom the
     * {@link #mesh} is being drawn, with the help of the {@link #frameBuffer}.
     * This texture has {@link Stage} coordinates but is drawn with the local
     * {@link Matrix4} (by the local {@link SpriteBatch}). To achieve this the
     * {@link TextureRegion} must know what region of the {@link Texture} has to
     * draw, this is done via
     * {@link TextureRegion#setRegion(float, float, float, float)} and with what
     * scale ({@link #scaleX}, {@link #scaleY}). Must keep in mind that
     * {@link Pixmap}s are drawn with OpenGL ES coordinates (y-down) so
     * {@link #showingTexRegion} is also flipped vertically via
     * {@link TextureRegion#flip(boolean, boolean)}.
     */
    private TextureRegion showingTexRegion;
    /**
     * Used while erasing.
     */
    private FrameBuffer fbo;
    private boolean erasing;
    private int vertexLimit;

    private boolean firstSemiCircle = false;
    private float lastAngle;

    private ShaderProgram meshShader;
    private Mesh mesh;
    private Region savedRegion = new Region(-1, 0, 0, 0);

    /**
     * Handles all the necessary data required to draw brush strokes, undo/redo
     * and delete them. The {@link #scaledView} parameter will be used to
     * perform local to stage coordinates conversion in order to recalculate the
     * new required data/positions. This class assumes that
     * {@link Pixmap#setBlending(Blending.None)} is activated in order to
     * function correctly.
     * 
     * @param controller
     *            Used to correctly perform the commands.
     */
    public MeshHelper(Actor scaledView, Controller controller) {
        this.primitiveType = GL20.GL_TRIANGLE_STRIP;
        this.lineVertices = new float[MAX_VERTICES];
        this.controller = controller;
        this.scaledView = scaledView;
        resetTotalBounds();

        ActionListener erasePixmaps = new ActionListener() {
            @Override
            public void enableChanged(Class actionClass, boolean enable) {
                if (!enable) {
                    if (actionClass == Undo.class) {
                        release(drawLine.undoPixmaps);
                    } else {
                        release(drawLine.redoPixmaps);
                    }
                }
            }
        };
        Actions actions = controller.getActions();
        actions.addActionListener(Undo.class, erasePixmaps);
        actions.addActionListener(Redo.class, erasePixmaps);
    }

    /**
     * @return true if the {@link #currModifiedPixmap} has been modified and has
     *         something to save.
     */
    boolean hasSomethingToSave() {
        if (this.currModifiedPixmap != null && this.currModifiedPixmap.pixmap != null
                && this.currModifiedPixmap.pixmap != this.flusher.pixmap && minX != Float.MAX_VALUE) {
            updateSavingRegion();
            return savedRegion.x != -1f;
        }
        return false;
    }

    /**
     * Checks if there are pixels to be saved and stores the region defined by
     * those pixels in a {@link Region}. If the modified pixmap is empty or
     * completely transparent, the {@link Region}'s x value will be set to -1.
     */
    private void updateSavingRegion() {
        Pixmap savedPixmap = this.currModifiedPixmap.pixmap;
        int x, y, xMax, yMax;
        x = y = Integer.MAX_VALUE;
        xMax = yMax = Integer.MIN_VALUE;
        int xPixels = savedPixmap.getWidth();
        int yPixels = savedPixmap.getHeight();
        for (int i = 0; i < xPixels; ++i) {
            for (int j = 0; j < yPixels; ++j) {
                int value = savedPixmap.getPixel(i, j);
                float a = ((value & 0x000000ff)) / 255f;
                if (a > 0) {
                    x = Math.min(x, i);
                    y = Math.min(y, j);
                    xMax = Math.max(xMax, i);
                    yMax = Math.max(yMax, j);
                }
            }
        }
        if (x < xMax && y < yMax) {
            savedRegion.x = x;
            savedRegion.y = y;
            savedRegion.w = xMax - x;
            savedRegion.h = yMax - y;
        } else {
            savedRegion.x = -1;
        }
    }

    /**
     * Disposes the contents of the {@link Array} and clears the {@link Array}.
     * 
     * @param pixmaps
     */
    private void release(Array<PixmapRegion> pixmaps) {
        if (pixmaps.size == 0)
            return;
        for (PixmapRegion pixmap : pixmaps) {
            pixmap.dispose();
            pixmap = null;
        }
        pixmaps.clear();
    }

    /**
     * Saves the minimum amount of pixels that encapsulates the drawn image.
     * 
     * @return
     */
    Region save(FileHandle file) {
        Pixmap modifPixmap = this.currModifiedPixmap.pixmap;
        PNG writer = null;
        Pixmap savedPixmap = null;
        try {
            savedPixmap = new Pixmap(savedRegion.w, savedRegion.h, modifPixmap.getFormat());

            savedPixmap.drawPixmap(modifPixmap, 0, 0, savedRegion.x, savedRegion.y, savedRegion.w, savedRegion.h);

            savedRegion.x += currModifiedPixmap.x;
            savedRegion.y += currModifiedPixmap.y;
            // Guess at deflated size.
            writer = new PNG((int) (savedPixmap.getWidth() * savedPixmap.getHeight() * 1.5f));
            writer.setFlipY(true);
            writer.write(file, savedPixmap);
        } catch (IOException ex) {
            throw new GdxRuntimeException("Error writing PNG: " + file, ex);
        } finally {
            if (writer != null) {
                writer.dispose();
                writer = null;
            }
            if (savedPixmap != null) {
                savedPixmap.dispose();
                savedPixmap = null;
            }
        }

        return savedRegion;
    }

    /**
     * Initializes the {@link #frameBuffer}, {@link #showingTexRegion} and
     * {@link #flusher} to the coordinates the the {@link Stage}, only if they
     * are null. This method should only be called in {@link #layout()}. If the
     * {@link Stage} size changed, the resources are translated via
     * {@link #translateResources(Actor)}.
     */
    public void initializeRenderingResources() {
        createShader();
        createMesh();

        this.recalculateMatrix = true;

        if (this.showingTexRegion == null) {
            this.showingTexRegion = new TextureRegion();
        }

        float w = scaledView.getWidth(), h = scaledView.getHeight();

        if (fbo == null) {
            fbo = new FrameBuffer(Format.RGBA8888, Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), false);
        }
        showingTexRegion.setRegion(fbo.getColorBufferTexture());

        clampMinX = 0f;
        clampMinY = 0f;

        clampMaxX = w;
        clampMaxY = h;

        scaledView.localToStageCoordinates(temp.set(0f, 0f));
        int stageX = MathUtils.round(temp.x), stageY = MathUtils.round(temp.y), width = MathUtils.round(w),
                height = MathUtils.round(h);
        showingTexRegion.setRegion(stageX, stageY, width, height);
        showingTexRegion.flip(false, true);
        if (this.flusher.pixmap == null) {
            this.flusher.pixmap = new Pixmap(width, height, Format.RGBA8888);
            flusher.x = stageX;
            flusher.y = stageY;
        }
    }

    /**
     * Creates the {@link #mesh} with the basic {@link VertexAttribute}s to
     * define a position.
     */
    private void createMesh() {
        this.mesh = new Mesh(true, MAX_VERTICES, 0, new VertexAttribute(Usage.Position, 2, "a_position"));
    }

    /**
     * Creates the {@link #meshShader} which will requite a {@link Matrix4} for
     * the vertex positioning and a {@link Color} for the fragments.
     */
    private void createShader() {
        // this shader tells OpenGL where to put things
        final String vertexShader = "attribute vec4 a_position; \n" + "uniform mat4 u_worldView;               \n"
                + "void main()                              \n" + "{                                        \n"
                + "   gl_Position =  u_worldView * a_position;   }";

        // this one tells it what goes in between the points (i.e
        // color/texture)
        final String fragmentShader = "#ifdef GL_ES     \n" + "precision mediump float;             \n"
                + "#endif                               \n" + "uniform vec4 u_color;            \n"
                + "void main()                          \n" + "{                                    \n"
                + "  gl_FragColor = u_color;            }";

        // make an actual shader from our strings
        ShaderProgram.pedantic = false;
        this.meshShader = new ShaderProgram(vertexShader, fragmentShader);

        // check there's no shader compile error
        if (!this.meshShader.isCompiled())
            throw new IllegalStateException(this.meshShader.getLog());
    }

    /**
     * Initializes the mesh with the pixels of the given group.
     * 
     * The result vectors must be in {@link #scaledView} coordinates.
     * 
     * @param toEdit
     * @param resultOrigin
     * @param resultSize
     */
    public void show(Group toEdit, Vector2 resultOrigin, Vector2 resultSize) {
        int x = MathUtils.round(resultOrigin.x), y = MathUtils.round(resultOrigin.y), width = (int) resultSize.x,
                height = (int) resultSize.y;
        minX = x;
        minY = y;
        maxX = minX + width;
        maxY = minY + height;

        scaledView.localToStageCoordinates(temp.set(x, y));
        int stageX = MathUtils.round(temp.x), stageY = MathUtils.round(temp.y);

        Batch batch = controller.getPlatform().getBatch();
        batch.setProjectionMatrix(combinedMatrix);
        fbo.begin();
        batch.begin();
        toEdit.draw(batch, 1f);
        batch.end();
        currModifiedPixmap = new PixmapRegion(ScreenUtils.getFrameBufferPixmap(stageX, stageY, width, height),
                stageX, stageY);
        fbo.end();
    }

    /**
     * Renders everything necessary to edit and draw brush strokes.
     * 
     * @param batch
     * @param parentAlpha
     */
    void draw(Batch batch, float parentAlpha) {
        if (!erasing) {
            drawShowingTexture(batch);
            if (this.recalculateMatrix) {
                this.recalculateMatrix = false;
                this.combinedMatrix.set(batch.getProjectionMatrix()).mul(batch.getTransformMatrix());
            }
            if (this.vertexIndex > MIN_VERTICES) {
                batch.end();
                drawMesh();
                batch.begin();
            }
        } else {
            if (this.vertexIndex > MIN_VERTICES && minX != Float.MAX_VALUE) {
                batch.end();
                fbo.begin();
                drawMesh();
                fbo.end();
                batch.begin();
            }
            drawShowingTexture(batch);
        }
    }

    /**
     * Draws the {@link #showingTexRegion}. Considering the
     * {@link #showingTexRegion} is created in {@link Stage} coordinate system,
     * the {@link Texture} is drawn scaled with a scale equal to 1 /
     * {@link #scaledView}.getScaleXY().
     * 
     * @param batch
     */
    private void drawShowingTexture(Batch batch) {
        batch.draw(this.showingTexRegion, 0, 0);
    }

    /**
     * Draws the {@link #mesh} with the {@link #meshShader}. The
     * {@link #meshShader} receives a {@link Color} specified via
     * {@link #setColor(Color)} and the {@link #combinedMatrix} from the
     * {@link Camera#combined} (ProjectionMatrix * TransformMatrix).
     */
    private void drawMesh() {

        this.meshShader.begin();

        this.meshShader.setUniformf("u_color", this.r, this.g, this.b, this.a);
        this.meshShader.setUniformMatrix("u_worldView", this.combinedMatrix);

        this.mesh.render(this.meshShader, this.primitiveType);

        this.meshShader.end();

    }

    private void dispose() {
        if (this.flusher.pixmap != null) {
            this.flusher.pixmap.dispose();
            this.flusher.pixmap = null;
        }
        if (fbo != null) {
            fbo.dispose();
            fbo = null;
        }
        this.mesh.dispose();
        this.mesh = null;
        this.meshShader.dispose();
        this.meshShader = null;
    }

    private final Runnable dispose = new Runnable() {

        @Override
        public void run() {
            dispose();
        }
    };

    /**
     * Starts adding vertices to the {@link #mesh}.
     * 
     * @param x
     * @param y
     */
    void touchDown(float x, float y) {
        vertexLimit = Integer.MAX_VALUE;
        drawHalfCircle(0, x, y);
        drawHalfCircle(180, x, y);

        this.lastX = x;
        this.lastY = y;
        this.mesh.setVertices(this.lineVertices, 0, this.vertexIndex);
        clampTotalBounds(x - drawRadius, y - drawRadius, x + drawRadius, y + drawRadius);
    }

    /**
     * This method transforms the input to the specified format of the
     * {@link #mesh} in order to render a brush stroke.
     * 
     * @param x
     * @param y
     */
    void touchDragged(float x, float y) {
        if (this.vertexIndex >= vertexLimit) {
            return;
        }

        if (this.unprojectedVertex.set(x, y).dst2(this.lastX, this.lastY) > DASH_ACCURACY) {

            if (!firstSemiCircle) {
                firstSemiCircle = true;
                this.vertexIndex = 0;

                this.unprojectedVertex.set(lastX, lastY).sub(x, y);
                float angle = unprojectedVertex.angle() - 90;
                drawHalfCircle(angle, lastX, lastY);
                prepareLine(angle, lastX, lastY);

                vertexIndex += ((1 + getHalfCircleTriangles()) * 4 + 2);
                lastAngle = angle + 180;

            }

            int circleTriangles = ((1 + getHalfCircleTriangles()) * 4 + 2);
            vertexIndex -= circleTriangles;

            vertexLimit = MAX_VERTICES - (circleTriangles + 4);

            this.unprojectedVertex.set(x, y).sub(this.lastX, this.lastY).nor();
            float currentAngle = unprojectedVertex.angle() - 90;

            if (Math.abs(currentAngle - lastAngle) > 45) {

                drawHalfCircle(lastAngle, lastX, lastY);
                prepareLine(lastAngle, lastX, lastY);
                x = lastX;
                y = lastY;
            }
            lastAngle = currentAngle;

            unprojectedVertex.set(-this.unprojectedVertex.y, this.unprojectedVertex.x).scl(this.drawRadius);

            float maxNorX = x + this.unprojectedVertex.x;
            this.lineVertices[this.vertexIndex++] = maxNorX;

            float maxNorY = y + this.unprojectedVertex.y;
            this.lineVertices[this.vertexIndex++] = maxNorY;

            float minNorX, minNorY;
            minNorX = x - this.unprojectedVertex.x;
            this.lineVertices[this.vertexIndex++] = minNorX;

            minNorY = y - this.unprojectedVertex.y;
            this.lineVertices[this.vertexIndex++] = minNorY;

            this.lineVertices[this.vertexIndex++] = x;
            this.lineVertices[this.vertexIndex++] = y;
            drawHalfCircle(currentAngle, x, y);

            clampTotalBounds(x - drawRadius, y - drawRadius, x + drawRadius, y + drawRadius);

            this.mesh.setVertices(this.lineVertices, 0, this.vertexIndex);

            this.lastX = x;
            this.lastY = y;
        }
    }

    /**
     * Finishes adding vertices to the {@link #mesh} and performs the #
     * {@link DrawLineCommand}.
     * 
     * @param x
     * @param y
     */
    void touchUp(float x, float y) {

        touchDragged(x, y);
        firstSemiCircle = false;
        if (minX != Float.MAX_VALUE) {
            this.controller.command(drawLine);
        } else {
            resetMesh();
        }
    }

    /**
     * Draws half circle with GL_TRIANGLE_STRIP vertices starting at a given
     * angle.
     * 
     * @param startingAngle
     * @param x
     * @param y
     */
    private void drawHalfCircle(float startingAngle, float x, float y) {
        final int triangleAmount = getHalfCircleTriangles();

        float startCount = triangleAmount * startingAngle / 180;

        float circleStep = MathUtils.PI / triangleAmount;

        for (int i = 0; i <= triangleAmount; ++i) {
            float deg = (i + startCount) * circleStep;
            lineVertices[vertexIndex++] = x + (drawRadius * MathUtils.cos(deg));
            lineVertices[vertexIndex++] = y + (drawRadius * MathUtils.sin(deg));
            lineVertices[vertexIndex++] = x;
            lineVertices[vertexIndex++] = y;
        }

    }

    /**
     * Prepares the vertices after drawing a half circle via
     * {@link #drawHalfCircle(float, float, float)}.
     */
    private void prepareLine(float angle, float x, float y) {

        float cosDeg = MathUtils.cosDeg(angle) * drawRadius;
        float maxNorX = x + cosDeg;
        this.lineVertices[this.vertexIndex++] = maxNorX;

        float sinDeg = MathUtils.sinDeg(angle) * drawRadius;
        float maxNorY = y + sinDeg;
        this.lineVertices[this.vertexIndex++] = maxNorY;

        float minNorX = x - cosDeg;
        this.lineVertices[this.vertexIndex++] = minNorX;

        float minNorY = y - sinDeg;
        this.lineVertices[this.vertexIndex++] = minNorY;
    }

    private int getHalfCircleTriangles() {
        return MathUtils.round(MAX_DOT_TRIANGLES * this.drawRadius / this.maxDrawRadius * .5f);
    }

    /**
     * Clears the {@link #mesh}. Nothing will be rendered by the mesh after the
     * call to this method unless new vertices are added to the {@link #mesh}
     * via {@link Mesh#setVertices(float[], int, int)}.
     */
    private void resetMesh() {
        this.vertexIndex = 0;
    }

    /**
     * Initializes {@link #minX}, {@link #minY} to {@link Float#MAX_VALUE} and
     * {@link #maxX}, {@link #maxY} to {@link Float#MIN_VALUE},
     */
    private void resetTotalBounds() {
        this.minX = this.minY = Float.MAX_VALUE;
        this.maxX = this.maxY = Float.MIN_VALUE;
    }

    /**
     * Clamps {@link #minX}, {@link #minY}, {@link #maxX} and {@link #maxY} to
     * the values defined by {@link #clampMinX}, {@link #clampMinY},
     * {@link #clampMaxX} and {@link #clampMaxY}.
     */
    private void clampTotalBounds() {
        if (this.minX < clampMinX) {
            this.minX = clampMinX;
        }

        if (this.maxX > clampMaxX) {
            this.maxX = clampMaxX;
        }

        if (this.minY < clampMinY) {
            this.minY = clampMinY;
        }

        if (this.maxY > clampMaxY) {
            this.maxY = clampMaxY;
        }
    }

    /**
     * Clamps {@link #minX}, {@link #minY}, {@link #maxX} and {@link #maxY} to
     * the values from the parameters. This method supposes the attributes are
     * given in the {@link Stage} coordinate system, so no conversion will be
     * performed.
     * 
     * @param minx
     * @param miny
     * @param maxx
     * @param maxy
     */
    private void clampTotalBounds(float minx, float miny, float maxx, float maxy) {
        if (!erasing) {
            this.minX = Math.min(this.minX, minx);
            this.minY = Math.min(this.minY, miny);
            this.maxX = Math.max(this.maxX, maxx);
            this.maxY = Math.max(this.maxY, maxy);
        }
    }

    /**
     * Updates {@link #minX}, {@link #minY}, {@link #maxX} and {@link #maxY} to
     * the coordinates of the {@link PixmapRegion pixRegion}. Since
     * {@link PixmapRegion pixRegion} uses {@link Stage} coordinate system which
     * is the same as the one used by {@link #minX}, {@link #minY},
     * {@link #maxX} and {@link #maxY} no conversion must be done before
     * updating the new values.
     */
    private void updateTotalBounds(PixmapRegion pixRegion) {
        final Pixmap pix = pixRegion.pixmap;

        if (pix != flusher.pixmap) {
            float x = pixRegion.x;
            float y = pixRegion.y;

            scaledView.stageToLocalCoordinates(temp.set(x, y));
            minX = temp.x;
            minY = temp.y;
            maxX = minX + pix.getWidth();
            maxY = minY + pix.getHeight();
        } else {
            resetTotalBounds();
        }
    }

    /**
     * Sets the {@link Color} of the brush.
     */
    void setColor(Color color) {
        this.r = color.r;
        this.g = color.g;
        this.b = color.b;
        this.a = color.a;
    }

    void setErasing(boolean erasing) {
        if (erasing) {
            a = 0f;
        } else {
            a = 1f;
        }
        this.erasing = erasing;
    }

    /**
     * Sets the width of the brush.
     */
    void setRadius(float radius) {
        this.drawRadius = radius;
    }

    /**
     * This value will be used to determine how many triangles will be processed
     * when drawing a dot via {@link GL20#GL_TRIANGLE_FAN}.
     */
    void setMaxDrawRadius(float maxRadius) {
        this.maxDrawRadius = maxRadius;
    }

    /**
     * Disposes all the {@link Array}s, {@link #currModifiedPixmap} and resets
     * {@link #minX}, {@link #minY}, {@link #maxX} and {@link #maxY} via
     * {@link #resetTotalBounds()}. Also disposed {@link #erasedPixmap} and
     * {@link #currModifiedPixmap} if they weren't already disposed.
     */
    void release() {
        resetTotalBounds();
        release(drawLine.redoPixmaps);
        release(drawLine.undoPixmaps);
        if (this.currModifiedPixmap != null) {
            this.currModifiedPixmap.dispose();
            this.currModifiedPixmap = null;
        }
        Gdx.app.postRunnable(dispose);
    }

    /**
     * Uses {@link MeshHelper} attributes in order to perform correctly brush
     * strokes.
     */
    private class DrawLineCommand extends Command {

        private final boolean debug = false;

        /**
         * Used by the {@link #drawLine} to perform correctly.
         */
        private final Array<PixmapRegion> undoPixmaps = new Array<PixmapRegion>(false, 15),
                redoPixmaps = new Array<PixmapRegion>(false, 15);

        private final ModelEvent dummyEvent = new ModelEvent() {
            @Override
            public Object getTarget() {
                return null;
            }
        };

        @Override
        public ModelEvent doCommand() {

            if (vertexIndex == 0 && redoPixmaps.size != 0) {

                undoPixmaps.add(currModifiedPixmap);
                currModifiedPixmap = null;

                final PixmapRegion oldPix = redoPixmaps.pop();
                debug(oldPix);
                showingTexRegion.getTexture().draw(oldPix.pixmap, oldPix.x, oldPix.y);
                currModifiedPixmap = oldPix;
                updateTotalBounds(oldPix);

            } else if (vertexIndex > 0) {

                clampTotalBounds();

                scaledView.localToStageCoordinates(temp.set(minX, minY));
                int pixX = MathUtils.round(temp.x);
                int pixY = MathUtils.round(temp.y);
                scaledView.localToStageCoordinates(temp.set(maxX, maxY));
                int pixWidth = MathUtils.round(temp.x - pixX);
                int pixHeight = MathUtils.round(temp.y - pixY);

                if (currModifiedPixmap != null) {
                    undoPixmaps.add(currModifiedPixmap);
                } else {
                    undoPixmaps.add(flusher);
                }

                takePixmap(pixX, pixY, pixWidth, pixHeight);
                resetMesh();
            }

            return this.dummyEvent;
        }

        private void takePixmap(int x, int y, int width, int height) {
            fbo.begin();
            drawMesh();
            currModifiedPixmap = new PixmapRegion(ScreenUtils.getFrameBufferPixmap(x, y, width, height), x, y);
            fbo.end();
        }

        private void debug(PixmapRegion pix) {
            if (debug) {
                pix.pixmap.setColor(Color.GREEN);
                pix.pixmap.drawRectangle(pix.x, pix.y, pix.pixmap.getWidth(), pix.pixmap.getHeight());
            }
        }

        @Override
        public boolean canUndo() {
            return true;
        }

        @Override
        public ModelEvent undoCommand() {
            if (undoPixmaps.size == 0) {
                return this.dummyEvent;
            }

            redoPixmaps.add(currModifiedPixmap);
            currModifiedPixmap = null;

            if (debug) {
                flusher.pixmap.setColor(Color.BLUE);
            }
            flusher.pixmap.fill();

            final PixmapRegion oldPix = undoPixmaps.pop();
            if (oldPix.pixmap != flusher.pixmap) {
                debug(oldPix);
                flusher.pixmap.drawPixmap(oldPix.pixmap, oldPix.x - flusher.x, oldPix.y - flusher.y);
            }
            currModifiedPixmap = oldPix;

            updateTotalBounds(oldPix);
            debug(flusher);

            showingTexRegion.getTexture().draw(flusher.pixmap, flusher.x, flusher.y);

            return this.dummyEvent;
        }

        @Override
        public boolean modifiesResource() {
            return false;
        }

        @Override
        public boolean combine(Command other) {
            return false;
        }
    }

    /**
     * Keeps a reference to a {@link Pixmap} and the screen position that should
     * be drawn. The position is represented in the {@link Stage} coordinate
     * system.
     */
    class PixmapRegion implements Disposable {
        Pixmap pixmap;
        int x, y;

        /**
         * Keeps a reference to a {@link Pixmap} and the screen position that
         * should be drawn. The position is represented in the {@link Stage}
         * coordinate system.
         */
        public PixmapRegion(Pixmap pixmap, int x, int y) {
            this.pixmap = pixmap;
            this.x = x;
            this.y = y;
        }

        @Override
        public void dispose() {
            if (this.pixmap != null && this.pixmap != flusher.pixmap) {
                this.pixmap.dispose();
                this.pixmap = null;
            }
        }
    }
}