org.ams.prettypaint.OutlinePolygon.java Source code

Java tutorial

Introduction

Here is the source code for org.ams.prettypaint.OutlinePolygon.java

Source

/*
 *
 *  The MIT License (MIT)
 *
 *  Copyright (c) <2015> <Andreas Modahl>
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *  THE SOFTWARE.
 *
 */
package org.ams.prettypaint;

import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Intersector;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import org.ams.core.Util;
import org.ams.prettypaint.def.OutlinePolygonDef;

/**
 * Uses triangle strips to draw anti aliased polygon edges.
 * Use together with {@link PrettyPolygonBatch} to draw things.
 *
 * @author Andreas
 */
public class OutlinePolygon implements PrettyPolygon {
    /** A vertex that is given by the user of this class. */
    protected static final int VERTEX_TYPE_USER = 1;

    /**
     * An auxiliary vertex that is needed to create the triangle strips.
     * Each user vertex is associated with just one aux vertex.
     */
    protected static final int VERTEX_TYPE_AUX = 0;

    /** The actual vertices of the polygon. */
    private final Array<Vector2> vertices = new Array<Vector2>(true, 1, Vector2.class);

    /** This array is only updated when {@link #getVerticesRotatedScaledAndTranslated()} is called. */
    private final Array<Vector2> verticesRotatedAndTranslated = new Array<Vector2>(true, 4, Vector2.class);

    /**
     * When a value in this array is true the StripVertex with corresponding index must be updated before drawing.
     * There are several methods that cause values in this array to become true.
     */
    private final Array<Boolean> needsUpdate = new Array<Boolean>(true, 4, Boolean.class);

    /** The actual triangle strip is contained in this array. */
    private final Array<StripVertex> vertexDataArray = new Array<StripVertex>(true, 4, StripVertex.class);

    private final AuxVertexFinder auxVertexFinder = new AuxVertexFinder();

    private final Vector2 tmp = new Vector2(), tmp1 = new Vector2(), tmp2 = new Vector2(), tmp3 = new Vector2();
    private final Rectangle tmpRectangle = new Rectangle();
    private final Color tmpColor = new Color();

    private final Rectangle frustum = new Rectangle();

    protected Array<OutlinePolygon> myParents = new Array<OutlinePolygon>();
    protected Array<OutlinePolygon> myChildren = new Array<OutlinePolygon>();

    private final Vector2 position = new Vector2();
    private float angleRad = 0;
    private float weight = 1.25f;
    private Color color = new Color(Color.BLACK);
    private float scale = 1f;
    private float halfWidth = 0.02f;
    private float opacity = 1;

    private boolean roundSharpCorners = true;

    private Array<BoundingBox> boundingBoxes = new Array<BoundingBox>(true, 4, BoundingBox.class);

    private DebugRenderer debugRenderer;
    private final Color debugFillGreen = new Color(0, 1, 0, 0.1f);
    private final Color debugFillRed = new Color(1, 0, 0, 0.1f);

    /** Whether to draw the inside triangle strip. */
    private boolean drawInside = true;

    /** Whether to draw the outside triangle strip. */
    private boolean drawOutside = true;

    /** I am unsure which value is optimal. */
    private int verticesPerBox = 10;

    private boolean visible = true;

    /**
     * Used by a parent to determine when it should draw. A parent draws
     * after all its children has had their draw method called. A child
     * is never drawn itself.
     */
    private int drawInvocations = 0;

    /** Whether to draw the last edge. */
    private boolean closedPolygon = true;

    /** Used by a {@link PrettyPolygonBatch} to determine if it should stop debug rendering this polygon. */
    private long timeOfLastDrawCall;

    private boolean drawCullingRectangles = false;
    private boolean drawTriangleStrips = false;
    private boolean drawLineFromFirstToLast = false;

    private boolean changesToStripOrCulling = false;

    private Object userData;

    public OutlinePolygon() {
        this(null);
    }

