package org.geogebra.common.util.clipper; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.geogebra.common.util.clipper.Clipper.ClipType; import org.geogebra.common.util.clipper.Clipper.EndType; import org.geogebra.common.util.clipper.Clipper.JoinType; import org.geogebra.common.util.clipper.Clipper.PolyFillType; import org.geogebra.common.util.clipper.Clipper.PolyType; import org.geogebra.common.util.clipper.Point.DoublePoint; public class ClipperOffset { private static boolean nearZero(double val) { return val > -TOLERANCE && val < TOLERANCE; } private Paths destPolys; private Path srcPoly; private Path destPoly; private final List<DoublePoint> normals; private double delta, inA, sin, cos; private double miterLim, stepsPerRad; // private LongPoint lowest; private DoublePoint lowest; private final PolyNode polyNodes; private final double arcTolerance; private final double miterLimit; private final static double TWO_PI = Math.PI * 2; private final static double DEFAULT_ARC_TOLERANCE = 0.25; private final static double TOLERANCE = 1.0E-20; public ClipperOffset() { this(2, DEFAULT_ARC_TOLERANCE); } /** * modified to be compatible with double */ public ClipperOffset(double miterLimit, double arcTolerance) { this.miterLimit = miterLimit; this.arcTolerance = arcTolerance; lowest = new DoublePoint(); lowest.setX(-1d); polyNodes = new PolyNode(); normals = new ArrayList<DoublePoint>(); } /** * modified to be compatible with double */ public void addPath(Path path, JoinType joinType, EndType endType) { int highI = path.size() - 1; if (highI < 0) { return; } final PolyNode newNode = new PolyNode(); newNode.setJoinType(joinType); newNode.setEndType(endType); // strip duplicate points from path and also get index to the lowest // point ... if (endType == EndType.CLOSED_LINE || endType == EndType.CLOSED_POLYGON) { while (highI > 0 && path.get(0) == path.get(highI)) { highI--; } } newNode.getPolygon().add(path.get(0)); int j = 0, k = 0; for (int i = 1; i <= highI; i++) { if (newNode.getPolygon().get(j) != path.get(i)) { j++; newNode.getPolygon().add(path.get(i)); if (path.get(i).getY() > newNode.getPolygon().get(k).getY() || path.get(i).getY() == newNode.getPolygon().get(k) .getY() && path.get(i).getX() < newNode.getPolygon() .get(k).getX()) { k = j; } } } if (endType == EndType.CLOSED_POLYGON && j < 2) { return; } polyNodes.addChild(newNode); // if this path's lowest pt is lower than all the others then update // m_lowest if (endType != EndType.CLOSED_POLYGON) { return; } if (lowest.getX() < 0) { lowest = new DoublePoint(polyNodes.getChildCount() - 1, k); } else { final DoublePoint ip = polyNodes.getChilds() .get((int) lowest.getX()).getPolygon() .get((int) lowest.getY()); if (newNode.getPolygon().get(k).getY() > ip.getY() || newNode.getPolygon().get(k).getY() == ip.getY() && newNode.getPolygon().get(k).getX() < ip.getX()) { lowest = new DoublePoint(polyNodes.getChildCount() - 1, k); } } } public void addPaths(Paths paths, JoinType joinType, EndType endType) { for (final Path p : paths) { addPath(p, joinType, endType); } } /** * modified to be compatible with double */ public void clear() { polyNodes.getChilds().clear(); lowest.setX(-1d); } /** * modified to be compatible with double */ private void doMiter(int j, int k, double r) { final double q = delta / r; destPoly.add(new DoublePoint( (srcPoly.get(j).getX() + (normals.get(k).getX() + normals.get(j).getX()) * q), (srcPoly.get(j).getY() + (normals.get(k).getY() + normals.get(j).getY()) * q))); } /** * modified to be compatible with double */ private void doOffset(double delta) { destPolys = new Paths(); this.delta = delta; // if Zero offset, just copy any CLOSED polygons to m_p and return ... if (nearZero(delta)) { for (int i = 0; i < polyNodes.getChildCount(); i++) { final PolyNode node = polyNodes.getChilds().get(i); if (node.getEndType() == EndType.CLOSED_POLYGON) { destPolys.add(node.getPolygon()); } } return; } // see offset_triginometry3.svg in the documentation folder ... if (miterLimit > 2) { miterLim = 2 / (miterLimit * miterLimit); } else { miterLim = 0.5; } double y; if (arcTolerance <= 0.0) { y = DEFAULT_ARC_TOLERANCE; } else if (arcTolerance > Math.abs(delta) * DEFAULT_ARC_TOLERANCE) { y = Math.abs(delta) * DEFAULT_ARC_TOLERANCE; } else { y = arcTolerance; } // see offset_triginometry2.svg in the documentation folder ... final double steps = Math.PI / Math.acos(1 - y / Math.abs(delta)); sin = Math.sin(TWO_PI / steps); cos = Math.cos(TWO_PI / steps); stepsPerRad = steps / TWO_PI; if (delta < 0.0) { sin = -sin; } for (int i = 0; i < polyNodes.getChildCount(); i++) { final PolyNode node = polyNodes.getChilds().get(i); srcPoly = node.getPolygon(); final int len = srcPoly.size(); if (len == 0 || delta <= 0 && (len < 3 || node.getEndType() != EndType.CLOSED_POLYGON)) { continue; } destPoly = new Path(); if (len == 1) { if (node.getJoinType() == JoinType.ROUND) { double X = 1.0, Y = 0.0; for (int j = 1; j <= steps; j++) { destPoly.add(new DoublePoint( (srcPoly.get(0).getX() + X * delta), (srcPoly.get(0).getY() + Y * delta))); final double X2 = X; X = X * cos - sin * Y; Y = X2 * sin + Y * cos; } } else { double X = -1.0, Y = -1.0; for (int j = 0; j < 4; ++j) { destPoly.add(new DoublePoint( (srcPoly.get(0).getX() + X * delta), (srcPoly.get(0).getY() + Y * delta))); if (X < 0) { X = 1; } else if (Y < 0) { Y = 1; } else { X = -1; } } } destPolys.add(destPoly); continue; } // build m_normals ... normals.clear(); for (int j = 0; j < len - 1; j++) { normals.add(Point.getUnitNormal(srcPoly.get(j), srcPoly.get(j + 1))); } if (node.getEndType() == EndType.CLOSED_LINE || node.getEndType() == EndType.CLOSED_POLYGON) { normals.add(Point.getUnitNormal(srcPoly.get(len - 1), srcPoly.get(0))); } else { normals.add(new DoublePoint(normals.get(len - 2))); } if (node.getEndType() == EndType.CLOSED_POLYGON) { final int[] k = new int[] { len - 1 }; for (int j = 0; j < len; j++) { offsetPoint(j, k, node.getJoinType()); } destPolys.add(destPoly); } else if (node.getEndType() == EndType.CLOSED_LINE) { final int[] k = new int[] { len - 1 }; for (int j = 0; j < len; j++) { offsetPoint(j, k, node.getJoinType()); } destPolys.add(destPoly); destPoly = new Path(); // re-build m_normals ... final DoublePoint n = normals.get(len - 1); for (int j = len - 1; j > 0; j--) { normals.set(j, new DoublePoint(-normals.get(j - 1).getX(), -normals.get(j - 1).getY())); } normals.set(0, new DoublePoint(-n.getX(), -n.getY(), 0)); k[0] = 0; for (int j = len - 1; j >= 0; j--) { offsetPoint(j, k, node.getJoinType()); } destPolys.add(destPoly); } else { final int[] k = new int[1]; for (int j = 1; j < len - 1; ++j) { offsetPoint(j, k, node.getJoinType()); } DoublePoint pt1; if (node.getEndType() == EndType.OPEN_BUTT) { final int j = len - 1; pt1 = new DoublePoint( (srcPoly.get(j).getX() + normals.get(j).getX() * delta), (srcPoly.get(j).getY() + normals.get(j).getY() * delta), 0); destPoly.add(pt1); pt1 = new DoublePoint( (srcPoly.get(j).getX() - normals.get(j).getX() * delta), (srcPoly.get(j).getY() - normals.get(j).getY() * delta), 0); destPoly.add(pt1); } else { final int j = len - 1; k[0] = len - 2; inA = 0; normals.set(j, new DoublePoint(-normals.get(j).getX(), -normals.get(j).getY())); if (node.getEndType() == EndType.OPEN_SQUARE) { doSquare(j, k[0]); } else { doRound(j, k[0]); } } // re-build m_normals ... for (int j = len - 1; j > 0; j--) { normals.set(j, new DoublePoint(-normals.get(j - 1).getX(), -normals.get(j - 1).getY())); } normals.set(0, new DoublePoint(-normals.get(1).getX(), -normals.get(1).getY())); k[0] = len - 1; for (int j = k[0] - 1; j > 0; --j) { offsetPoint(j, k, node.getJoinType()); } if (node.getEndType() == EndType.OPEN_BUTT) { pt1 = new DoublePoint( (srcPoly.get(0).getX() - normals.get(0).getX() * delta), (srcPoly.get(0).getY() - normals.get(0).getY() * delta)); destPoly.add(pt1); pt1 = new DoublePoint( (srcPoly.get(0).getX() + normals.get(0).getX() * delta), (srcPoly.get(0).getY() + normals.get(0).getY() * delta)); destPoly.add(pt1); } else { k[0] = 1; inA = 0; if (node.getEndType() == EndType.OPEN_SQUARE) { doSquare(0, 1); } else { doRound(0, 1); } } destPolys.add(destPoly); } } } /** * modified to be compatible with double */ private void doRound(int j, int k) { final double a = Math.atan2(inA, normals.get(k).getX() * normals.get(j).getX() + normals.get(k).getY() * normals.get(j).getY()); final int steps = Math.max((int) Math.round(stepsPerRad * Math.abs(a)), 1); double X = normals.get(k).getX(), Y = normals.get(k).getY(), X2; for (int i = 0; i < steps; ++i) { destPoly.add(new DoublePoint((srcPoly.get(j).getX() + X * delta), (srcPoly.get(j).getY() + Y * delta))); X2 = X; X = X * cos - sin * Y; Y = X2 * sin + Y * cos; } destPoly.add(new DoublePoint( (srcPoly.get(j).getX() + normals.get(j).getX() * delta), (srcPoly.get(j).getY() + normals.get(j).getY() * delta))); } private void doSquare(int j, int k) { final double nkx = normals.get(k).getX(); final double nky = normals.get(k).getY(); final double njx = normals.get(j).getX(); final double njy = normals.get(j).getY(); final double sjx = srcPoly.get(j).getX(); final double sjy = srcPoly.get(j).getY(); final double dx = Math.tan(Math.atan2(inA, nkx * njx + nky * njy) / 4); destPoly.add(new DoublePoint((sjx + delta * (nkx - nky * dx)), (sjy + delta * (nky + nkx * dx)), 0)); destPoly.add(new DoublePoint((sjx + delta * (njx + njy * dx)), (sjy + delta * (njy - njx * dx)), 0)); } // ------------------------------------------------------------------------------ /** * modified to be compatible with double */ public void execute(Paths solution, double delta) { solution.clear(); fixOrientations(); doOffset(delta); // now clean up 'corners' ... final DefaultClipper clpr = new DefaultClipper( Clipper.REVERSE_SOLUTION); clpr.addPaths(destPolys, PolyType.SUBJECT, true); if (delta > 0) { clpr.execute(ClipType.UNION, solution, PolyFillType.POSITIVE, PolyFillType.POSITIVE); } else { final DoubleRect r = destPolys.getBounds(); final Path outer = new Path(4); outer.add(new DoublePoint(r.left - 10, r.bottom + 10, 0)); outer.add(new DoublePoint(r.right + 10, r.bottom + 10, 0)); outer.add(new DoublePoint(r.right + 10, r.top - 10, 0)); outer.add(new DoublePoint(r.left - 10, r.top - 10, 0)); clpr.addPath(outer, PolyType.SUBJECT, true); clpr.execute(ClipType.UNION, solution, PolyFillType.NEGATIVE, PolyFillType.NEGATIVE); if (solution.size() > 0) { solution.remove(0); } } } // ------------------------------------------------------------------------------ /** * modified to be compatible with double */ public void execute(PolyTree solution, double delta) { solution.Clear(); fixOrientations(); doOffset(delta); // now clean up 'corners' ... final DefaultClipper clpr = new DefaultClipper( Clipper.REVERSE_SOLUTION); clpr.addPaths(destPolys, PolyType.SUBJECT, true); if (delta > 0) { clpr.execute(ClipType.UNION, solution, PolyFillType.POSITIVE, PolyFillType.POSITIVE); } else { final DoubleRect r = destPolys.getBounds(); final Path outer = new Path(4); outer.add(new DoublePoint(r.left - 10, r.bottom + 10, 0)); outer.add(new DoublePoint(r.right + 10, r.bottom + 10, 0)); outer.add(new DoublePoint(r.right + 10, r.top - 10, 0)); outer.add(new DoublePoint(r.left - 10, r.top - 10, 0)); clpr.addPath(outer, PolyType.SUBJECT, true); clpr.execute(ClipType.UNION, solution, PolyFillType.NEGATIVE, PolyFillType.NEGATIVE); // remove the outer PolyNode rectangle ... if (solution.getChildCount() == 1 && solution.getChilds().get(0).getChildCount() > 0) { final PolyNode outerNode = solution.getChilds().get(0); solution.getChilds().set(0, outerNode.getChilds().get(0)); solution.getChilds().get(0).setParent(solution); for (int i = 1; i < outerNode.getChildCount(); i++) { solution.addChild(outerNode.getChilds().get(i)); } } else { solution.Clear(); } } } // ------------------------------------------------------------------------------ private void fixOrientations() { // fixup orientations of all closed paths if the orientation of the // closed path with the lowermost vertex is wrong ... if (lowest.getX() >= 0 && !polyNodes.childs.get((int) lowest.getX()) .getPolygon().orientation()) { for (int i = 0; i < polyNodes.getChildCount(); i++) { final PolyNode node = polyNodes.childs.get(i); if (node.getEndType() == EndType.CLOSED_POLYGON || node.getEndType() == EndType.CLOSED_LINE && node.getPolygon().orientation()) { Collections.reverse(node.getPolygon()); } } } else { for (int i = 0; i < polyNodes.getChildCount(); i++) { final PolyNode node = polyNodes.childs.get(i); if (node.getEndType() == EndType.CLOSED_LINE && !node.getPolygon().orientation()) { Collections.reverse(node.getPolygon()); } } } } private void offsetPoint(int j, int[] kV, JoinType jointype) { // cross product ... final int k = kV[0]; final double nkx = normals.get(k).getX(); final double nky = normals.get(k).getY(); final double njy = normals.get(j).getY(); final double njx = normals.get(j).getX(); final double sjx = srcPoly.get(j).getX(); final double sjy = srcPoly.get(j).getY(); inA = nkx * njy - njx * nky; if (Math.abs(inA * delta) < 1.0) { // dot product ... final double cosA = nkx * njx + njy * nky; if (cosA > 0) // angle ==> 0 degrees { destPoly.add(new DoublePoint((sjx + nkx * delta), (sjy + nky * delta), 0)); return; } // else angle ==> 180 degrees } else if (inA > 1.0) { inA = 1.0; } else if (inA < -1.0) { inA = -1.0; } if (inA * delta < 0) { destPoly.add( new DoublePoint((sjx + nkx * delta), (sjy + nky * delta))); destPoly.add(srcPoly.get(j)); destPoly.add( new DoublePoint((sjx + njx * delta), (sjy + njy * delta))); } else { switch (jointype) { case MITER: { final double r = 1 + njx * nkx + njy * nky; if (r >= miterLim) { doMiter(j, k, r); } else { doSquare(j, k); } break; } case SQUARE: doSquare(j, k); break; case ROUND: doRound(j, k); break; } } kV[0] = j; } // ------------------------------------------------------------------------------ }