/*
* @(#)DoubleStroke.java
*
* Copyright (c) 1996-2010 The authors and contributors of JHotDraw.
* You may not use, copy or modify this file, except in compliance with the
* accompanying license terms.
*/
package org.jhotdraw.geom;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.awt.*;
import java.awt.geom.*;
/**
* Draws a double stroke (an outline of an outline).
* The inner width of a DoubleStroke defines the distance between the two
* outlines being drawn. The outline width of a DoubleStroke defines the
* thickness of the outline.
*
* @author Werner Randelshofer
* @version $Id$
*/
public class DoubleStroke implements Stroke {
private BasicStroke outlineStroke;
private double innerWidth;
private double outlineWidth;
private double miterLimit;
private float[] dashes;
private float dashPhase;
public DoubleStroke(double innerWidth, double outlineWidth) {
this(innerWidth, outlineWidth, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL, 10f, null, 0f);
}
public DoubleStroke(double innerWidth, double outlineWidth, int cap, int join, double miterLimit, @Nullable float[] dashes, float dashPhase) {
this.innerWidth = innerWidth;
this.outlineWidth = outlineWidth;
this.miterLimit = miterLimit;
// outlineStroke = new BasicStroke(outlineWidth, cap, join, miterLimit, dashes, dashPhase);
outlineStroke = new BasicStroke((float) outlineWidth, cap, BasicStroke.JOIN_BEVEL, (float) miterLimit, dashes, dashPhase);
}
@Override
public Shape createStrokedShape(Shape s) {
BezierPath bp = new BezierPath();
Path2D.Double left = new Path2D.Double();
Path2D.Double right = new Path2D.Double();
double[] coords = new double[6];
// FIXME - We only do a flattened path
for (PathIterator i = s.getPathIterator(null, 0.1d); !i.isDone(); i.next()) {
int type = i.currentSegment(coords);
switch (type) {
case PathIterator.SEG_MOVETO:
if (bp.size() != 0) {
traceStroke(bp, left, right);
}
bp.clear();
bp.setClosed(false);
bp.moveTo(coords[0], coords[1]);
break;
case PathIterator.SEG_LINETO:
if (coords[0] != bp.get(bp.size() - 1).x[0]
|| coords[1] != bp.get(bp.size() - 1).y[0]) {
bp.lineTo(coords[0], coords[1]);
}
break;
case PathIterator.SEG_QUADTO:
bp.quadTo(coords[0], coords[1], coords[2], coords[3]);
break;
case PathIterator.SEG_CUBICTO:
bp.curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]);
break;
case PathIterator.SEG_CLOSE:
bp.setClosed(true);
break;
}
}
if (bp.size() != 0) {
traceStroke(bp, left, right);
}
// Note: This could be extended to use different stroke objects for
// the inner and the outher path.
right.append(left, false);
return outlineStroke.createStrokedShape(right);
}
protected void traceStroke(BezierPath bp, Path2D.Double left, Path2D.Double right) {
// XXX - We only support straight line segments here
// Corners of the current and the previous thick line
double[] currentCorners = new double[8];
double[] prevCorners = new double[8];
Point2D.Double intersect;
// Remove duplicate nodes from bezier path.
if (bp.isClosed()) {
BezierPath.Node prev = bp.get(bp.size() - 1);
for (int i = 0; i < bp.size(); i++) {
BezierPath.Node node = bp.get(i);
if (prev.x[0] == node.x[0] && prev.y[0] == node.y[0]) {
bp.remove(i--);
} else {
prev = node;
}
}
} else {
BezierPath.Node prev = bp.get(0);
for (int i = 1; i < bp.size(); i++) {
BezierPath.Node node = bp.get(i);
if (prev.x[0] == node.x[0] && prev.y[0] == node.y[0]) {
bp.remove(i--);
} else {
prev = node;
}
}
}
// Handle the first point of the bezier path
if (bp.isClosed() && bp.size() > 1) {
prevCorners = computeThickLine(
bp.get(bp.size() - 1).x[0], bp.get(bp.size() - 1).y[0],
bp.get(0).x[0], bp.get(0).y[0],
innerWidth, prevCorners);
currentCorners = computeThickLine(
bp.get(0).x[0], bp.get(0).y[0],
bp.get(1).x[0], bp.get(1).y[0],
innerWidth, currentCorners);
intersect = Geom.intersect(
prevCorners[0], prevCorners[1],
prevCorners[4], prevCorners[5],
currentCorners[0], currentCorners[1],
currentCorners[4], currentCorners[5], miterLimit);
if (intersect != null) {
right.moveTo(intersect.x, intersect.y);
} else {
right.moveTo(prevCorners[4], prevCorners[5]);
right.lineTo(currentCorners[0], currentCorners[1]);
}
intersect = Geom.intersect(
prevCorners[2], prevCorners[3],
prevCorners[6], prevCorners[7],
currentCorners[2], currentCorners[3],
currentCorners[6], currentCorners[7], miterLimit);
if (intersect != null) {
left.moveTo(intersect.x, intersect.y);
} else {
left.moveTo(prevCorners[6], prevCorners[7]);
left.lineTo(currentCorners[2], currentCorners[3]);
}
} else {
if (bp.size() > 1) {
currentCorners = computeThickLine(
bp.get(0).x[0], bp.get(0).y[0],
bp.get(1).x[0], bp.get(1).y[0],
innerWidth, currentCorners);
right.moveTo(currentCorners[0], currentCorners[1]);
left.moveTo(currentCorners[2], currentCorners[3]);
}
}
// Handle points in the middle of the bezier path
for (int i = 1, n = bp.size() - 1; i < n; i++) {
double[] tmp = prevCorners;
prevCorners = currentCorners;
currentCorners = computeThickLine(
bp.get(i).x[0], bp.get(i).y[0],
bp.get(i + 1).x[0], bp.get(i + 1).y[0],
innerWidth, tmp);
intersect = Geom.intersect(
prevCorners[0], prevCorners[1],
prevCorners[4], prevCorners[5],
currentCorners[0], currentCorners[1],
currentCorners[4], currentCorners[5], miterLimit);
if (intersect != null) {
right.lineTo(intersect.x, intersect.y);
} else {
right.lineTo(prevCorners[4], prevCorners[5]);
right.lineTo(currentCorners[0], currentCorners[1]);
}
intersect = Geom.intersect(
prevCorners[2], prevCorners[3],
prevCorners[6], prevCorners[7],
currentCorners[2], currentCorners[3],
currentCorners[6], currentCorners[7], miterLimit);
if (intersect != null) {
left.lineTo(intersect.x, intersect.y);
} else {
left.lineTo(prevCorners[6], prevCorners[7]);
left.lineTo(currentCorners[2], currentCorners[3]);
}
}
// Handle the last point of the bezier path
if (bp.isClosed() && bp.size() > 0) {
double[] tmp = prevCorners;
prevCorners = currentCorners;
currentCorners = computeThickLine(
bp.get(bp.size() - 1).x[0], bp.get(bp.size() - 1).y[0],
bp.get(0).x[0], bp.get(0).y[0],
//bp.get(1).x[0], bp.get(1).y[0],
innerWidth, tmp);
intersect = Geom.intersect(
prevCorners[0], prevCorners[1],
prevCorners[4], prevCorners[5],
currentCorners[0], currentCorners[1],
currentCorners[4], currentCorners[5], miterLimit);
if (intersect != null) {
right.lineTo(intersect.x, intersect.y);
} else {
right.lineTo(prevCorners[4], prevCorners[5]);
right.lineTo(currentCorners[0], currentCorners[1]);
}
intersect = Geom.intersect(
prevCorners[2], prevCorners[3],
prevCorners[6], prevCorners[7],
currentCorners[2], currentCorners[3],
currentCorners[6], currentCorners[7], miterLimit);
if (intersect != null) {
left.lineTo(intersect.x, intersect.y);
} else {
left.lineTo(prevCorners[6], prevCorners[7]);
left.lineTo(currentCorners[2], currentCorners[3]);
}
right.closePath();
left.closePath();
} else {
if (bp.size() > 1) {
right.lineTo(currentCorners[4], currentCorners[5]);
left.lineTo(currentCorners[6], currentCorners[7]);
}
}
}
private double[] computeThickLine(double[] seg, int offset, double corners[]) {
return computeThickLine(seg[0 + offset], seg[1 + offset], seg[2 + offset], seg[3 + offset], innerWidth, corners);
}
private double[] computeThickLine(double x1, double y1, double x2, double y2, double thickness, double corners[]) {
double dx = x2 - x1;
double dy = y2 - y1;
// line length
double lineLength = Math.sqrt(dx * dx + dy * dy);
double scale = thickness / (2d * lineLength);
// The x and y increments from an endpoint needed to create a rectangle...
double ddx = -scale * dy;
double ddy = scale * dx;
/*
ddx += (ddx > 0) ? 0.5 : -0.5;
ddy += (ddy > 0) ? 0.5 : -0.5;
*/
// Now we can compute the corner points...
corners[0] = x1 + ddx;
corners[1] = y1 + ddy;
corners[2] = x1 - ddx;
corners[3] = y1 - ddy;
corners[4] = x2 + ddx;
corners[5] = y2 + ddy;
corners[6] = x2 - ddx;
corners[7] = y2 - ddy;
return corners;
}
}