    /** Draws anti aliased polygon edges. */
    public OutlinePolygon(OutlinePolygonDef outlinePolygonDef) {

        debugRenderer = new DebugRenderer(this) {

            @Override
            public void draw(ShapeRenderer shapeRenderer) {
                if (drawCullingRectangles)
                    drawCullingRectangles(shapeRenderer, Color.GREEN);

                if (drawLineFromFirstToLast)
                    drawLineFromFirstToLast(shapeRenderer, Color.BLUE);

                if (drawTriangleStrips)
                    drawTriangleStrips(shapeRenderer, Color.RED, Color.ORANGE);

            }

            @Override
            void update() {
                super.update();
                boolean enabled = drawLineFromFirstToLast;
                enabled |= drawCullingRectangles;
                enabled |= drawTriangleStrips;

                enabled &= myParents.size == 0;

                boolean change = this.enabled != enabled;
                if (!change)
                    return;

                this.enabled = enabled;

                debugColors.clear();

                if (drawCullingRectangles) {
                    debugColors.add(new DebugColor(Color.GREEN, "Bounding box"));
                }
                if (drawLineFromFirstToLast) {
                    debugColors.add(new DebugColor(Color.BLUE, "Line from first to last vertex in bounding box"));
                }
                if (drawTriangleStrips) {
                    debugColors.add(new DebugColor(Color.RED, "Triangle strip visualization"));
                    debugColors.add(new DebugColor(Color.ORANGE, "Triangle strip visualization"));

                }

            }
        };

        auxVertexFinder.setHalfWidth(this.halfWidth);

        if (outlinePolygonDef != null) {

            setDrawOutside(outlinePolygonDef.drawOutside);
            setDrawInside(outlinePolygonDef.drawInside);
            setColor(outlinePolygonDef.color);
            setClosedPolygon(outlinePolygonDef.closedPolygon);
            setHalfWidth(outlinePolygonDef.halfWidth);
            setWeight(outlinePolygonDef.weight);
            setRoundSharpCorners(outlinePolygonDef.roundSharpCorners);

            setDrawLineFromFirstToLast(outlinePolygonDef.drawLineFromFirstToLast);
            setDrawCullingRectangles(outlinePolygonDef.drawBoundingBoxes);
            setDrawTriangleStrips(outlinePolygonDef.drawTriangleStrips);

            setVisible(outlinePolygonDef.visible);
            setVertices(outlinePolygonDef.vertices);
            setScale(outlinePolygonDef.scale);
            setAngle(outlinePolygonDef.angle);
            setPosition(outlinePolygonDef.position);
            setOpacity(outlinePolygonDef.opacity);

        }
    }

    public Rectangle getBoundingRectangle() {
        Rectangle rectangle = new Rectangle();
        boolean initialized = false;

        for (BoundingBox boundingBox : boundingBoxes) {
            updateBoxCulling(boundingBox);

            Rectangle cullingArea = getCullingArea(tmpRectangle, boundingBox.rectangle, angleRad, position, scale);

            if (!initialized) {
                rectangle.set(cullingArea);
                initialized = true;
            } else
                rectangle.merge(cullingArea);
        }

        return rectangle;

    }

    /** Do not modify. These are set by the {@link OutlineMerger}. */
    public Array<OutlinePolygon> getMyParents() {
        return myParents;
    }

    /** Do not modify. These are set by the {@link OutlineMerger}. */
    public Array<OutlinePolygon> getMyChildren() {
        return myChildren;
    }

    @Override
    public Object getUserData() {
        return userData;
    }

    @Override
    public OutlinePolygon setUserData(Object userData) {
        this.userData = userData;
        return this;
    }

    @Override
    public OutlinePolygon setOpacity(float opacity) {
        this.opacity = opacity;
        return this;
    }

    @Override
    public float getOpacity() {
        return opacity;
    }

    @Override
    public long getTimeOfLastDrawCall() {
        return timeOfLastDrawCall;
    }

    /**
     * You can call this method to update a vertex individually, instead of updating
     * all the vertices with {@link #setVertices(Array).
     *
     * @param vertexIndex the index of the vertex you wish to change
     * @param vertex      the new vertex coordinates
     */
    public void updateVertex(int vertexIndex, Vector2 vertex) {
        updateVertex(vertexIndex, vertex.x, vertex.y);
    }

    /**
     * You can call this method to update a vertex individually, instead of updating
     * all the vertices with {@link #setVertices(Array).
     *
     * @param vertexIndex the index of the vertex you wish to change
     * @param newX        the new x coordinate
     * @param newY        the new y coordinate
     */
    public void updateVertex(int vertexIndex, float newX, float newY) {
        vertices.items[vertexIndex].set(newX, newY);

        // schedule this vertex and its neighbours for a remake
        needsUpdate.items[vertexIndex] = true;
        needsUpdate.items[(vertexIndex + 1) % needsUpdate.size] = true;
        needsUpdate.items[(vertexIndex - 1 + needsUpdate.size) % needsUpdate.size] = true;

        changesToStripOrCulling = true;
    }

    public void addVertex(Vector2 vertex) {
        addVertex(vertex.x, vertex.y);
    }

    public void addVertex(float x, float y) {
        vertices.add(new Vector2(x, y));

        if (needsUpdate.size >= 1)
            needsUpdate.items[needsUpdate.size - 1] = true;
        needsUpdate.add(true);

        if (closedPolygon)
            needsUpdate.items[0] = true;

        vertexDataArray.add(new StripVertex());
        verticesRotatedAndTranslated.add(new Vector2());

        changesToStripOrCulling = true;

    }

    public void removeVertex(int index) {
        vertices.removeIndex(index);
        verticesRotatedAndTranslated.removeIndex(index);
        needsUpdate.removeIndex(index);
        vertexDataArray.removeIndex(index);

        changesToStripOrCulling = true;

        if (vertices.size == 0)
            return;

        int previous = (index - 1 + vertices.size) % vertices.size;

        if (previous == vertices.size - 1 && closedPolygon)
            needsUpdate.items[previous] = true;
        else if (previous >= 0)
            needsUpdate.items[previous] = true;

        int next = index % vertices.size;

        if (next == 0 && closedPolygon)
            needsUpdate.items[next] = true;
        else if (next >= 0)
            needsUpdate.items[next] = true;

    }

