/* * @(#)PathSegment.java * * $Date: 2014-03-13 09:15:48 +0100 (Cs, 13 márc. 2014) $ * * Copyright (c) 2011 by Jeremy Wood. * All rights reserved. * * The copyright of this software is owned by Jeremy Wood. * You may not use, copy or modify this software, except in * accordance with the license agreement you entered into with * Jeremy Wood. For details see accompanying license terms. * * This software is probably, but not necessarily, discussed here: * https://javagraphics.java.net/ * * That site should also contain the most recent official version * of this software. (See the SVN repository for more details.) */ package com.bric.geom; import net.jafama.FastMath; import java.awt.geom.AffineTransform; import java.awt.geom.PathIterator; public abstract class PathSegment { public int type; /** This maps the type integer to a human-readable string. * This is intended for debugging and exceptions. * * @param type one of the PathIterator "SEG" constants. * @return a string such as "SEG_CLOSE" or "SEG_LINETO". */ public static final String toTypeName(int type) { if(type==PathIterator.SEG_CLOSE) return "SEG_CLOSE"; if(type==PathIterator.SEG_CUBICTO) return "SEG_CUBICTO"; if(type==PathIterator.SEG_LINETO) return "SEG_LINETO"; if(type==PathIterator.SEG_MOVETO) return "SEG_MOVETO"; if(type==PathIterator.SEG_QUADTO) return "SEG_QUADTO"; return "UNKNOWN"; } public abstract void rotate(float f); public static class Float extends PathSegment { public float data[]; public PathSegment.Float next; public PathSegment.Float prev; private float[] xCoeffs; private float[] yCoeffs; /** A very small value that is considered "equivalent to zero" * in some operations where machine error may occur. */ protected static final float ZERO = .01f; /** Initializes a SEG_MOVETO segment. * * This is the only publicly available constructor: to * add other segments you should use the methods lineTo, * cubicTo, etc. * * @param moveX * @param moveY */ public Float(float moveX,float moveY) { type = PathIterator.SEG_MOVETO; data = new float[] { moveX, moveY }; } /** This constructor does not initialize anything. */ protected Float() {} /** Subclasses must override this method to return * a segment of the correct type. */ protected Float newSegment() { return new Float(); } public float[] getXCoeffs() { return getXCoeffs(null); } public float[] getXCoeffs(AffineTransform transform) { if(prev==null) { System.err.println(this); throw new NullPointerException("prev was null"); } if(prev.data==null) { System.err.println(this); throw new NullPointerException("prev.data was null"); } if(transform!=null && transform.isIdentity()) transform = null; if(xCoeffs!=null && transform==null) return xCoeffs; double[] last = new double[] { prev.data[prev.data.length-2], prev.data[prev.data.length-1] }; if(transform!=null) { transform.transform(last, 0, last, 0, 1); } float[] myData; if(transform!=null) { myData = new float[data.length]; transform.transform(data,0,myData,0,data.length/2); } else { myData = data; } float[] rValue = null; if(type==PathIterator.SEG_CUBICTO) { rValue = new float[4]; rValue[0] = (float)(-last[0] + 3 * myData[0] - 3 * myData[2] + myData[4]); rValue[1] = (float)(3 * last[0] - 6 * myData[0] + 3 * myData[2]); rValue[2] = (float)(-3 * last[0] + 3 * myData[0]); rValue[3] = (float)(last[0]); } else if(type==PathIterator.SEG_QUADTO) { rValue = new float[3]; rValue[0] = (float)(last[0] - 2 * myData[0] + myData[2]); rValue[1] = (float)(-2 * last[0] + 2 * myData[0]); rValue[2] = (float)(last[0]); } else if(type==PathIterator.SEG_LINETO) { rValue = new float[2]; rValue[0] = (float)(-last[0]+myData[0]); rValue[1] = (float)(last[0]); } else if(type==PathIterator.SEG_MOVETO) { throw new UnsupportedOperationException("MOVETO segments cannot be broken down into parametric equations."); } else if(type==PathIterator.SEG_CLOSE) { throw new UnsupportedOperationException("CLOSE segments cannot be broken down into parametric equations."); } if(transform==null) xCoeffs = rValue; return rValue; } public float[] getYCoeffs() { return getYCoeffs(null); } public float[] getYCoeffs(AffineTransform transform) { if(prev==null) throw new NullPointerException("prev was null"); if(prev.data==null) throw new NullPointerException("prev.data was null"); if(transform!=null && transform.isIdentity()) transform = null; if(yCoeffs!=null && transform==null) return yCoeffs; double[] last = new double[] { prev.data[prev.data.length-2], prev.data[prev.data.length-1] }; if(transform!=null) { transform.transform(last, 0, last, 0, 1); } float[] myData; if(transform!=null) { myData = new float[data.length]; transform.transform(data,0,myData,0,data.length/2); } else { myData = data; } float[] rValue = null; if(type==PathIterator.SEG_CUBICTO) { rValue = new float[4]; rValue[0] = (float)(-last[1] + 3 * myData[1] - 3 * myData[3] + myData[5]); rValue[1] = (float)(3 * last[1] - 6 * myData[1] + 3 * myData[3]); rValue[2] = (float)(-3 * last[1] + 3 * myData[1]); rValue[3] = (float)(last[1]); } else if(type==PathIterator.SEG_QUADTO) { rValue = new float[3]; rValue[0] = (float)(last[1] - 2 * myData[1] + myData[3]); rValue[1] = (float)(-2 * last[1] + 2 * myData[1]); rValue[2] = (float)(last[1]); } else if(type==PathIterator.SEG_LINETO) { rValue = new float[2]; rValue[0] = (float)(-last[1]+myData[1]); rValue[1] = (float)(last[1]); } else if(type==PathIterator.SEG_MOVETO) { throw new UnsupportedOperationException("MOVETO segments cannot be broken down into parametric equations."); } else if(type==PathIterator.SEG_CLOSE) { throw new UnsupportedOperationException("CLOSE segments cannot be broken down into parametric equations."); } if(transform==null) yCoeffs = rValue; return rValue; } /** Returns the tangent angle at the point t. * * @param t the t-value to get the tangent angle for. * @param confine this may confine the result to [-pi/2,pi/2]. * If this is false, then the result may range from [-pi,pi]. * @return the tangent angle (in radians). */ public float getTheta(float t,AffineTransform transform,boolean confine) { float angle = getTheta(t,transform,0); if(confine) { if(angle>Math.PI/2) { angle = angle-(float)Math.PI; } else if(angle<-Math.PI/2) { angle = angle+(float)Math.PI; } } return angle; } public boolean isThetaWellDefined(float t) { float[] x_coeffs = getXCoeffs(); float[] y_coeffs = getYCoeffs(); float dx, dy; if(x_coeffs.length==2) { dx = x_coeffs[0]; dy = y_coeffs[0]; } else if(x_coeffs.length==3) { dx = 2*x_coeffs[0]*t+x_coeffs[1]; dy = 2*y_coeffs[0]*t+y_coeffs[1]; } else if(x_coeffs.length==4) { dx = 3*x_coeffs[0]*t*t+2*x_coeffs[1]*t+x_coeffs[2]; dy = 3*y_coeffs[0]*t*t+2*y_coeffs[1]*t+y_coeffs[2]; } else { System.err.println("x_coeffs.length = "+x_coeffs.length); System.err.println(this); throw new RuntimeException("Unexpected condition."); } if(Math.abs(dx)<ZERO && Math.abs(dy)<ZERO) return false; return true; } /** Returns the tangent angle at the point t. * * @param t the t-value to get the tangent angle for. * @param direction in the (extremely) rare case where the * tangent slope is ambiguous, this indicates which side * of the t argument to look at. * That is, if you approach t from a smaller value, * the angle might be K. If you approach t from a larger * value, the angle might be (K+pi). This will happen * when the direction of the curve is undefined at t, because * the curve is switching from K to (K+pi). * When this argument is positive, it assumes t is being * approached by values greater than t. When negative, it * assumes t is approached from values less than t. * @return the tangent angle (in radians). */ public float getTheta(float t,AffineTransform transform, int direction) { float[] x_coeffs = getXCoeffs(transform); float[] y_coeffs = getYCoeffs(transform); float dx, dy; if(x_coeffs.length==2) { dx = x_coeffs[0]; dy = y_coeffs[0]; } else if(x_coeffs.length==3) { dx = 2*x_coeffs[0]*t+x_coeffs[1]; dy = 2*y_coeffs[0]*t+y_coeffs[1]; } else if(x_coeffs.length==4) { dx = 3*x_coeffs[0]*t*t+2*x_coeffs[1]*t+x_coeffs[2]; dy = 3*y_coeffs[0]*t*t+2*y_coeffs[1]*t+y_coeffs[2]; } else { System.err.println("x_coeffs.length = "+x_coeffs.length); System.err.println(this); throw new RuntimeException("Unexpected condition."); } if(!(Math.abs(dx)<ZERO && Math.abs(dy)<ZERO)) { float angle = (float)Math.atan2(dy,dx); return angle; } //If both dx and dy approach zero, a friend who's much smarter //than I am pointed out L'Hopital's rule says you can then //take the next derivative if(x_coeffs.length==3) { dx = 2*x_coeffs[0]; dy = 2*y_coeffs[0]; } else if(x_coeffs.length==4) { dx = 6*x_coeffs[0]*t+2*x_coeffs[1]; dy = 6*y_coeffs[0]*t+2*y_coeffs[1]; } float angle1 = (float)Math.atan2(dy,dx); float angle2; if(angle1>0) { angle2 = angle1-(float)Math.PI; } else { angle2 = angle1+(float)Math.PI; } if(direction==0) return angle1; if(x_coeffs.length==2) return angle1; //you could throw an exception here, too? dx = 0; dy = 0; if(direction>1) direction = 1; if(direction<1) direction = -1; float incr = .000001f*direction; while(Math.abs(dx)<ZERO && Math.abs(dy)<ZERO) { if(x_coeffs.length==3) { dx = 2*x_coeffs[0]*t+x_coeffs[1]; dy = 2*y_coeffs[0]*t+y_coeffs[1]; } else if(x_coeffs.length==4) { dx = 3*x_coeffs[0]*t*t+2*x_coeffs[1]*t+x_coeffs[2]; dy = 3*y_coeffs[0]*t*t+2*y_coeffs[1]*t+y_coeffs[2]; } t = t+incr; } float otherAngle = (float)Math.atan2(dy,dx); if(difference(otherAngle,angle1)<difference(otherAngle,angle2)) { return angle1; } return angle2; } private float difference(float angle1,float angle2) { float diff = Math.abs(angle1-angle2); if(diff>Math.PI) { diff = (float)(2*Math.PI-diff); } return diff; } public float getX(float t) { float[] x_coeffs = getXCoeffs(); if(x_coeffs.length==2) { return x_coeffs[0]*t+x_coeffs[1]; } else if(x_coeffs.length==3) { return x_coeffs[0]*t*t+x_coeffs[1]*t+x_coeffs[2]; } else if(x_coeffs.length==4) { return x_coeffs[0]*t*t*t+x_coeffs[1]*t*t+x_coeffs[2]*t+x_coeffs[3]; } else { System.err.println("x_coeffs.length = "+x_coeffs.length); System.err.println(this); throw new RuntimeException("Unexpected condition."); } } public float getY(float t) { float[] y_coeffs = getYCoeffs(); if(y_coeffs.length==2) { return y_coeffs[0]*t+y_coeffs[1]; } else if(y_coeffs.length==3) { return y_coeffs[0]*t*t+y_coeffs[1]*t+y_coeffs[2]; } else if(y_coeffs.length==4) { return y_coeffs[0]*t*t*t+y_coeffs[1]*t*t+y_coeffs[2]*t+y_coeffs[3]; } else { System.err.println("y_coeffs.length = "+y_coeffs.length); System.err.println(this); throw new RuntimeException("Unexpected condition."); } } @Override public String toString() { return toString((Float)null); } public String toString(Float end) { return "PathSegment.Float[ "+getPath(end)+ " ]"; } protected String getPath(Float end) { StringBuffer sb = new StringBuffer(); Float f = this; while(f!=end && f!=null) { if(f.type==PathIterator.SEG_MOVETO) { sb.append("m "+f.data[0]+" "+f.data[1]+" "); } else if(f.type==PathIterator.SEG_LINETO) { sb.append("l "+f.data[0]+" "+f.data[1]+" "); } else if(f.type==PathIterator.SEG_QUADTO) { sb.append("q "+f.data[0]+" "+f.data[1]+" "+f.data[2]+" "+f.data[3]+" "); } else if(f.type==PathIterator.SEG_CUBICTO) { sb.append("c "+f.data[0]+" "+f.data[1]+" "+f.data[2]+" "+f.data[3]+" "+f.data[4]+" "+f.data[5]+" "); } else if(f.type==PathIterator.SEG_CLOSE) { sb.append("z "); } else { throw new RuntimeException("Unexpected type: "+type); } f = f.next; } return sb.toString().trim(); } public Float moveTo(float x,float y) { Float newSegment = newSegment(); newSegment.type = PathIterator.SEG_MOVETO; newSegment.data = new float[] {x, y}; append(newSegment); return newSegment; } public Float lineTo(float x,float y) { Float newSegment = newSegment(); newSegment.type = PathIterator.SEG_LINETO; newSegment.data = new float[] {x, y}; append(newSegment); return newSegment; } public Float quadTo(float cx,float cy,float x, float y) { Float newSegment = newSegment(); newSegment.type = PathIterator.SEG_QUADTO; newSegment.data = new float[] {cx, cy, x, y}; append(newSegment); return newSegment; } public Float cubicTo(float cx0,float cy0,float cx1,float cy1,float x1,float y1) { Float newSegment = newSegment(); newSegment.type = PathIterator.SEG_CUBICTO; newSegment.data = new float[] {cx0, cy0, cx1, cy1, x1, y1}; append(newSegment); return newSegment; } public Float close() { Float newSegment = newSegment(); newSegment.type = PathIterator.SEG_CLOSE; append(newSegment); return newSegment; } protected void append(Float seg) { if(next!=null) throw new RuntimeException("Illegal attempt to append shape data to a segment that already has a next segment."); if(seg.prev!=null) throw new RuntimeException("Illegal attempt to append shape data that already exists in another sequence."); next = seg; seg.prev = this; } public Float getHead() { Float t = this; while(t.prev!=null) { t = t.prev; } return t; } public Float getTail() { Float t = this; while(t.next!=null) { t = t.next; } return t; } @Override public void rotate(float f) { if(f==0) return; float cos = (float) FastMath.cos(f); float sin = (float) FastMath.sin(f); for(int a = 0; a<data.length; a+=2) { float x = data[a]; float y = data[a+1]; data[a] = cos*x-sin*y; data[a+1] = sin*x+cos*y; } xCoeffs = null; yCoeffs = null; } public void write(PathWriter dest,float t0,float t1,AffineTransform transform) { if(type==PathIterator.SEG_LINETO) { double[] pt = new double[] { getX(t1), getY(t1) }; if(transform!=null) { transform.transform(pt, 0, pt, 0, 1); } dest.lineTo((float)pt[0],(float)pt[1]); } else if(type==PathIterator.SEG_QUADTO) { float[] x_ = getXCoeffs(transform); float[] y_ = getYCoeffs(transform); PathWriter.quadTo(dest, t0, t1, x_[0], x_[1], x_[2], y_[0], y_[1], y_[2]); } else if(type==PathIterator.SEG_CUBICTO) { float[] x_ = getXCoeffs(transform); float[] y_ = getYCoeffs(transform); PathWriter.cubicTo(dest, t0, t1, x_[0], x_[1], x_[2], x_[3], y_[0], y_[1], y_[2], y_[3]); } else if(type==PathIterator.SEG_MOVETO) { double[] pt = new double[] { data[0], data[1] }; if(transform!=null) { transform.transform(pt, 0, pt, 0, 1); } dest.moveTo((float)pt[0],(float)pt[1]); } else { throw new UnsupportedOperationException(toTypeName(type)+" not supported here."); } } } public static String toString(float[] array) { if (array == null) return null; StringBuffer sb = new StringBuffer(); sb.append("["); for (int a = 0; a < array.length; a++) { if (a != 0) { sb.append(", "); } sb.append(java.lang.Float.toString(array[a])); } sb.append("]"); return sb.toString(); } }