/*
* 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.ceil;
import static java.lang.Math.cos;
import static java.lang.Math.sin;
import static java.lang.Math.sqrt;
import static org.jogamp.glg2d.impl.GLG2DNotImplemented.notImplemented;
import java.awt.BasicStroke;
import java.nio.FloatBuffer;
import org.jogamp.glg2d.VertexBuffer;
import com.jogamp.common.nio.Buffers;
/**
* Draws a line, as outlined by a {@link BasicStroke}. The current
* implementation supports everything except dashes. This class draws a series
* of quads for each line segment, joins corners and endpoints as appropriate.
*/
public abstract class BasicStrokeLineVisitor extends SimplePathVisitor {
protected static float THETA_STEP = 0.5f;
protected static float COS_STEP = (float) cos(THETA_STEP);
protected static float SIN_STEP = (float) sin(THETA_STEP);
protected int lineJoin;
protected int endCap;
protected float lineOffset;
protected float miterLimit;
protected float[] lastPoint;
protected float[] secondLastPoint;
protected float[] firstPoint;
protected float[] secondPoint;
protected VertexBuffer vBuffer = new VertexBuffer(1024);
protected FloatBuffer tmpBuffer = Buffers.newDirectFloatBuffer(1024);
@Override
public void setStroke(BasicStroke stroke) {
lineJoin = stroke.getLineJoin();
lineOffset = stroke.getLineWidth() / 2;
endCap = stroke.getEndCap();
miterLimit = stroke.getMiterLimit();
// TODO
if (stroke.getDashArray() != null) {
notImplemented("BasicStroke with dash array");
}
}
@Override
public void beginPoly(int windingRule) {
clear();
}
@Override
public void endPoly() {
finishAndDrawLine();
}
@Override
public void moveTo(float[] vertex) {
finishAndDrawLine();
lastPoint = new float[] { vertex[0], vertex[1] };
firstPoint = lastPoint;
}
@Override
public void lineTo(float[] vertex) {
// ignore 0-length lines
if (lastPoint[0] == vertex[0] && lastPoint[1] == vertex[1]) {
return;
}
float[] vClone = new float[] { vertex[0], vertex[1] };
if (secondPoint == null) {
secondPoint = vClone;
}
if (secondLastPoint != null) {
applyCorner(vertex);
}
secondLastPoint = lastPoint;
lastPoint = vClone;
}
@Override
public void closeLine() {
/*
* Our first point we stroked is around the second point we hit. So we add
* the first 2 points so we do all the corners. Then we end on the last two
* points to finish the last two triangles.
*/
if (firstPoint != null && secondPoint != null) {
lineTo(firstPoint);
lineTo(secondPoint);
FloatBuffer buf = vBuffer.getBuffer();
addVertex(buf.get(0), buf.get(1));
addVertex(buf.get(2), buf.get(3));
drawBuffer();
}
clear();
}
protected void clear() {
vBuffer.clear();
firstPoint = secondPoint = null;
lastPoint = secondLastPoint = null;
}
protected void finishAndDrawLine() {
if (firstPoint != null && secondPoint != null) {
applyEndCap(secondLastPoint, lastPoint, false);
FloatBuffer buf = vBuffer.getBuffer();
if (tmpBuffer.capacity() < buf.position()) {
tmpBuffer = Buffers.newDirectFloatBuffer(buf.position());
}
tmpBuffer.clear();
buf.flip();
tmpBuffer.put(buf);
tmpBuffer.flip();
buf.clear();
applyEndCap(firstPoint, secondPoint, true);
buf.put(tmpBuffer);
drawBuffer();
}
clear();
}
@Override
public void quadTo(float[] previousVertex, float[] control) {
int originalJoin = lineJoin;
// go around the corners quickly
lineJoin = BasicStroke.JOIN_BEVEL;
super.quadTo(previousVertex, control);
lineJoin = originalJoin;
}
@Override
public void cubicTo(float[] previousVertex, float[] control) {
int originalJoin = lineJoin;
// go around the corners quickly
lineJoin = BasicStroke.JOIN_BEVEL;
super.cubicTo(previousVertex, control);
lineJoin = originalJoin;
}
protected void applyCorner(float[] vertex) {
switch (lineJoin) {
case BasicStroke.JOIN_BEVEL:
drawCornerBevel(secondLastPoint, lastPoint, vertex);
break;
case BasicStroke.JOIN_ROUND:
drawCornerRound(secondLastPoint, lastPoint, vertex);
break;
case BasicStroke.JOIN_MITER:
drawCornerMiter(secondLastPoint, lastPoint, vertex);
break;
default:
GLG2DNotImplemented.notImplemented("BasicStroke with unknown line join: " + lineJoin);
}
}
protected void drawCornerRound(float[] secondLastPoint, float[] lastPoint, float[] point) {
float[] offset1 = lineOffset(secondLastPoint, lastPoint);
float[] offset2 = lineOffset(lastPoint, point);
float[] v1 = subtract(lastPoint, secondLastPoint);
normalize(v1);
float[] v2 = subtract(lastPoint, point);
normalize(v2);
float[] rightPt1 = add(lastPoint, offset1);
float[] rightPt2 = add(lastPoint, offset2);
float[] leftPt1 = subtract(lastPoint, offset1);
float[] leftPt2 = subtract(lastPoint, offset2);
float alpha = getIntersectionAlpha(rightPt1, v1, rightPt2, v2);
// get the outside angle (our vectors v1, v2 are unit vectors)
float theta = (float) (Math.PI - acos(v1[0] * v2[0] + v1[1] * v2[1]));
// if inside corner is right side
if (alpha <= 0) {
float[] rightInside = addScaled(rightPt1, v1, alpha);
addVertex(rightInside[0], rightInside[1]);
addVertex(leftPt1[0], leftPt1[1]);
int max = (int) ceil(theta / THETA_STEP);
// rotate the other way
for (int i = 0; i < max; i++) {
float newX = COS_STEP * offset1[0] + SIN_STEP * offset1[1];
offset1[1] = -SIN_STEP * offset1[0] + COS_STEP * offset1[1];
offset1[0] = newX;
addVertex(rightInside[0], rightInside[1]);
addVertex(lastPoint[0] - offset1[0], lastPoint[1] - offset1[1]);
}
addVertex(rightInside[0], rightInside[1]);
addVertex(leftPt2[0], leftPt2[1]);
} else {
alpha = -alpha;
float[] leftInside = addScaled(leftPt1, v1, alpha);
addVertex(rightPt1[0], rightPt1[1]);
addVertex(leftInside[0], leftInside[1]);
int max = (int) ceil(theta / THETA_STEP);
for (int i = 0; i < max; i++) {
float newX = COS_STEP * offset1[0] - SIN_STEP * offset1[1];
offset1[1] = SIN_STEP * offset1[0] + COS_STEP * offset1[1];
offset1[0] = newX;
addVertex(lastPoint[0] + offset1[0], lastPoint[1] + offset1[1]);
addVertex(leftInside[0], leftInside[1]);
}
addVertex(rightPt2[0], rightPt2[1]);
addVertex(leftInside[0], leftInside[1]);
}
}
protected void drawCornerBevel(float[] secondLastPoint, float[] lastPoint, float[] point) {
float[] offset1 = lineOffset(secondLastPoint, lastPoint);
float[] offset2 = lineOffset(lastPoint, point);
float[] v1 = subtract(lastPoint, secondLastPoint);
normalize(v1);
float[] v2 = subtract(lastPoint, point);
normalize(v2);
float[] rightPt1 = add(lastPoint, offset1);
float[] rightPt2 = add(lastPoint, offset2);
float[] leftPt1 = subtract(lastPoint, offset1);
float[] leftPt2 = subtract(lastPoint, offset2);
float alpha = getIntersectionAlpha(rightPt1, v1, rightPt2, v2);
// if inside corner is right side
if (alpha <= 0) {
float[] rightInside = addScaled(rightPt1, v1, alpha);
addVertex(rightInside[0], rightInside[1]);
addVertex(leftPt1[0], leftPt1[1]);
addVertex(rightInside[0], rightInside[1]);
addVertex(leftPt2[0], leftPt2[1]);
} else {
// carry the math through and this turns out
alpha = -alpha;
float[] leftInside = addScaled(leftPt1, v1, alpha);
addVertex(rightPt1[0], rightPt1[1]);
addVertex(leftInside[0], leftInside[1]);
addVertex(rightPt2[0], rightPt2[1]);
addVertex(leftInside[0], leftInside[1]);
}
}
protected void drawCornerMiter(float[] secondLastPoint, float[] lastPoint, float[] point) {
float[] offset1 = lineOffset(secondLastPoint, lastPoint);
float[] offset2 = lineOffset(lastPoint, point);
float[] v1 = subtract(lastPoint, secondLastPoint);
normalize(v1);
float[] v2 = subtract(lastPoint, point);
normalize(v2);
float[] rightPt1 = add(lastPoint, offset1);
float[] rightPt2 = add(lastPoint, offset2);
float[] leftPt1 = subtract(lastPoint, offset1);
float alpha = getIntersectionAlpha(rightPt1, v1, rightPt2, v2);
float[] rightCorner = addScaled(rightPt1, v1, alpha);
// other side is just the negative alpha
alpha = -alpha;
float[] leftCorner = addScaled(leftPt1, v1, alpha);
// If we exceed the miter limit, draw beveled corner
float dist = distance(rightCorner, leftCorner);
if (dist > miterLimit * lineOffset * 2) {
drawCornerBevel(secondLastPoint, lastPoint, point);
} else {
addVertex(rightCorner[0], rightCorner[1]);
addVertex(leftCorner[0], leftCorner[1]);
}
}
protected float distance(float[] pt1, float[] pt2) {
double diffX = pt1[0] - pt2[0];
double diffY = pt1[1] - pt2[1];
double distSq = diffX * diffX + diffY * diffY;
return (float) sqrt(distSq);
}
protected float[] addScaled(float[] pt, float[] v, float alpha) {
return new float[] { pt[0] + v[0] * alpha, pt[1] + v[1] * alpha };
}
protected void normalize(float[] v) {
float norm = (float) sqrt(v[0] * v[0] + v[1] * v[1]);
v[0] /= norm;
v[1] /= norm;
}
protected float[] subtract(float[] pt1, float[] pt2) {
return new float[] { pt1[0] - pt2[0], pt1[1] - pt2[1] };
}
protected float[] add(float[] pt1, float[] pt2) {
return new float[] { pt2[0] + pt1[0], pt2[1] + pt1[1] };
}
protected float getIntersectionAlpha(float[] pt1, float[] v1, float[] pt2, float[] v2) {
float t = (pt2[0] - pt1[0]) * v2[1] - (pt2[1] - pt1[1]) * v2[0];
t /= v1[0] * v2[1] - v1[1] * v2[0];
return t;
}
protected float[] lineOffset(float[] linePoint1, float[] linePoint2) {
float[] vec = new float[2];
vec[0] = linePoint2[0] - linePoint1[0];
vec[1] = linePoint2[1] - linePoint1[1];
float norm = vec[0] * vec[0] + vec[1] * vec[1];
norm = (float) sqrt(norm);
float scale = lineOffset / norm;
float[] offset = new float[2];
offset[0] = vec[1] * scale;
offset[1] = -vec[0] * scale;
return offset;
}
protected float[] lineCorners(float[] linePoint1, float[] linePoint2, float[] vertex, float offset) {
float[] translated = new float[2];
translated[0] = linePoint2[0] - linePoint1[0];
translated[1] = linePoint2[1] - linePoint1[1];
float norm = translated[0] * translated[0] + translated[1] * translated[1];
norm = (float) sqrt(norm);
float scale = offset / norm;
float[] corners = new float[4];
corners[0] = translated[1] * scale + vertex[0];
corners[1] = -translated[0] * scale + vertex[1];
corners[2] = -translated[1] * scale + vertex[0];
corners[3] = translated[0] * scale + vertex[1];
return corners;
}
/**
* Finds the intersection of two lines. This method was written to reduce the
* number of array creations and so is quite dense. However, it is easy to
* understand the theory behind the computation. I found this at <a
* href="http://mathforum.org/library/drmath/view/62814.html"
* >http://mathforum.org/library/drmath/view/62814.html</a>.
*
* <p>
* We have two lines, specified by three points (P1, P2, P3). They share the
* second point. This gives us an easy way to represent the line in parametric
* form. For example the first line has the form
*
* <pre>
* <x, y> = <P1<sub>x</sub>, P1<sub>y</sub>> + t * <P2<sub>x</sub>-P1<sub>x</sub>, P2<sub>y</sub>-P1<sub>y</sub>>
* </pre>
*
* </p>
* <p>
* <code><P1<sub>x</sub>, P1<sub>y</sub>></code> is a point on the line,
* while
* <code><P2<sub>x</sub>-P1<sub>x</sub>, P2<sub>y</sub>-P1<sub>y</sub>></code>
* is the direction of the line. The method for solving for the intersection
* of these two parametric lines is straightforward. Let <code>o1</code> and
* <code>o2</code> be the points on the lines and <code>v1</code> and
* <code>v2</code> be the two direction vectors. Now we have
*
* <pre>
* p1 = o1 + t * v1
* p2 = o2 + s * v2
* </pre>
*
* We can solve to find the intersection by
*
* <pre>
* o1 + t * v1 = o2 + s * v2
* t * v1 = o2 - o1 + s * v2
* (t * v1) x v2 = (o2 - o1 + s * v2) x v2 ; cross product by v2
* t * (v1 x v2) = (o2 - o1) x v2 ; to get rid of s term
* </pre>
*
* Solving for <code>t</code> is easy since we only have the z component. Put
* <code>t</code> back into the first equation gives us our point of
* intersection.
* </p>
* <p>
* This method solves for <code>t</code>, but not directly for lines
* intersecting the point parameters. Since we're trying to use this for the
* miter corners, we want to solve for the intersections of the two outside
* edges of the lines that go from <code>secondLastPoint</code> to
* <code>lastPoint</code> and from <code>lastPoint</code> to
* <code>point</code>.
* </p>
*/
protected float[] getMiterIntersections(float[] secondLastPoint, float[] lastPoint, float[] point) {
float[] o1 = lineCorners(secondLastPoint, lastPoint, lastPoint, lineOffset);
float[] o2 = lineCorners(lastPoint, point, lastPoint, lineOffset);
float[] v1 = new float[2];
v1[0] = lastPoint[0] - secondLastPoint[0];
v1[1] = lastPoint[1] - secondLastPoint[1];
float[] v2 = new float[2];
v2[0] = lastPoint[0] - point[0];
v2[1] = lastPoint[1] - point[1];
float norm = (float) sqrt(v1[0] * v1[0] + v1[1] * v1[1]);
v1[0] /= norm;
v1[1] /= norm;
norm = (float) sqrt(v2[0] * v2[0] + v2[1] * v2[1]);
v2[0] /= norm;
v2[1] /= norm;
float[] intersections = new float[4];
float t = (o2[0] - o1[0]) * v2[1] - (o2[1] - o1[1]) * v2[0];
t /= v1[0] * v2[1] - v1[1] * v2[0];
intersections[0] = o1[0] + t * v1[0];
intersections[1] = o1[1] + t * v1[1];
t = (o2[2] - o1[2]) * v2[1] - (o2[3] - o1[3]) * v2[0];
t /= v1[0] * v2[1] - v1[1] * v2[0];
intersections[2] = o1[2] + t * v1[0];
intersections[3] = o1[3] + t * v1[1];
return intersections;
}
protected void applyEndCap(float[] point1, float[] point2, boolean first) {
switch (endCap) {
case BasicStroke.CAP_BUTT:
drawCapButt(point1, point2, first);
break;
case BasicStroke.CAP_SQUARE:
drawCapSquare(point1, point2, first);
break;
case BasicStroke.CAP_ROUND:
drawCapRound(point1, point2, first);
break;
}
}
protected void drawCapButt(float[] point1, float[] point2, boolean first) {
float[] offset = lineOffset(point1, point2);
float[] pt = first ? point1 : point2;
float[] cornerPt = add(pt, offset);
addVertex(cornerPt[0], cornerPt[1]);
cornerPt = subtract(pt, offset);
addVertex(cornerPt[0], cornerPt[1]);
}
protected void drawCapSquare(float[] point1, float[] point2, boolean first) {
float[] offset = lineOffset(point1, point2);
float[] offsetRotated;
float[] pt;
if (first) {
offsetRotated = new float[] { offset[1], -offset[0] };
pt = point1;
} else {
offsetRotated = new float[] { -offset[1], offset[0] };
pt = point2;
}
float[] cornerPt = add(add(pt, offset), offsetRotated);
addVertex(cornerPt[0], cornerPt[1]);
cornerPt = add(subtract(pt, offset), offsetRotated);
addVertex(cornerPt[0], cornerPt[1]);
}
protected void drawCapRound(float[] point1, float[] point2, boolean first) {
/*
* Instead of doing a triangle-fan around the cap, we're going to jump back
* and forth from the tip toward the body of the line.
*/
float[] offsetRight;
float[] offsetLeft;
float[] pt;
if (first) {
float[] v = subtract(point1, point2);
normalize(v);
v[0] *= lineOffset;
v[1] *= lineOffset;
offsetRight = v;
offsetLeft = new float[] { v[0], v[1] };
pt = point1;
} else {
offsetRight = lineOffset(point1, point2);
offsetLeft = new float[] { -offsetRight[0], -offsetRight[1] };
pt = point2;
}
int max = (int) ceil(Math.PI / 2 / THETA_STEP);
for (int i = 0; i < max; i++) {
addVertex(pt[0] + offsetRight[0], pt[1] + offsetRight[1]);
addVertex(pt[0] + offsetLeft[0], pt[1] + offsetLeft[1]);
float newX = COS_STEP * offsetRight[0] + -SIN_STEP * offsetRight[1];
offsetRight[1] = SIN_STEP * offsetRight[0] + COS_STEP * offsetRight[1];
offsetRight[0] = newX;
newX = COS_STEP * offsetLeft[0] + SIN_STEP * offsetLeft[1];
offsetLeft[1] = -SIN_STEP * offsetLeft[0] + COS_STEP * offsetLeft[1];
offsetLeft[0] = newX;
}
if (first) {
offsetRight = lineOffset(point1, point2);
addVertex(pt[0] + offsetRight[0], point1[1] + offsetRight[1]);
addVertex(pt[0] - offsetRight[0], point1[1] - offsetRight[1]);
} else {
float[] v = subtract(point2, point1);
normalize(v);
v[0] *= lineOffset;
v[1] *= lineOffset;
addVertex(pt[0] + v[0], pt[1] + v[1]);
}
}
protected void addVertex(float x, float y) {
vBuffer.addVertex(x, y);
}
protected abstract void drawBuffer();
}