    public int getVertexCount() {
        return vertices.size;
    }

    /**
     * Rounding of sharp corners can make the outlines look better.
     *
     * @return whether sharp corners are being rounded.
     */
    public boolean isRoundingSharpCorners() {
        return roundSharpCorners;
    }

    /**
     * Rounding of sharp corners can make the outlines look better.
     *
     * @param roundSharpCorners whether to round sharp corners.
     */
    public void setRoundSharpCorners(boolean roundSharpCorners) {
        boolean change = this.roundSharpCorners != roundSharpCorners;

        if (change) {
            for (int i = 0; i < needsUpdate.size; i++) {
                needsUpdate.items[i] = true;
            }
            changesToStripOrCulling = true;
        }

        this.roundSharpCorners = roundSharpCorners;
    }

    /**
     * Draw the edges defined by {@link #setVertices(Array)}.
     * If {@link OutlineMerger#mergeOutlines(Array)} has been used on this {@link OutlinePolygon} then
     * the draw method may just be redirected to this outlinePolygon's parents.
     *
     * @param batch Accumulates data and sends it in large portions to the gpu, instead of sending small portions more often.
     * @return this for chaining.
     */
    public OutlinePolygon draw(PrettyPolygonBatch batch) {

        if (myParents.size > 0) {
            // if i have a parent i will not be drawn
            for (OutlinePolygon outlinePolygon : myParents) {
                outlinePolygon.draw(batch);
            }
            return this;
        }

        drawInvocations++;

        if (myChildren.size == 0 || drawInvocations >= myChildren.size) {

            if (!visible)
                return this;

            debugRenderer.queueIfEnabled(batch);

            timeOfLastDrawCall = System.currentTimeMillis();
            // if i don't have any children i will just attempt to draw right away
            // also if all my children has had their draw method called i will attempt to draw

            drawInvocations = 0;

            if (halfWidth <= 0)
                return this;
            if (color.a <= 0)
                return this;
            if (weight <= 0)
                return this;
            if (scale <= 0)
                return this;
            if (!drawInside && !drawOutside)
                return this;

            Rectangle frustum = this.frustum.set(batch.frustum);

            tmpColor.set(color).a *= opacity;

            updateStripAndCulling();

            for (BoundingBox box : boundingBoxes) {

                Rectangle cullingArea = getCullingArea(tmpRectangle, box.rectangle, angleRad, position, scale);

                if (frustum.overlaps(cullingArea)) {
                    // if we reached here we can draw the vertices within this particular BoundingBox

                    if (drawInside) {
                        batch.drawOutline(vertexDataArray, closedPolygon, true, box.begin,
                                box.begin + box.count + 1, box.dataCount, tmpColor, scale, angleRad, position.x,
                                position.y, weight);
                    }

                    if (drawOutside) {
                        batch.drawOutline(vertexDataArray, closedPolygon, false, box.begin,
                                box.begin + box.count + 1, box.dataCount, tmpColor, scale, angleRad, position.x,
                                position.y, weight);
                    }
                }
            }
        }
        return this;
    }

    /**
     * When you call methods like for example {@link #setHalfWidth(float)} or {@link #updateVertex(int, float, float)}
     * work is being queued up. Before each draw call this method is automatically called to do all this work.
     * <p/>
     * It can be smart to call this method after you are done configuring your OutlinePolygon, but before you
     * exit your loading screen. This way you do more work during the loading screen and get less initial lag.
     */
    public void updateStripAndCulling() {
        if (!changesToStripOrCulling)
            return;

        updateAllBoxIndices();

        boolean clockwiseUpdated = false;

        for (int i = 0; i < needsUpdate.size; i++) {
            boolean update = needsUpdate.items[i];
            if (update) {
                if (!clockwiseUpdated) {
                    clockwiseUpdated = true;
                    updateAuxVertexFinderClockwise();
                }
                if (drawInside) {
                    updateVertex(i, true);
                }
                if (drawOutside) {
                    updateVertex(i, false);
                }

                needsUpdate.items[i] = false;
            }
        }

        for (BoundingBox box : boundingBoxes) {
            if (box.needsCullingUpdate) {
                updateBoxCulling(box);
                box.needsCullingUpdate = false;
            }
        }
    }

    /**
     * The aux vertex finder needs to know if the polygon is a clockwise one.
     * Instead of figuring out whether the polygon is clockwise each time it needs to know
     * this method is called at most once during {@link #updateStripAndCulling()}, and only if
     * the vertices has been changed.
     */
    private void updateAuxVertexFinderClockwise() {
        boolean clockwisePolygon = Util.clockwisePolygon(vertices);
        auxVertexFinder.setClockwise(clockwisePolygon);
    }

    /**  */
    private void updateAllBoxIndices() {
        while (boundingBoxes.size * verticesPerBox < vertices.size) {
            if (boundingBoxes.size >= 1) {
                boundingBoxes.items[boundingBoxes.size - 1].count = verticesPerBox;
            }
            boundingBoxes.add(new BoundingBox(boundingBoxes.size * verticesPerBox, verticesPerBox));
        }
        while (boundingBoxes.size * verticesPerBox >= vertices.size + verticesPerBox) {
            boundingBoxes.pop();
        }
        if (boundingBoxes.size > 0) {

            int n = vertices.size % verticesPerBox;
            if (n == 0)
                n = verticesPerBox;
            if (!closedPolygon)
                n--;
            boundingBoxes.peek().count = n;

        }
    }

