/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package edu.mit.csail.sdg.alloy4graph;
import java.util.ArrayList;
import java.util.List;
import java.awt.geom.CubicCurve2D;
/** Mutable; represents a connected path.
*
* <p><b>Thread Safety:</b> Can be called only by the AWT event thread.
*/
final class Curve {
/** starting position and ending position. */
public double startX, startY, endX, endY;
/** The list of segments in this curve. */
public final List<CubicCurve2D.Double> list = new ArrayList<CubicCurve2D.Double>();
/** Construct a curve starting at the given location. */
public Curve(double startX, double startY) {
this.startX=startX; this.endX=startX;
this.startY=startY; this.endY=startY;
}
/** Make a deep copy of this Curve object. */
public Curve dup() {
Curve ans = new Curve(startX, startY);
ans.endX = endX;
ans.endY = endY;
for(CubicCurve2D.Double x:list) {
CubicCurve2D.Double c = new CubicCurve2D.Double();
c.setCurve(x);
ans.list.add(c);
}
return ans;
}
/** Precondition: this.lastX==next.firstX and this.lastY==next.firstY.
* Note: the resulting Curve will still share the same CubicCurve2D objects as this and that. */
Curve join(Curve that) {
Curve ans = new Curve(startX, startY);
ans.list.addAll(list);
ans.list.addAll(that.list);
ans.endX=that.endX;
ans.endY=that.endY;
return ans;
}
/** Produce a cubic bezier curve representing a straightline segment from (x1,y1) to (x2,y2) */
private static CubicCurve2D.Double makeline(double x1, double y1, double x2, double y2) {
return new CubicCurve2D.Double(x1, y1, (x2-x1)*0.3+x1, (y2-y1)*0.3+y1, (x2-x1)*0.6+x1, (y2-y1)*0.6+y1, x2, y2);
}
/** Add a straightline segment to (ax,ay) */
public Curve lineTo(double ax, double ay) {
list.add(makeline(endX, endY, ax, ay));
this.endX = ax;
this.endY = ay;
return this;
}
/** Add a cubic bezier segment to (cx,cy) using (ax,ay) and (bx,by) as the two control points. */
public Curve cubicTo(double ax, double ay, double bx, double by, double cx, double cy) {
list.add(new CubicCurve2D.Double(endX, endY, ax, ay, bx, by, cx, cy));
this.endX = cx;
this.endY = cy;
return this;
}
/** Returns true if (x1,y1)..(x2,y2) intersects (x,y3)..(x,y4) */
private static boolean intersects(double x1, double y1, double x2, double y2, double x, double y3, double y4) {
if (!(y3<y4)) { double tmp=y3; y3=y4; y4=tmp; }
if (!((x1<=x && x<=x2) || (x2<=x && x<=x1))) return false;
double m = (y2-y1)/(x2-x1);
double i = (x-x1)*m + y1;
if ((y3<=i && i<=y4) || (y4<=i && i<=y3)) return true; else return false;
}
void bendUp(double x, double y1, double y2, double gap) {
for(int i=0; i<list.size(); i++) {
CubicCurve2D.Double c=list.get(i);
if (intersects(c.x1, c.y1, c.x2, c.y2, x, y1, y2)) {
list.set(i, makeline(c.x1, c.y1, x, y1-gap));
list.add(i+1, makeline(x, y1-gap, c.x2, c.y2));
return;
}
}
}
void bendDown(double x, double y1, double y2, double gap) {
for(int i=0; i<list.size(); i++) {
CubicCurve2D.Double c=list.get(i);
if (intersects(c.x1, c.y1, c.x2, c.y2, x, y1, y2)) {
list.set(i, makeline(c.x1, c.y1, x, y2+gap));
list.add(i+1, makeline(x, y2+gap, c.x2, c.y2));
return;
}
}
}
/** Chop the start of this curve. */
public void chopStart(double t) {
int n=list.size();
t=t*n;
double di=StrictMath.floor(t);
int i=(int)di;
//
if (i<0) return;
if (i>=n) list.clear();
while(i>0 && !list.isEmpty()) { list.remove(0); i--; }
if (list.isEmpty()) { list.add(new CubicCurve2D.Double(startX=endX, startY=endY, endX,endY, endX,endY, endX,endY)); return; }
CubicCurve2D.Double tmp=new CubicCurve2D.Double();
divide(t-di, list.get(0), new CubicCurve2D.Double(), tmp);
list.get(0).setCurve(tmp);
startX=tmp.x1;
startY=tmp.y1;
}
/** Chop the end of this curve. */
public void chopEnd(double t) {
int n=list.size();
t=t*n;
double di=StrictMath.floor(t);
int i=(int)di;
//
if (i<0) list.clear();
if (i>=n) return;
while(i+1<list.size()) list.remove(i+1);
if (list.isEmpty()) { endX=startX; endY=startY; list.add(new CubicCurve2D.Double(endX,endY, endX,endY, endX,endY, endX,endY)); return; }
CubicCurve2D.Double tmp=new CubicCurve2D.Double();
divide(t-di, list.get(i), tmp, new CubicCurve2D.Double());
list.get(i).setCurve(tmp);
endX=tmp.x2;
endY=tmp.y2;
}
/** Returns the x position at the given y position, or defaultValue if the line doesn't pass thru the given y position.
* <p> Precondition: the curve is monotonic in the vertical direction.
*/
public double getXatY(double y, double startT, double endT, double defaultValue) {
double a=startT, b=endT, ay=getY(a), by=getY(b);
if (ay>by) { double tmp=ay; ay=by; by=tmp; a=endT; b=startT; }
if (!(ay<=y && y<=by)) return defaultValue;
while(StrictMath.abs(a-b)>0.001D) {
double t=(a+b)/2, ty=getY(t);
if (ty==y) {a=t;break;} else if (ty<y) {a=t;} else {b=t;}
}
return getX(a);
}
/** Returns the x position at the given point 0 <= t <= 1 */
public double getX(double t) {
int n=list.size();
t=t*n;
double di=StrictMath.floor(t);
int i=(int)di;
if (i<0) return startX; else if (i>=n) return endX; else return getX(list.get(i), t-di);
}
/** Returns the y position at the given point 0 <= t <= 1 */
public double getY(double t) {
int n=list.size();
t=t*n;
double di=StrictMath.floor(t);
int i=(int)di;
if (i<0) return startY; else if (i>=n) return endY; else return getY(list.get(i), t-di);
}
/** Returns the x position of the given segment at the given point 0 <= t <= 1 */
private double getX(CubicCurve2D.Double curve, double t) {
double px=(curve.ctrlx1-curve.x1)*t+curve.x1, qx=(curve.ctrlx2-curve.ctrlx1)*t+curve.ctrlx1, rx=(curve.x2-curve.ctrlx2)*t+curve.ctrlx2;
double sx=(qx-px)*t+px, tx=(rx-qx)*t+qx;
return (tx-sx)*t+sx;
}
/** Returns the y position of the given segment at the given point 0 <= t <= 1 */
private double getY(CubicCurve2D.Double curve, double t) {
double py=(curve.ctrly1-curve.y1)*t+curve.y1, qy=(curve.ctrly2-curve.ctrly1)*t+curve.ctrly1, ry=(curve.y2-curve.ctrly2)*t+curve.ctrly2;
double sy=(qy-py)*t+py, ty=(ry-qy)*t+qy;
return (ty-sy)*t+sy;
}
/** Given 0<=t<=1 and an existing curve, divide it into two chunks and store the two chunks into "first" and "second" */
public static void divide(double t, CubicCurve2D.Double curve, CubicCurve2D.Double first, CubicCurve2D.Double second) {
// This algorithm uses de Casteljau's algorithm for chopping one bezier curve into two bezier curves
first.x1 = curve.x1;
second.x2 = curve.x2;
first.ctrlx1 = (1-t)*curve.x1 + t*curve.ctrlx1;
double x = (1-t)*curve.ctrlx1 + t*curve.ctrlx2;
second.ctrlx2 = (1-t)*curve.ctrlx2 + t*curve.x2;
first.ctrlx2 = (1-t)*first.ctrlx1 + t*x;
second.ctrlx1 = (1-t)*x + t*second.ctrlx2;
second.x1 = first.x2 = (1-t)*first.ctrlx2 + t*second.ctrlx1;
// now that we've computed the x coordinates, we now compute the y coordinates
first.y1 = curve.y1;
second.y2 = curve.y2;
first.ctrly1 = (1-t)*curve.y1 + t*curve.ctrly1;
double y = (1-t)*curve.ctrly1 + t*curve.ctrly2;
second.ctrly2 = (1-t)*curve.ctrly2 + t*curve.y2;
first.ctrly2 = (1-t)*first.ctrly1 + t*y;
second.ctrly1 = (1-t)*y + t*second.ctrly2;
second.y1 = first.y2 = (1-t)*first.ctrly2 + t*second.ctrly1;
}
}