// © Michiel Kamermans 2010 - CC 3.0 license (AT-NC) /** * Linear path point. Any segment between * two linear points is a straight line. * * A point is actually a collection of three points: * - base * - left, with respect to a direction of 0 * - right, with respect to a direction of 0 * * For linear points the point directionality is computed * based on the previous and next point. */ class Point { String name; // Own coordinates int x; int y; // The directionanlity for this point. For linear points, the directionality is defined by // the prev+next point. For bezier points, the directionality is already defined by the // from-prev and to-next control points, so only linear points 'compute' them. float directionality = 0; float directionality_radians = 0; // quick getters int getX() { return x; } int getY() { return y; } float getDirectionality() { return directionality; } float getDirectionalityInRadians() { return directionality_radians; } // Neighbouring point Point previous = null; Point next = null; // Extrusion points Point left = null; Point right = null; // controls the extrusion thickness at this point float lscale = 1.0; float rscale = 1.0; float getLScale() { return lscale; } float getRScale() { return rscale; } void setLScale(float ls) { left=null; lscale = ls;} void setRScale(float rs) { right=null; rscale = rs; } void addLScale(float ls) { left = null; lscale += ls; } void addRScale(float rs) { right=null; rscale += rs; } /** * Points are constructed at some x/y coordinate */ Point(String name, int x, int y) { this.name = name; this.x = x; this.y = y; } /** * return the point preceeding this one. */ Point getPrevious() { return previous; } /** * return the point following this one. */ Point getNext() { return next; } /** * Indicate what this point's previous neighbour is */ void setPrevious(Point p) { this.previous = p; updateDirectionality(); } /** * Indicate what this point's next neighbour is */ void setNext(Point n) { this.next = n; updateDirectionality(); } /** * Drawing this point only draws the real point, * not the path extrusions. */ void draw() { ellipse(x,y,5,5); } float distance = 10; float getLeftXOffset() { return lscale * distance * cos(directionality_radians - (PI/2.0)); } float getLeftYOffset() { return lscale * distance * sin(directionality_radians - (PI/2.0)); } float getRightXOffset() { return rscale * distance * cos(directionality_radians + (PI/2.0)); } float getRightYOffset() { return rscale * distance * sin(directionality_radians + (PI/2.0)); } /** * build a new point, offset with respect to this point. * this function is used for the left/right extrusions */ Point buildOffsetPoint(String suffix, float nx, float ny) { int pointx = (int)Math.round(x + nx); int pointy = (int)Math.round(y + ny); return new Point(name+"_"+suffix,pointx, pointy); } /** * Return a new point representing the left extrusion point. */ Point getLeftPoint() { if(left == null) { float nx = getLeftXOffset(); float ny = getLeftYOffset(); left = buildOffsetPoint("left", nx, ny); } return left; } /** * Return a new point representing the left extrusion point. */ Point getRightPoint() { if(right == null) { float nx = getRightXOffset(); float ny = getRightYOffset(); right = buildOffsetPoint("right", nx, ny); } return right; } // Used for offset computation: angular offset per quadrant, in degrees float d1 = 0.0; float d2 = 90.0; float d3 = 180.0; float d4 = 270.0; /** * Update the directionality for this point. This method is empty * in the BezierPoint class, because bezier points don't need other * points to know their directionality. */ void updateDirectionality() { float angle_from_previous = 0; float angle_to_next = 0; // determine the angle to the previous point, if there is one. boolean sameasprev = false; if(previous!=null) { float dx = 0; float dy = 0; // bezier or linear computation? if(previous instanceof BezierPoint) { BezierPoint bp = (BezierPoint) previous; dx = (x - bp.cx_next); dy = (y - bp.cy_next); } // linear computation else { dx = (x - previous.x); dy = (y - previous.y); } // absolute values, for tangents float adx = (int)abs(dx); float ady = (int)abs(dy); // vertical travel only if(dx==0) { angle_from_previous = (dy>=0? d4 : d2); if(next==null) { sameasprev = true; }} // horizontal travel only else if(dy==0) { angle_from_previous = (dx>=0? d3 : d1); if(next==null) { sameasprev = true; }} // cases computed using atan(dy/dx) else if(dx>0 && dy>0) { // X+, Y+ angle_from_previous = d3 + (atan(ady/adx) * (180.0/PI)); } else if(dx<0 && dy<0) { // X-, Y- angle_from_previous = d1 + (atan(ady/adx) * (180.0/PI)); } // cases computed using atan(dx/dy) else if(dx<0 && dy>0) { // X-, Y+ angle_from_previous = d4 + (atan(adx/ady) * (180.0/PI)); } else if(dx>0 && dy<0) { // X+, Y- angle_from_previous = d2 + (atan(adx/ady) * (180.0/PI)); } // singleton case: there is no next (last point) if(next==null) { angle_to_next = -1; } } // determine the angle to the next point, if there is one if(next!=null) { float dx = 0; float dy = 0; // bezier or linear computation? if(next instanceof BezierPoint) { BezierPoint bn = (BezierPoint) next; dx = (bn.cx_prev - x); dy = (bn.cy_prev - y); } // linear computation else { dx = (next.x -x); dy = (next.y - y); } // absolute values, for tangents float adx = (int)abs(dx); float ady = (int)abs(dy); // vertical travel only if(dx==0) { angle_to_next = (dy>=0? d2 : d4); } // horizontal travel only else if(dy==0) { angle_to_next = (dx>=0? d1 : d3); } // cases computed using atan(dy/dx) else if(dx>0 && dy>0) { // X+, Y+ angle_to_next = d1 + (atan(ady/adx) * (180.0/PI)); } else if(dx<0 && dy<0) { // X-, Y- angle_to_next = d3 + (atan(ady/adx) * (180.0/PI)); } // cases computed using atan(dx/dy) else if(dx<0 && dy>0) { // X-, Y+ angle_to_next = d2 + (atan(adx/ady) * (180.0/PI)); } else if(dx>0 && dy<0) { // X+, Y- angle_to_next = d4 + (atan(adx/ady) * (180.0/PI)); } // singleton case: there is no previous (first point) if(previous==null) { angle_from_previous = -1; } } // finally, set the directionality values if(angle_to_next==-1) { directionality = (angle_from_previous+180.0)%360.0; } else if(angle_from_previous==-1) { directionality = angle_to_next; } else { float midangle = (angle_from_previous/2.0 ) + (angle_to_next/2.0); // except not quite. which merge rule do we have to use? if(angle_from_previous < angle_to_next) { directionality = midangle + 90; } else { directionality = midangle - 90; }} directionality_radians = directionality*PI/180.0; // clear left/right if directionality changed left = null; right = null; } /** * Generate the left->forward SVG for this point */ VectorInstruction getLeftSVG() { if(previous == null) { return new MoveTo(getLeftPoint().x, getLeftPoint().y, getLeftPoint().x, getLeftPoint().y); } if(previous instanceof BezierPoint) { BezierPoint b = (BezierPoint) previous; BezierPoint bl = b.getLeftPoint(); return new CurveTo(bl.x, bl.y, bl.cx_next, bl.cy_next, getLeftPoint().x, getLeftPoint().y, getLeftPoint().x, getLeftPoint().y); } else { return new LineTo(previous.getLeftPoint().x, previous.getLeftPoint().y, getLeftPoint().x, getLeftPoint().y); } } /** * Generate the right->return SVG for this point */ ArrayList/**/ getRightSVG() { ArrayList instructions = new ArrayList(); if(next == null) { instructions.add(new LineTo(getLeftPoint().x, getLeftPoint().y, getRightPoint().x, getRightPoint().y)); } // explicit end cap else { instructions.add(new LineTo(next.getRightPoint().x, next.previous.getRightPoint().y, getRightPoint().x, getRightPoint().y)); } VectorInstruction postfix = null; if(previous == null) { postfix = new ClosePath(); } // implicit start cap else if(previous instanceof BezierPoint) { // A "half bezier" curve (bezier -> point -> anything) needs an explicit coordinate // for that [point] in the series, so that the curve SVG start at the correct (implied) // coordinates. BezierPoint b = (BezierPoint) previous; BezierPoint br = b.getRightPoint(); instructions.add(new LineTo(br.x, br.y, getRightPoint().x, getRightPoint().y)); instructions.add(new CurveTo(getRightPoint().x, getRightPoint().y, getRightPoint().x, getRightPoint().y, br.cx_next, br.cy_next, br.x, br.y)); } else { instructions.add(new LineTo(previous.getRightPoint().x, previous.getRightPoint().y, getRightPoint().x, getRightPoint().y)); } if(postfix!=null) { instructions.add(postfix); } return instructions; } }