    // TODO Comment
    private void updateBoxCulling(BoundingBox box) {
        Rectangle rectangle = box.rectangle;

        box.dataCount = 0;

        boolean initialized = false;
        for (int n = box.begin; n < box.begin + box.count; n++) {

            Array<Float> inside = vertexDataArray.items[n].insideVertexData;
            Array<Float> outside = vertexDataArray.items[n].outsideVertexData;

            if (!initialized) {
                if (inside.size > 0 && drawInside) {
                    rectangle.set(inside.items[0], inside.items[1], 0, 0);
                } else if (outside.size > 0 && drawOutside) {
                    rectangle.set(outside.items[0], outside.items[1], 0, 0);
                }

                if (closedPolygon || n > 0) {
                    int k = (box.begin - 1 + vertices.size) % vertices.size;

                    if (drawInside) {
                        Array<Float> _inside = vertexDataArray.items[k].insideVertexData;
                        for (int i = 0; i < _inside.size; i += 3) {
                            rectangle.merge(_inside.items[i], _inside.items[i + 1]);
                            box.dataCount++;
                        }
                    }

                    if (drawOutside) {
                        Array<Float> _outside = vertexDataArray.items[k].outsideVertexData;
                        for (int i = 0; i < _outside.size; i += 3) {
                            rectangle.merge(_outside.items[i], _outside.items[i + 1]);
                            box.dataCount++;
                        }
                    }
                }

                if (closedPolygon || n < vertices.size) {
                    int k = (box.begin + box.count + vertices.size) % vertices.size;

                    if (drawInside) {
                        Array<Float> _inside = vertexDataArray.items[k].insideVertexData;
                        for (int i = 0; i < _inside.size; i += 3) {
                            rectangle.merge(_inside.items[i], _inside.items[i + 1]);
                            box.dataCount++;
                        }
                    }

                    if (drawOutside) {
                        Array<Float> _outside = vertexDataArray.items[k].outsideVertexData;
                        for (int i = 0; i < _outside.size; i += 3) {
                            rectangle.merge(_outside.items[i], _outside.items[i + 1]);
                            box.dataCount++;
                        }
                    }
                }

                initialized = true;
            }

            if (drawInside)
                for (int i = 0; i < inside.size; i += 3) {
                    rectangle.merge(inside.items[i], inside.items[i + 1]);
                    box.dataCount++;
                }

            if (drawOutside)
                for (int i = 0; i < outside.size; i += 3) {
                    rectangle.merge(outside.items[i], outside.items[i + 1]);
                    box.dataCount++;
                }
        }
    }

    // TODO Comment
    private void updateVertex(int index, boolean inside) {
        if (!closedPolygon && index == 0) {
            updateVertexBeginning(inside);
        } else if (!closedPolygon && index == vertices.size - 1) {
            updateVertexEnding(inside);
        } else {
            updateVertexDefault(index, inside);
        }
    }

    // TODO Comment
    private void updateVertexDefault(int index, boolean inside) {

        BoundingBox box = boundingBoxes.items[index / verticesPerBox];
        box.needsCullingUpdate = true;

        int k = index % vertices.size;
        StripVertex stripVertex = vertexDataArray.items[k];
        AuxVertexFinder auxVertexFinder = this.auxVertexFinder;

        Array<Float> vertexData = inside ? stripVertex.insideVertexData : stripVertex.outsideVertexData;
        vertexData.clear();

        auxVertexFinder.setInsideStrip(inside);

        Vector2 currentVertex = vertices.items[k];
        Vector2 defaultAux = auxVertexFinder.getAux(vertices, k);

        boolean roundCorner = false;

        { // within these brackets i figure out if i should round the corner.
          // i hope to rewrite this code to something i can understand one day after writing it
            Vector2 previous = tmp.set(vertices.items[(k - 1 + vertices.size) % vertices.size]);
            Vector2 copyOfDefaultAux = tmp1.set(defaultAux);

            previous.sub(currentVertex);
            copyOfDefaultAux.sub(currentVertex);

            float angle = previous.angleRad() - copyOfDefaultAux.angleRad();
            angle = ((angle + MathUtils.PI2) % MathUtils.PI) * 2f;
            boolean angleMoreThanPI;
            if (inside) {
                angleMoreThanPI = angle > MathUtils.PI * 1.1f;
            } else {
                angleMoreThanPI = angle < MathUtils.PI * 0.9f;
            }

            if (auxVertexFinder.clockWisePolygon)
                angleMoreThanPI = !angleMoreThanPI;

            if (angleMoreThanPI) {
                boolean sharpCorner = Math.abs(MathUtils.PI - angle) > Math.PI * 0.4f;
                roundCorner = roundSharpCorners && sharpCorner;
            }
        }

        if (roundCorner) {
            Vector2 beginningAux = auxVertexFinder.getAuxEnding(vertices, k, 0);
            Vector2 endingAux = auxVertexFinder.getAuxBeginning(vertices, k, 0);
            Vector2 middleAux = tmp.set(defaultAux).sub(currentVertex).nor().scl(halfWidth).add(currentVertex);

            add(currentVertex, VERTEX_TYPE_USER, vertexData);
            add(beginningAux, VERTEX_TYPE_AUX, vertexData);

            add(currentVertex, VERTEX_TYPE_USER, vertexData);
            add(middleAux, VERTEX_TYPE_AUX, vertexData);

            add(currentVertex, VERTEX_TYPE_USER, vertexData);
            add(endingAux, VERTEX_TYPE_AUX, vertexData);
        } else {
            add(currentVertex, VERTEX_TYPE_USER, vertexData);
            add(defaultAux, VERTEX_TYPE_AUX, vertexData);
        }

    }

