package org.joshy.sketch.model;
import org.joshy.gfx.draw.*;
import org.joshy.gfx.draw.Paint;
import org.joshy.gfx.node.Bounds;
import org.joshy.sketch.util.Util;
import java.awt.*;
import java.awt.geom.*;
import java.util.ArrayList;
import java.util.List;
/**
* Spath represents a path composed of line segments and curves. It may contain
* multiple subpaths by closing a path and doing a 'moveto' to start a new one.
* All points are represented by a pathpoint. A pathpoint with closeto=true means
* the current subpath is completed by going back to the previous moveto and the
* next subpath begins with a new moveto.
*
*
*
*/
public class SPath2 extends SShape implements SelfDrawable {
public List<PathPoint> points;
private boolean closed;
private Path2D.Double path2d;
private PathPoint lastMoveTo;
public SPath2() {
this.points = new ArrayList<PathPoint>();
}
@Override
public Bounds getBounds() {
if(path2d != null) {
Rectangle bds = path2d.getBounds();
return new Bounds(
bds.getX(),
bds.getY(),
bds.getWidth(),
bds.getHeight());
}
return new Bounds(0,0,100,100);
}
public Bounds getTransformedBounds() {
if(path2d != null) {
AffineTransform af = new AffineTransform();
af.translate(getTranslateX(),getTranslateY());
af.translate(getAnchorX(),getAnchorY());
af.rotate(Math.toRadians(getRotate()));
af.scale(getScaleX(), getScaleY());
af.translate(-getAnchorX(),-getAnchorY());
Shape sh = af.createTransformedShape(path2d);
Rectangle2D bds = sh.getBounds2D();
return Util.toBounds(bds);
}
return new Bounds(0,0,100,100);
}
@Override
public boolean contains(Point2D point) {
if(path2d != null) {
return path2d.contains(point.getX()-getTranslateX(),point.getY()-getTranslateY());
}
return getBounds().contains(point.getX(),point.getY());
}
public void addPoint(PathPoint point) {
this.points.add(point);
}
public void draw(GFX g) {
g.setPureStrokes(true);
drawShadow(g);
if(isClosed()) {
double opacity = -1;
Paint paint = getFillPaint();
if(paint != null) {
if(paint instanceof FlatColor) {
g.setPaint(((FlatColor)paint).deriveWithAlpha(getFillOpacity()));
}
if(paint instanceof MultiGradientFill) {
g.setPaint(paint);
}
if(paint instanceof PatternPaint) {
opacity = g.getOpacity();
g.setOpacity(getFillOpacity());
g.setPaint(paint);
}
fillShape(g);
}
if(opacity >=0) g.setOpacity(opacity);
}
if(getStrokeWidth() > 0 && getStrokePaint() != null) {
g.setPaint(getStrokePaint());
Path2D.Double pth = toPath(this);
g.setStrokeWidth(getStrokeWidth());
g.drawPath(pth);
g.setStrokeWidth(1);
}
g.setPureStrokes(false);
}
@Override
protected void fillShape(GFX g) {
Path2D.Double path = toPath(this);
g.fillPath(path);
}
public static Path2D.Double toPath(SPath2 node) {
Path2D.Double pth = new Path2D.Double();
//int last = node.points.size()-1;
for(int i=0; i<node.points.size(); i++) {
SPath2.PathPoint point = node.points.get(i);
//g.setPaint(FlatColor.BLACK);
if(point.startPath || i == 0) {
pth.moveTo(point.x, point.y);
continue;
}
SPath2.PathPoint prev = node.points.get(i - 1);
pth.curveTo(prev.cx2, prev.cy2,
point.cx1, point.cy1,
point.x, point.y
);
if(point.closePath) {
if(node.isClosed()) {
SPath2.PathPoint first = node.points.get(0);
pth.curveTo(point.cx2, point.cy2,
first.cx1, first.cy1,
first.x, first.y
);
pth.closePath();
}
}
}
return pth;
}
public Path2D.Double toTransformedPath() throws NoninvertibleTransformException {
Path2D.Double p2d = SPath2.toPath(this);
AffineTransform af = new AffineTransform();
af.translate(getTranslateX(),getTranslateY());
af.rotate(Math.toRadians(getRotate()));
af.scale(getScaleX(), getScaleY());
return new Path2D.Double(p2d,af.createInverse());
}
public void close(boolean closed) {
setClosed(closed);
recalcPath();
}
public void recalcPath() {
path2d = new Path2D.Double();
int last = points.size()-1;
for(int i=0; i<points.size(); i++) {
PathPoint point = points.get(i);
if(i == 0) {
path2d.moveTo(point.x,point.y);
continue;
}
PathPoint prev = points.get(i - 1);
path2d.curveTo(prev.cx2,prev.cy2,
point.cx1,point.cy1,
point.x,point.y
);
if(i == points.size()-1) {
path2d.closePath();
}
}
}
public void setClosed(boolean closed) {
this.closed = closed;
if(points.size() > 1) {
points.get(points.size()-1).closePath = closed;
}
}
public boolean isClosed() {
return closed;
}
public List<PathPoint> getPoints() {
return points;
}
public void setPoints(List<PathPoint> points) {
this.points = points;
recalcPath();
}
public Iterable<PathSegment> calculateSegments() {
List<PathSegment> segs = new ArrayList<PathSegment>();
for(int i=0; i<points.size()-1;i++) {
PathPoint curr = points.get(i);
PathPoint next = points.get(i+1);
segs.add(new PathSegment(curr,next,i));
}
if(isClosed()) {
int last = points.size()-1;
segs.add(new PathSegment(points.get(last),points.get(0),last));
}
return segs;
}
public PathPoint splitPath(PathTuple location) {
PathPoint a = location.a;
PathPoint b = location.b;
PathPoint c = new PathPoint(0,0);
double co[] = new double[14];
co[0] = a.x; co[1] = a.y;
co[2] = a.cx2; co[3] = a.cy2;
co[4] = b.cx1; co[5] = b.cy1;
co[6] = b.x; co[7] = b.y;
split(co,0,location.t);
a.x = co[0]; a.y = co[1];
a.cx2 = co[2]; a.cy2 = co[3];
c.cx1 = co[4]; c.cy1 = co[5];
c.x = co[6]; c.y = co[7];
c.cx2 = co[8]; c.cy2 = co[9];
b.cx1 = co[10]; b.cy1 = co[11];
b.x = co[12]; b.y = co[13];
points.add(location.index+1,c);
return c;
}
public void unSplitPath(PathTuple temp, PathPoint a, PathPoint b, PathPoint pt) {
temp.a.copyFrom(a);
temp.b.copyFrom(b);
points.remove(pt);
}
/*
* Split the cubic Bezier stored at coords[pos...pos+7] representing
* the parametric range [0..1] into two subcurves representing the
* parametric subranges [0..t] and [t..1]. Store the results back
* into the array at coords[pos...pos+7] and coords[pos+6...pos+13].
*/
public static void split(double coords[], int pos, double t) {
double x0, y0, cx0, cy0, cx1, cy1, x1, y1;
coords[pos+12] = x1 = coords[pos+6];
coords[pos+13] = y1 = coords[pos+7];
cx1 = coords[pos+4];
cy1 = coords[pos+5];
x1 = cx1 + (x1 - cx1) * t;
y1 = cy1 + (y1 - cy1) * t;
x0 = coords[pos+0];
y0 = coords[pos+1];
cx0 = coords[pos+2];
cy0 = coords[pos+3];
x0 = x0 + (cx0 - x0) * t;
y0 = y0 + (cy0 - y0) * t;
cx0 = cx0 + (cx1 - cx0) * t;
cy0 = cy0 + (cy1 - cy0) * t;
cx1 = cx0 + (x1 - cx0) * t;
cy1 = cy0 + (y1 - cy0) * t;
cx0 = x0 + (cx0 - x0) * t;
cy0 = y0 + (cy0 - y0) * t;
coords[pos+2] = x0;
coords[pos+3] = y0;
coords[pos+4] = cx0;
coords[pos+5] = cy0;
coords[pos+6] = cx0 + (cx1 - cx0) * t;
coords[pos+7] = cy0 + (cy1 - cy0) * t;
coords[pos+8] = cx1;
coords[pos+9] = cy1;
coords[pos+10] = x1;
coords[pos+11] = y1;
}
public void normalize() {
double minX = Double.MAX_VALUE;
double minY = Double.MAX_VALUE;
double maxX = Double.MIN_VALUE;
double maxY = Double.MIN_VALUE;
for(PathPoint pt : points) {
minX = Math.min(pt.x,minX);
minY = Math.min(pt.y,minY);
maxX = Math.max(pt.x,maxX);
maxY = Math.max(pt.y,maxY);
}
for(PathPoint pt : points) {
pt.x -= minX;
pt.y -= minY;
pt.cx1 -= minX;
pt.cx2 -= minX;
pt.cy1 -= minY;
pt.cy2 -= minY;
}
setTranslateX(getTranslateX() + minX);
setTranslateY(getTranslateY() + minY);
recalcPath();
setAnchorX((int) ((maxX - minX) / 2.0));
setAnchorY((int) ((maxY - minY) / 2.0));
}
public PathPoint moveTo(double x, double y) {
//u.p("move to: " + x + " " + y);
PathPoint p = new PathPoint(x, y);
p.startPath = true;
addPoint(p);
lastMoveTo = p;
return p;
}
public PathPoint lineTo(double x, double y) {
//u.p("line to: " + x + " " + y);
PathPoint p = new PathPoint(x, y);
addPoint(p);
return p;
}
public PathPoint curveTo(PathPoint prev, double x1, double y1, double x2, double y2, double x, double y) {
//u.p("curve to: " + x + " " + y);
PathPoint p = new PathPoint(x,y,x2,y2,x,y);
prev.cx2 = x1;
prev.cy2 = y1;
addPoint(p);
return p;
}
public PathPoint closeTo(PathPoint prev) {
//u.p("closing subpath");
prev.closePath = true;
prev.endPath = true;
close(true);
return prev;
}
public static SPath2 fromPathIterator(PathIterator it) {
SPath2 sPath = new SPath2();
PathPoint prev = null;
while(!it.isDone()) {
double[] coords = new double[6];
int n = it.currentSegment(coords);
if(n == PathIterator.SEG_MOVETO) {
prev = sPath.moveTo(coords[0],coords[1]);
}
if(n == PathIterator.SEG_LINETO) {
prev = sPath.lineTo(coords[0],coords[1]);
}
if(n == PathIterator.SEG_CUBICTO) {
prev = sPath.curveTo(prev, coords[0],coords[1],coords[2],coords[3],coords[4],coords[5]);
}
if(n == PathIterator.SEG_CLOSE) {
sPath.close(true);
break;
}
it.next();
}
return sPath;
}
public static class PathTuple {
public double distance;
public double t;
public Point2D.Double point;
public PathPoint a;
public PathPoint b;
public int index;
public PathTuple copy() {
PathTuple pt = new PathTuple();
pt.distance = distance;
pt.t = t;
pt.point = point;
pt.a = a;
pt.b = b;
pt.index = index;
return pt;
}
}
public static class PathSegment {
Point2D.Double p1;
Point2D.Double p2;
Point2D.Double p3;
Point2D.Double p4;
private PathPoint a;
private PathPoint b;
private int index;
public PathSegment(PathPoint curr, PathPoint next, int index) {
a = curr;
b = next;
this.index = index;
this.p1 = new Point2D.Double(curr.x,curr.y);
this.p2 = new Point2D.Double(curr.cx2,curr.cy2);
this.p3 = new Point2D.Double(next.cx1,next.cy1);
this.p4 = new Point2D.Double(next.x,next.y);
}
public PathTuple closestDistance(Point2D.Double point) {
Point2D.Double closest = calculatePoint(p1,p2,p3,p4,0);
double closestDistance = calculateDistance(point.getX(),point.getY(),closest);
double closestT = 0;
for(double t=0; t<=1.0; t+=0.01) {
Point2D.Double b = calculatePoint(p1,p2,p3,p4,t);
double distance = calculateDistance(point.getX(),point.getY(),b);
if(distance < closestDistance) {
closestDistance = distance;
closest = b;
closestT = t;
}
}
PathTuple tup = new PathTuple();
tup.t = closestT;
tup.point = closest;
tup.distance = closestDistance;
tup.a = a;
tup.b = b;
tup.index = index;
return tup;
}
private double calculateDistance(double x, double y, Point2D.Double b) {
double dx = x-b.x;
double dy = y-b.y;
double distance = Math.sqrt(dx*dx+dy*dy);
return distance;
}
private Point2D.Double calculatePoint(Point2D.Double p1, Point2D.Double p2, Point2D.Double p3, Point2D.Double p4, double mu) {
double mum1 = 1 - mu;
double mum13 = mum1 * mum1 * mum1;
double mu3 = mu*mu*mu;
Point2D.Double p = new Point2D.Double();
p.x = mum13*p1.x + 3*mu*mum1*mum1*p2.x + 3*mu*mu*mum1*p3.x + mu3*p4.x;
p.y = mum13*p1.y + 3*mu*mum1*mum1*p2.y + 3*mu*mu*mum1*p3.y + mu3*p4.y;
return p;
}
}
public static class PathPoint {
public double x;
public double y;
public boolean bound;
public double cx1;
public double cy1;
public double cx2;
public double cy2;
public boolean startPath = false;
public boolean endPath = false;
public boolean closePath = false;
public PathPoint(double x, double y, double cx1, double cy1, double cx2, double cy2) {
this.x = x;
this.y = y;
this.bound = false;
this.cx1 = cx1;
this.cy1 = cy1;
this.cx2 = cx2;
this.cy2 = cy2;
}
public PathPoint(double x, double y) {
this.x = x;
this.y = y;
this.cx1 = x;
this.cy1 = y;
this.cx2 = x;
this.cy2 = y;
}
public double distance(double x, double y) {
double x2 = this.x - x;
double y2 = this.y - y;
return Math.sqrt(x2*x2+y2*y2);
}
public PathPoint copy() {
PathPoint cp = new PathPoint(x, y, cx1, cy1, cx2, cy2);
cp.startPath = startPath;
cp.closePath = closePath;
return cp;
}
public void copyFrom(PathPoint a) {
this.x = a.x;
this.y = a.y;
this.cx1 = a.cx1;
this.cy1 = a.cy1;
this.cx2 = a.cx2;
this.cy2 = a.cy2;
this.startPath = a.startPath;
this.closePath = a.closePath;
}
}
@Override
public SNode duplicate(SNode dupe) {
if(dupe == null) {
dupe = new SPath2();
}
for(PathPoint point : this.getPoints()) {
((SPath2)dupe).addPoint(point.copy());
}
((SPath2)dupe).setClosed(this.isClosed());
((SPath2)dupe).recalcPath();
return super.duplicate(dupe);
}
@Override
public Area toArea() {
return new Area(transformShape(this.path2d));
}
}