/* * Copyright 2015 Brandon Borkholder * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jogamp.glg2d.impl; import static java.lang.Math.acos; import static java.lang.Math.sqrt; import java.awt.BasicStroke; import java.nio.FloatBuffer; import com.jogamp.opengl.GL; import org.jogamp.glg2d.PathVisitor; import org.jogamp.glg2d.VertexBuffer; /** * Tesselating is expensive. This is a simple workaround to check if we can just * draw the simple, convex polygon without tesselating. At each corner, we have * to check the sign of the z-component of the cross-product. If it's the same * all the way around, we know that every turn went the same direction. That * ensures it's convex. That's necessary, but not sufficient since we might * still have self-intersections. For that, we check that the total curvature * along the path is 2π. That ensures it's simple. * * <p> * This checks every corner and if it has the same sign and total curvature is * 2π, we know the polygon is convex. Once we get to the end, we draw it. If * it's not convex, then we fall back to tesselating it. * </p> * <p> * There are many places where we could fail being a simple convex polygon and * then have to fail over to the tesselator. As soon as we fail over we need to * catch the tesselator up to the current position and then use the tesselator * from then on. For that reason, this class is a little messy. * </p> */ public class SimpleOrTesselatingVisitor extends SimplePathVisitor { /** * This buffer is used to store points for the simple polygon, until we find * out it's not simple. Then we push all this data to the tesselator and * ignore the buffer. */ protected VertexBuffer buffer = new VertexBuffer(1024); /** * This is the buffer of vertices we'll use to test the corner. */ protected float[] previousVertices; protected int numberOfPreviousVertices; /** * The total curvature along the path. Since we know we close the path, if * it's a simple, convex polygon, we'll have a total curvature of 2π. */ protected double totalCurvature; /** * All corners must have the same sign. */ protected int sign; /** * The flag to indicate if we currently believe this polygon to be simple and * convex. */ protected boolean isConvexSoFar; /** * The flag to indicate if we are on our first segment (move-to). If we have * multiple move-to's, then we need to tesselate. */ protected boolean firstContour; /** * Keep the winding rule for when we pass the information off to the * tesselator. */ protected int windingRule; protected PathVisitor tesselatorFallback; protected PathVisitor simpleFallback; public SimpleOrTesselatingVisitor(PathVisitor simpleVisitor, PathVisitor tesselatorVisitor) { tesselatorFallback = tesselatorVisitor; simpleFallback = simpleVisitor; } @Override public void setGLContext(GL context) { simpleFallback.setGLContext(context); tesselatorFallback.setGLContext(context); } @Override public void setStroke(BasicStroke stroke) { // this is only used to fill, no need to consider stroke } @Override public void beginPoly(int windingRule) { isConvexSoFar = true; firstContour = true; sign = 0; totalCurvature = 0; this.windingRule = windingRule; } @Override public void moveTo(float[] vertex) { if (firstContour) { firstContour = false; } else if (isConvexSoFar) { setUseTesselator(false); } if (isConvexSoFar) { numberOfPreviousVertices = 1; previousVertices = new float[] { vertex[0], vertex[1], 0, 0 }; buffer.clear(); buffer.addVertex(vertex[0], vertex[1]); } else { tesselatorFallback.closeLine(); tesselatorFallback.moveTo(vertex); } } @Override public void lineTo(float[] vertex) { if (isConvexSoFar) { buffer.addVertex(vertex[0], vertex[1]); if (!isValidCorner(vertex)) { setUseTesselator(false); } } else { tesselatorFallback.lineTo(vertex); } } /** * Returns true if the corner is correct, using the new vertex and the buffer * of previous vertices. This always updates the buffer of previous vertices. */ protected boolean isValidCorner(float[] vertex) { if (numberOfPreviousVertices >= 2) { double diff1 = previousVertices[2] - previousVertices[0]; double diff2 = previousVertices[3] - previousVertices[1]; double diff3 = vertex[0] - previousVertices[0]; double diff4 = vertex[1] - previousVertices[1]; double cross2 = diff1 * diff4 - diff2 * diff3; /* * Check that the current sign of the cross-product is the same as the * others. */ int currentSign = sign(cross2); if (sign == 0) { sign = currentSign; // allow for currentSign = 0, in which case we don't care } else if (currentSign * sign == -1) { return false; } /* * Check that the total curvature along the path is less than 2π. */ double norm1sq = diff1 * diff1 + diff2 * diff2; double norm2sq = diff3 * diff3 + diff4 * diff4; double dot = diff1 * diff3 + diff2 * diff4; double cosThetasq = dot * dot / (norm1sq * norm2sq); double theta = acos(sqrt(cosThetasq)); totalCurvature += theta; if (totalCurvature > 2 * Math.PI + 1e-3) { return false; } } numberOfPreviousVertices++; previousVertices[2] = previousVertices[0]; previousVertices[3] = previousVertices[1]; previousVertices[0] = vertex[0]; previousVertices[1] = vertex[1]; return true; } protected int sign(double value) { if (value > 1e-8) { return 1; } else if (value < -1e-8) { return -1; } else { return 0; } } @Override public void closeLine() { if (isConvexSoFar) { /* * If we're convex so far, we need to finish out all the corners to make * sure everything is kosher. */ FloatBuffer buf = buffer.getBuffer(); float[] vertex = new float[2]; int position = buf.position(); buf.rewind(); buf.get(vertex); boolean good = false; if (isValidCorner(vertex)) { buf.get(vertex); if (isValidCorner(vertex)) { good = true; } } buf.position(position); if (!good) { setUseTesselator(true); } } else { tesselatorFallback.closeLine(); } } @Override public void endPoly() { if (isConvexSoFar) { simpleFallback.beginPoly(windingRule); drawToVisitor(simpleFallback, true); simpleFallback.endPoly(); } else { tesselatorFallback.endPoly(); } } /** * Sets the state to start using the tesselator. This will catch the * tesselator up to the current position and then set {@code isConvexSoFar} to * false so we can start using the tesselator exclusively. * * If {@code doClose} is true, then we will also close the line when we update * the tesselator. This is for when we realized it's not a simple poly after * we already finished the first path. */ protected void setUseTesselator(boolean doClose) { isConvexSoFar = false; tesselatorFallback.beginPoly(windingRule); drawToVisitor(tesselatorFallback, doClose); } protected void drawToVisitor(PathVisitor visitor, boolean doClose) { FloatBuffer buf = buffer.getBuffer(); buf.flip(); float[] vertex = new float[2]; if (buf.hasRemaining()) { buf.get(vertex); visitor.moveTo(vertex); } while (buf.hasRemaining()) { buf.get(vertex); visitor.lineTo(vertex); } if (doClose) { visitor.closeLine(); } // put everything back the way it was buffer.clear(); } }