    /**
     * Updates a strip vertex with vertices shaping a rounded ending. This method
     * is called when the polygon is open (not {@link #isClosedPolygon()}).
     *
     * @param inside whether this is a <b>inside</b> triangle strip,
     *               as opposed to an outside triangle strip.
     */
    private void updateVertexEnding(boolean inside) {
        // if polygon is not closed the endings must be fixed
        int index = vertices.size - 1;

        BoundingBox box = boundingBoxes.items[index / verticesPerBox];
        box.needsCullingUpdate = true;

        StripVertex stripVertex = vertexDataArray.items[index];
        AuxVertexFinder auxVertexFinder = this.auxVertexFinder;

        Array<Float> vertexData = inside ? stripVertex.insideVertexData : stripVertex.outsideVertexData;
        vertexData.clear();

        auxVertexFinder.setInsideStrip(inside);

        Vector2 currentVertex = vertices.items[index];

        Vector2 currentAux = auxVertexFinder.getAuxEnding(vertices, index, 0);
        add(currentVertex, VERTEX_TYPE_USER, vertexData);
        add(currentAux, VERTEX_TYPE_AUX, vertexData);

        currentAux = auxVertexFinder.getAuxEnding(vertices, index, MathUtils.PI * 0.25f);
        add(currentVertex, VERTEX_TYPE_USER, vertexData);
        add(currentAux, VERTEX_TYPE_AUX, vertexData);

        currentAux = auxVertexFinder.getAuxEnding(vertices, index, MathUtils.PI * 0.5f);
        add(currentVertex, VERTEX_TYPE_USER, vertexData);
        add(currentAux, VERTEX_TYPE_AUX, vertexData);
    }

    /**
     * Updates a strip vertex with vertices shaping a rounded beginning. This method
     * is called when the polygon is open (not {@link #isClosedPolygon()}).
     *
     * @param inside whether this is a <b>inside</b> triangle strip,
     *               as opposed to an outside triangle strip.
     */
    private void updateVertexBeginning(boolean inside) {
        // if polygon is not closed the endings must be fixed
        int index = 0;

        BoundingBox box = boundingBoxes.items[0];
        box.needsCullingUpdate = true;

        StripVertex stripVertex = vertexDataArray.items[index];
        AuxVertexFinder auxVertexFinder = this.auxVertexFinder;

        Array<Float> vertexData = inside ? stripVertex.insideVertexData : stripVertex.outsideVertexData;
        vertexData.clear();

        auxVertexFinder.setInsideStrip(inside);

        Vector2 currentVertex = vertices.items[index];

        Vector2 currentAux = auxVertexFinder.getAuxBeginning(vertices, index, MathUtils.PI * 0.5f);
        add(currentVertex, VERTEX_TYPE_USER, vertexData);
        add(currentAux, VERTEX_TYPE_AUX, vertexData);

        currentAux = auxVertexFinder.getAuxBeginning(vertices, index, MathUtils.PI * 0.25f);
        add(currentVertex, VERTEX_TYPE_USER, vertexData);
        add(currentAux, VERTEX_TYPE_AUX, vertexData);

        currentAux = auxVertexFinder.getAuxBeginning(vertices, index, 0);
        add(currentVertex, VERTEX_TYPE_USER, vertexData);
        add(currentAux, VERTEX_TYPE_AUX, vertexData);
    }

    /**
     * There are the vertices set by the user and then there are the auxiliary vertices that
     * are needed to build a pretty triangle strip. This class can find these auxiliary vertices.
     */
    private class AuxVertexFinder {

        private final Vector2 m1 = new Vector2(), m2 = new Vector2(), n1 = new Vector2(), n2 = new Vector2(),
                nor1 = new Vector2(), nor2 = new Vector2();

        /** Whether to find auxVertices for inside or outside strips */
        boolean insideStrip = true;
        /** Whether to find auxVertices for a clockwise or anti clockwise polygon */
        boolean clockWisePolygon = true;

        /** The width of the inside or outside strip. */
        float halfWidth = 0.02f;

        /** Whether to find auxVertices for inside or outside strips */
        void setInsideStrip(boolean insideStrip) {
            this.insideStrip = insideStrip;
        }

