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.gfx.util.u;
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 SPath extends SShape implements SelfDrawable {
private Path2D.Double path2d;
private List<SPath.SubPath> subPaths = new ArrayList<SPath.SubPath>();
private SubPath currentSubPath;
public SPath() {
currentSubPath = new SubPath();
subPaths.add(currentSubPath);
}
@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 draw(GFX g) {
g.setPureStrokes(true);
drawShadow(g);
if(anyClosed()) {
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);
}
public boolean anyClosed() {
for(SubPath sub : subPaths) {
if(sub.closed()) return true;
}
return false;
}
@Override
protected void fillShape(GFX g) {
Path2D.Double path = toPath(this);
g.fillPath(path);
}
public static Path2D.Double toPath(SubPath sub) {
Path2D.Double path = new Path2D.Double();
PathPoint first = null;
PathPoint prev = null;
for(PathPoint point : sub.points) {
if(prev == null) {
first = point;
path.moveTo(point.x,point.y);
} else {
path.curveTo(prev.cx2,prev.cy2,point.cx1,point.cy1,point.x,point.y);
}
prev = point;
}
if(sub.closed()) {
path.curveTo(prev.cx2,prev.cy2,first.cx1,first.cy1,first.x,first.y);
path.closePath();
}
return path;
}
/*
there are three possible states
completely open
closed using an auto-close
closed manually.
when created in the editor it will be closed manually
*/
public static Path2D.Double toPath(SPath node) {
Path2D.Double path = new Path2D.Double();
for(SubPath sub : node.subPaths) {
PathPoint first = null;
PathPoint prev = null;
for(PathPoint point : sub.points) {
//first
if(prev == null) {
first = point;
path.moveTo(point.x,point.y);
} else {
path.curveTo(prev.cx2,prev.cy2,point.cx1,point.cy1,point.x,point.y);
}
prev = point;
}
if(sub.closed()) {
path.curveTo(prev.cx2,prev.cy2,first.cx1,first.cy1,first.x,first.y);
path.closePath();
}
}
return path;
}
public Path2D.Double toTransformedPath() throws NoninvertibleTransformException {
Path2D.Double p2d = SPath.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 recalcPath() {
this.path2d = toPath(this);
}
/*
* 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 : allPoints()) {
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 : allPoints()) {
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));
}
private Iterable<PathPoint> allPoints() {
List<PathPoint> pts = new ArrayList<PathPoint>();
for(SubPath sub : getSubPaths()) {
pts.addAll(sub.getPoints());
}
return pts;
}
public PathPoint moveTo(double x, double y) {
if(currentSubPath.closed()) {
currentSubPath = new SubPath();
subPaths.add(currentSubPath);
}
PathPoint p = new PathPoint(x, y);
p.startPath = true;
currentSubPath.addPoint(p);
return p;
}
public PathPoint lineTo(double x, double y) {
//u.p("line to: " + x + " " + y);
PathPoint p = new PathPoint(x, y);
currentSubPath.addPoint(p);
return p;
}
/**
*
* @param prev the previous point
* @param x1 left control point x
* @param y1 left control point y
* @param x2 right control point x
* @param y2 right control point y
* @param x final x
* @param y final y
* @return
*/
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;
currentSubPath.addPoint(p);
return p;
}
public static SPath fromPathIterator(PathIterator it) {
SPath sPath = new SPath();
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();
}
it.next();
}
sPath.recalcPath();
return sPath;
}
private static void print(int n, double[] coords) {
switch(n) {
case PathIterator.SEG_MOVETO:
u.p("moveto " + coords[0] + " " + coords[1]);
return;
case PathIterator.SEG_LINETO:
u.p("lineto " + coords[0] + " " + coords[1]);
return;
case PathIterator.SEG_CUBICTO:
u.p("cubic to " + coords[0] + " " + coords[1] + " - " + coords[2] + " " + coords[3] + " - " + coords[4] + " " + coords[5]);
return;
case PathIterator.SEG_CLOSE:
u.p("close");
return;
default:
u.p("unknown type: " + n);
}
}
public List<SubPath> getSubPaths() {
return subPaths;
}
public void close() {
this.currentSubPath.autoClosed = true;
}
public void addPoint(PathPoint currentPoint) {
this.currentSubPath.addPoint(currentPoint);
}
public void newSubPath() {
this.currentSubPath = new SubPath();
this.subPaths.add(this.currentSubPath);
}
public void dump() {
u.p("SPath: ");
for(SubPath sub : getSubPaths()) {
u.p(" sub: closed = " + sub.autoClosed + ", size = " + sub.size());
for(PathPoint pt : sub.getPoints()) {
u.p(" pt " + pt.x + " " + pt.y );
}
}
}
public static class SubPath {
private List<PathPoint> points = new ArrayList<PathPoint>();
private boolean autoClosed;
public int size() {
return points.size();
}
public void addPoint(PathPoint point) {
this.points.add(point);
}
public boolean closed() {
return autoClosed;
}
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);
}
public SubPath copy() {
SubPath dupe = new SubPath();
for(PathPoint point : this.points) {
dupe.addPoint(point.copy());
}
dupe.autoClosed = this.autoClosed;
return dupe;
}
public PathPoint getPoint(int i) {
return points.get(i);
}
public List<PathPoint> getPoints() {
return points;
}
public void removePoint(PathPoint hoverPoint) {
this.points.remove(hoverPoint);
}
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(closed()) {
int last = points.size()-1;
segs.add(new PathSegment(points.get(last),points.get(0),last));
}
return segs;
}
public void doAutoclose() {
this.autoClosed = true;
}
}
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 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 SPath();
}
for(SubPath sub : this.subPaths) {
((SPath)dupe).addSubPath(sub.copy());
}
((SPath)dupe).recalcPath();
return super.duplicate(dupe);
}
private void addSubPath(SubPath copy) {
this.subPaths.add(copy);
}
@Override
public Area toArea() {
return new Area(transformShape(this.path2d));
}
}