        /** Whether to find auxVertices for a clockwise or anti clockwise polygon */
        void setClockwise(boolean clockwise) {
            this.clockWisePolygon = clockwise;
        }

        /** The width of the inside or outside strip. */
        void setHalfWidth(float halfWidth) {
            this.halfWidth = halfWidth;

        }

        /**
         * Finds the auxiliary vertices that are used to fill all the edges that are made. There is typically
         * one of these for each vertex set by the user.
         */
        private Vector2 getAux(Array<Vector2> vertices, int i) {
            int dir = clockWisePolygon ? 1 : -1;

            if (insideStrip)
                dir *= -1;
            Util.getEdge(vertices, (i + vertices.size - 1) % vertices.size, m1, m2);

            nor1.set(m2).sub(m1).nor().scl(halfWidth).rotate90(dir);
            m1.add(nor1);
            m2.add(nor1);

            Util.getEdge(vertices, (i + vertices.size) % vertices.size, n1, n2);

            nor2.set(n2).sub(n1).nor().scl(halfWidth).rotate90(dir);
            n1.add(nor2);
            n2.add(nor2);

            Vector2 result = new Vector2();
            Intersector.intersectLines(m1, m2, n1, n2, result);
            return result;

        }

        /** When the polygon is not closed we need auxiliary vertices to round the ending. */
        private Vector2 getAuxBeginning(Array<Vector2> vertices, int i, float extraAngleRad) {
            Util.getEdge(vertices, i, m1, m2);
            nor1.set(m1).sub(m2).nor().scl(halfWidth);

            int dir = clockWisePolygon ? -1 : 1;

            if (insideStrip)
                dir *= -1;

            nor1.rotate90(dir);
            nor1.rotateRad(extraAngleRad * (-dir));

            return new Vector2(m1).add(nor1);
        }

        /** When the polygon is not closed we need auxiliary vertices to round the beginning. */
        private Vector2 getAuxEnding(Array<Vector2> vertices, int i, float extraAngleRad) {
            Util.getEdge(vertices, (i - 1 + vertices.size) % vertices.size, m1, m2);
            nor1.set(m2).sub(m1).nor().scl(halfWidth);

            int dir = clockWisePolygon ? 1 : -1;

            if (insideStrip)
                dir *= -1;

            nor1.rotate90(dir);
            nor1.rotateRad(extraAngleRad * (-dir));

            return new Vector2(m2).add(nor1);
        }
    }

    /** @return half the width of the edges. */
    public float getHalfWidth() {
        return halfWidth;
    }

    /**
     * @param halfWidth half the width of the edges.
     * @return this for chaining.
     */
    public OutlinePolygon setHalfWidth(float halfWidth) {
        this.halfWidth = halfWidth;
        auxVertexFinder.setHalfWidth(this.halfWidth);

        for (int i = 0; i < needsUpdate.size; i++) {
            needsUpdate.items[i] = true;
        }
        changesToStripOrCulling = true;

        return this;
    }

    /**
     * Lines have an insideStrip and an outside part. Sometimes you only want to draw one of them. For example
     * if you want to draw a shadow you may only want to draw the outside part.
     *
     * @param drawInside whether to draw the insideStrip part of the lines.
     * @return this for chaining.
     */
    public OutlinePolygon setDrawInside(boolean drawInside) {
        this.drawInside = drawInside;
        return this;
    }

    /**
     * Lines have an insideStrip and an outside part. Sometimes you only want to draw one of them. For example
     * if you want to draw a shadow you may only want to draw the outside part.
     *
     * @return whether the insideStrip part of the lines are drawn.
     */
    public boolean isInsideDrawn() {
        return drawInside;
    }

    /**
     * Lines have an insideStrip and an outside part. Sometimes you only want to draw one of them. For example
     * if you want to draw a shadow you may only want to draw the outside part.
     *
     * @param drawOutside whether to draw the outside part of the lines.
     * @return this for chaining.
     */
    public OutlinePolygon setDrawOutside(boolean drawOutside) {
        this.drawOutside = drawOutside;
        return this;
    }

    /**
     * Lines have an insideStrip and an outside part. Sometimes you only want to draw one of them. For example
     * if you want to draw a shadow you may only want to draw the outside part.
     *
     * @return whether the outside part of the lines are drawn.
     */
    public boolean isOutsideDrawn() {
        return drawOutside;
    }

    @Override
    public Array<Vector2> getVerticesRotatedScaledAndTranslated(float rotation, float scale, float transX,
            float transY) {

        for (int i = 0; i < vertices.size; i++) {
            Vector2 w = verticesRotatedAndTranslated.items[i];
            w.set(vertices.items[i]);
            w.rotateRad(rotation);
            w.scl(scale);
            w.add(transX, transY);
        }
        return verticesRotatedAndTranslated;
    }

    @Override
    public Array<Vector2> getVertices() {
        return vertices;
    }

    @Override
    public final OutlinePolygon setVertices(Array<Vector2> vertices) {

        this.boundingBoxes.clear();

        this.vertices.clear();
        for (Vector2 v : vertices) {
            this.vertices.add(new Vector2(v));
        }

        this.needsUpdate.clear();
        for (Vector2 v : vertices) {
            this.needsUpdate.add(true);
        }

        this.vertexDataArray.clear();
        for (Vector2 v : vertices) {
            this.vertexDataArray.add(new StripVertex());
        }

        this.verticesRotatedAndTranslated.clear();
        for (Vector2 v : vertices) {
            this.verticesRotatedAndTranslated.add(new Vector2(v));
        }

        changesToStripOrCulling = true;

        return this;
    }

    /** Set the data for one vertex. Also merges this vertex with the BoundingBox's rectangle. */
    private void add(Vector2 vertex, float alpha, Array<Float> vertexData) {
        vertexData.add(vertex.x);
        vertexData.add(vertex.y);
        vertexData.add(alpha);
    }

    @Override
    public float getAngle() {
        return angleRad;
    }

    @Override
    public OutlinePolygon setAngle(float angle) {
        this.angleRad = angle;
        return this;
    }

    @Override
    public Vector2 getPosition() {
        return position;
    }

    @Override
    public OutlinePolygon setPosition(float x, float y) {
        this.position.set(x, y);
        return this;
    }

    @Override
    public OutlinePolygon setPosition(Vector2 position) {
        this.position.set(position);
        return this;
    }

    /**
     * The weight of the "brush". Increase to make edges bolder.
     *
     * @return the weight of the outline.
     */
    public float getWeight() {
        return weight;
    }

    /**
     * The weight of the "brush". Increase to make edges bolder.
     *
     * @param weight the weight of the outline.
     * @return this for chaining.
     */
    public OutlinePolygon setWeight(float weight) {
        this.weight = weight;
        return this;
    }

    /**
     * The color gets more transparent the farther from the center of an edge.
     *
     * @return The color of the edges.
     */
    public Color getColor() {
        return color;
    }

    /**
     * The color gets more transparent the farther from the center of an edge.
     *
     * @param color the color of the edges.
     * @return this for chaining.
     */
    public OutlinePolygon setColor(Color color) {
        this.color = color;
        return this;
    }

    @Override
    public float getScale() {
        return scale;
    }

    @Override
    public OutlinePolygon setScale(float scale) {
        this.scale = scale;
        return this;
    }

    /**
     * Calculates the culling area of the bounding box after it has scaled, rotated and translates. This bounding box
     * contains a bunch of vertices. This way i don't have to merge hundreds of vertices to get a reasonable culling area,
     * just the four of the bounding box.
     */
    private Rectangle getCullingArea(Rectangle cullingArea, Rectangle boundingBox, float rotation,
            Vector2 translation, float scale) {

        tmp.set(boundingBox.x, boundingBox.y).scl(scale).rotateRad(rotation).add(translation);
        cullingArea.set(tmp.x, tmp.y, 0, 0);

        tmp.set(boundingBox.x + boundingBox.width, boundingBox.y).scl(scale).rotateRad(rotation).add(translation);
        cullingArea.merge(tmp);

        tmp.set(boundingBox.x + boundingBox.width, boundingBox.y + boundingBox.height).scl(scale)
                .rotateRad(rotation).add(translation);
        cullingArea.merge(tmp);

        tmp.set(boundingBox.x, boundingBox.y + boundingBox.height).scl(scale).rotateRad(rotation).add(translation);
        cullingArea.merge(tmp);

        return cullingArea;
    }

    /**
     * When the polygon is closed an edge is drawn from the last vertex to the first.
     *
     * @return whether the polygon is closed.
     */
    public boolean isClosedPolygon() {
        return closedPolygon;
    }

    /**
     * When the polygon is closed an edge is drawn from the last vertex to the first.
     *
     * @param closedPolygon whether the polygon should be closed.
     * @return this for chaining.
     */
    public OutlinePolygon setClosedPolygon(boolean closedPolygon) {
        boolean change = this.closedPolygon != closedPolygon;

        this.closedPolygon = closedPolygon;

        if (change) {
            if (needsUpdate.size > 0) {
                needsUpdate.items[needsUpdate.size - 1] = true;
                needsUpdate.items[0] = true;
            }

        }

        changesToStripOrCulling = true;

        return this;
    }

    /** For debugging. */
    public OutlinePolygon setDrawCullingRectangles(boolean drawCullingRectangles) {
        this.drawCullingRectangles = drawCullingRectangles;
        debugRenderer.update();
        return this;
    }

    /** For debugging. */
    public boolean isDrawingCullingRectangles() {
        return drawCullingRectangles;
    }

    /** For debugging. */
    public OutlinePolygon setDrawTriangleStrips(boolean drawTriangleStrips) {
        this.drawTriangleStrips = drawTriangleStrips;
        debugRenderer.update();
        return this;
    }

    /** For debugging. */
    public boolean isDrawingTriangleStrips() {
        return drawTriangleStrips;
    }

    /** For debugging. */
    public OutlinePolygon setDrawLineFromFirstToLast(boolean drawLineFromFirstToLast) {
        this.drawLineFromFirstToLast = drawLineFromFirstToLast;
        debugRenderer.update();
        return this;
    }

    /** For debugging. */
    public boolean isDrawingLineFromFirstToLast() {
        return drawLineFromFirstToLast;
    }

    private void drawCullingRectangles(ShapeRenderer shapeRenderer, Color color) {
        for (BoundingBox br : boundingBoxes) {

            Rectangle r = br.rectangle;
            Rectangle cullingArea = getCullingArea(tmpRectangle, r, angleRad, position, scale);

            shapeRenderer.set(ShapeRenderer.ShapeType.Filled);

            Color fillColor = tmpColor.set(color);
            fillColor.a *= 0.25f;

            shapeRenderer.setColor(fillColor);

            shapeRenderer.rect(cullingArea.x, cullingArea.y, cullingArea.width, cullingArea.height);

            tmp.set(r.x, r.y).rotateRad(angleRad).add(position);
            tmp1.set(r.x + r.width, r.y).rotateRad(angleRad).add(position);
            tmp2.set(r.x + r.width, r.y + r.height).rotateRad(angleRad).add(position);
            tmp3.set(r.x, r.y + r.height).rotateRad(angleRad).add(position);

            shapeRenderer.set(ShapeRenderer.ShapeType.Line);
            shapeRenderer.setColor(color);

            shapeRenderer.line(tmp, tmp1);
            shapeRenderer.line(tmp1, tmp2);
            shapeRenderer.line(tmp2, tmp3);
            shapeRenderer.line(tmp3, tmp);

        }
    }

    private void drawLineFromFirstToLast(ShapeRenderer shapeRenderer, Color color) {
        shapeRenderer.setColor(color);
        for (BoundingBox br : boundingBoxes) {

            tmp.set(vertices.items[br.begin]);
            tmp.rotateRad(angleRad);
            tmp.scl(scale);
            tmp.add(position);

            tmp1.set(vertices.items[(br.begin + br.count) % vertices.size]);
            tmp1.rotateRad(angleRad);
            tmp1.scl(scale);
            tmp1.add(position);

            shapeRenderer.line(tmp, tmp1);

        }
    }

    private void drawTriangleStrips(ShapeRenderer shapeRenderer, Color color, Color color1) {
        for (int i = 0; i < vertexDataArray.size; i++) {
            StripVertex bb = vertexDataArray.items[i];

            Array<Float> data = bb.insideVertexData;
            for (int j = 0; j < data.size - 3;) {

                shapeRenderer.setColor(j == 0 ? color : color1);

                tmp.x = data.items[j];
                tmp.y = data.items[j + 1];
                tmp.rotateRad(angleRad);
                tmp.scl(scale);
                tmp.add(position);

                tmp1.x = data.items[j + 3];
                tmp1.y = data.items[j + 4];
                tmp1.rotateRad(angleRad);
                tmp1.scl(scale);
                tmp1.add(position);
                j += 3;

                shapeRenderer.line(tmp, tmp1);
            }
            data = bb.outsideVertexData;
            for (int j = 0; j < data.size - 3;) {

                shapeRenderer.setColor(j == 0 ? Color.ORANGE : Color.RED);

                tmp.x = data.items[j];
                tmp.y = data.items[j + 1];
                tmp.rotateRad(angleRad);
                tmp.scl(scale);
                tmp.add(position);

                tmp1.x = data.items[j + 3];
                tmp1.y = data.items[j + 4];
                tmp1.rotateRad(angleRad);
                tmp1.scl(scale);
                tmp1.add(position);
                j += 3;

                shapeRenderer.line(tmp, tmp1);
            }

        }
    }

    /**
     * The BoundingBox is for dividing outlines into different parts.
     * Each box can be rendered independently of the others,
     * it is therefore convenient for frustum culling.
     * <p/>
     * This is a public class because GWT doesn't want to compile otherwise.
     */
    public static class BoundingBox {
        /** The actual bounding rectangle */
        Rectangle rectangle = new Rectangle();

        /** Index of the first vertex that this bounding rectangle include. */
        int begin;
        /** The number of vertices that this bounding rectangle include. */
        int count;

        int dataCount;

        /**
         * This box needs a culling update when:
         * - a new stripVertex is added to it
         * - a stripVertex is removed from it
         * - a stripVertex within it changes
         */
        boolean needsCullingUpdate = true;

        BoundingBox(int begin, int count) {
            this.begin = begin;
            this.count = count;
        }
    }

    /**
     * Each vertex supplied by the user must be supplemented by other
     * vertices to make a pretty triangle strip. A strip vertex
     * contains the user vertex and all the other vertices associated with it.
     * It is ordered so that when passed to the gpu, together with the previous and next vertex, a
     * pretty triangle strip is formed.
     * <p/>
     * This is a public class because GWT doesn't want to compile otherwise.
     */
    public static class StripVertex {

        Array<Float> insideVertexData = new Array<Float>(true, 9, Float.class);

        Array<Float> outsideVertexData = new Array<Float>(true, 9, Float.class);

    }

    @Override
    public OutlinePolygon setVisible(boolean visible) {
        this.visible = visible;
        return this;
    }

    @Override
    public boolean isVisible() {
        return visible;
    }
}