// Round Any Corner

// rounds selected corners of PathItems.
// Especially for the corners at the intersection point of curves,
// this script may work better than "Round Corners" filter (but slower).

// ## How To Use

// ## Illustrator CS
//  1. Select the anchor(s) or whole path(es) to round.
//  2. Run this script. A prompt window appears to set the rounding radius.
//     Input the radius in point, then click OK.
//  #. You can choose a behavior like Illustrator10 by change the setting.
//     (-- see "setting" section below)

// ## Illustrator 10
//  1. Select the anchor(s) or whole path(es) to round
//     WITH a foreground path that specifies the rounding radius.
//     Half width of foreground path is used for the radius. (excluding stroke width)
//     Using a circle is most suitable for intuitive understanding and ease of use.
//     The script asks you to continue if there's a difference greater than 1 pt
//     between width and height of foreground path.
//  2. Run this script.  The foreground path is removed after rounding.
//  #. When the number of selected path is 1, predefined radius is used
//     for rounding. (-- see "setting" section below)

// ## Rounding Method
// Basically, the rounding method is compatible with the "Round Corners" filter.
// It is to add two anchors instead of the original anchor, at the points of
// specified line length from each selected corner.  So if there're too many
// anchors on original path, this script can not round nicely.

// ## Radius
// Actually, the specified "radius" is not for a radius of arcs which drawn.
// It is for the line length from each selected corner and is for the base
// to compute the length of handles.  The reason calling it "radius" is
// for compatibility with the "Round Corners" filter.


// This script does not round the corners which already rounded.
// (for example, select a circle and run this script does nothing)

// ### notice
// In the rounding process, the script merges anchors which nearly
// overlapped (when the distance between anchors is less than 0.05 points).

// This script does not work for some part of compound pathes.
// When this occurs, please select part of the compound path or release the compound path and
// select them, then run script again.
// I still have not figured out how to get properties from grouped pathes inside a compound path.


// JavaScript Script for Adobe Illustrator CS3
// Tested with Adobe Illustrator CS3 13.0.3, Windows XP SP2 (Japanese version).
// This script provided "as is" without warranty of any kind.
// Free to use and distribute.

// Copyright(c) 2005-2009 SATO Hiroyuki
// http://park12.wakwak.com/~shp/lc/et/en_aics_script.html

// Change Log
// 2005-09-18 release on the web
// ...
// 2009-04-28 modified input part to accept unit and operator
// 2009-05-23 some refinements

var ver10 = (version.indexOf('10') == 0);

roundAnyCorner();
function roundAnyCorner(){
  // setting ----------------------------------------------

  // -- rr : rounding radius ( unit : point )
  // ## on IllustratorCS, this value is for default value in prompt window.
  
  var rr = 10;

  // -- use_foreground_shape_to_set_radius
  // set this "true" to use half width of foreground path as rounding radius
  // instead of set the radius with prompt window.
  // ## on Illustrator10, this value is always true.
  
  var use_foreground_shape_to_set_radius = false; // set true or false
  
  // -- check_circle
  // when use_foreground_shape_to_set_radius = true, 
  // setting this "true", the script asks you to continue if there's a difference
  // greater than 1 pt between width and height of foreground path.
  // it only checks difference between width and height.
  
  var check_circle = true;

  // ------------------------------------------------------
  if(ver10) use_foreground_shape_to_set_radius = true;

  var s = [];
  getPathItemsInSelection(1, s); // extract pathItems which pathpoints length is greater than 1
  if(s.length < 1) return;

  // When use half width of foreground path as rounding radius
  if(use_foreground_shape_to_set_radius){
    if(s.length < 2){
      // case : the number of selected path is 1
      // ver10 -- round by predefined value
      // CS -- ask radius by prompt window
      if(! ver10) use_foreground_shape_to_set_radius = false;
    } else {
      // case : the number of selected path > 1
      var pi = s.shift();      // remove foreground path from list of selection
      var rr2 = getRadius(pi, check_circle); // get half width of it
      
      if(rr2 < 0) return;
      
      if(rr2 > 0){
        rr = rr2;
        pi.remove();           // remove the path
      } else {
        // case : the width of foreground path is 0
        // ver10 -- round by predefined value.  the foreground path remains.
        // CS -- ask radius by prompt window  the foreground path remains.
        if(! ver10) use_foreground_shape_to_set_radius = false;
      }
    }
  }
  
  if(!use_foreground_shape_to_set_radius){
    rr = prompt("Radius\r-- you can use unit(\"pt\"(default), \"mm\", \"in\")\r   and operator ( + - * / )", rr); // input the radius;
    
    if(!rr) return;
    
    rr = rr.replace(/mm/ig, "*2.83464567");
    rr = rr.replace(/pt/ig, "");
    rr = rr.replace(/in/ig, "*72");
    rr = rr.replace(/\s+/g,"");
    
    try{
      var eval_rr = eval(rr);
    } catch(e){
      alert("ERROR:\n" + e.description);
      return;
    }
    
    if(isNaN(eval_rr) || eval_rr <= 0){
      alert("\nERROR: fail to convert the expression to a positive number.");
      return;
    }

    rr = eval_rr;
  }

 // var tim = new Date();
  var p, op, pnts;
  var skipList, adjRdirAtEnd, redrawFlg;
  var i, nxi, pvi, q, d,ds, r, g, t, qb;
  var anc1, ldir1, rdir1, anc2, ldir2, rdir2;
  
  var hanLen = 4 * (Math.sqrt(2) - 1) / 3;
  var ptyp = PointType.SMOOTH;

  for(var j = 0; j < s.length; j++){
    p = s[j].pathPoints;
   if(readjustAnchors(p) < 2) continue; // reduce anchors
    op = !s[j].closed;
    pnts = op ? [getDat(p[0])] : [];
    redrawFlg = false;
    adjRdirAtEnd = 0;

    skipList = [(op || !isSelected(p[0]) || ! isCorner(p, 0))];
    for(i = 1; i < p.length; i++){
      skipList.push((! isSelected(p[i])
                     || ! isCorner(p,i)
                     || (op && i == p.length - 1)));
    }
    
    for(i = 0; i < p.length; i++){
      nxi = parseIdx(p, i + 1);
      if(nxi < 0) break;
      
      pvi = parseIdx(p, i - 1);

      q = [p[i].anchor,          p[i].rightDirection,
           p[nxi].leftDirection, p[nxi].anchor];

      ds = dist(q[0], q[3]) / 2;
      if(arrEq(q[0], q[1]) && arrEq(q[2], q[3])){  // straight side
        r = Math.min(ds, rr);
        g = getRad(q[0], q[3]);
        anc1 = getPnt(q[0], g, r);
        ldir1 = getPnt(anc1, g + Math.PI, r * hanLen);
        
        if(skipList[nxi]){
          if(!skipList[i]){
            pnts.push([anc1, anc1, ldir1, ptyp]);
            redrawFlg = true;
          }
          pnts.push(getDat(p[nxi]));
        } else {
          if(r<rr){  // when the length of the side is less than rr * 2
            pnts.push([anc1,
                       getPnt(anc1, getRad(ldir1, anc1), r * hanLen),
                       ldir1,
                       ptyp]);
          } else {
            if(!skipList[i]) pnts.push([anc1, anc1, ldir1, ptyp]);
            anc2 = getPnt(q[3], g+Math.PI, r);
            pnts.push([anc2,
                       getPnt(anc2, g, r * hanLen),
                       anc2,
                       ptyp]);
          }
          redrawFlg = true;
        }
      } else {  // not straight side
        d = getT4Len(q, 0) / 2;
        r = Math.min(d,rr);
        t = getT4Len(q, r);
        anc1 = bezier(q, t);
        rdir1 = defHan(t, q, 1);
        ldir1 = getPnt(anc1, getRad(rdir1, anc1), r * hanLen);

        if(skipList[nxi]){
          if(skipList[i]){
            pnts.push(getDat(p[nxi]));
          } else {
            pnts.push([anc1, rdir1, ldir1, ptyp]);
            with(p[nxi]) pnts.push([anchor,
                                    rightDirection,
                                    adjHan(anchor, leftDirection, 1 - t),
                                    ptyp]);
            redrawFlg = true;
          }
        } else { // skipList[nxi] = false
          if(r < rr){  // the length of the side is less than rr * 2
            if(skipList[i]){
              if(!op && i == 0){
                adjRdirAtEnd = t;
              } else {
                pnts[pnts.length-1][1] = adjHan(q[0], q[1], t);
              }
              pnts.push([anc1,
                         getPnt(anc1, getRad(ldir1, anc1), r * hanLen),
                         defHan(t, q, 0),
                         ptyp]);
            } else {
              pnts.push([anc1,
                         getPnt(anc1, getRad(ldir1, anc1), r * hanLen),
                         ldir1,
                         ptyp]);
            }
          } else {  // round the corner with the radius rr
            if(skipList[i]){
              t = getT4Len(q, -r);
              anc2 = bezier(q, t);
              
              if(!op && i==0) {
                adjRdirAtEnd = t;
              } else {
                pnts[pnts.length - 1][1] = adjHan(q[0], q[1], t);
              }

              ldir2 = defHan(t, q, 0);
              rdir2 = getPnt(anc2, getRad(ldir2, anc2), r * hanLen);

              pnts.push([anc2, rdir2, ldir2 , ptyp]);
            } else {
              qb = [anc1, rdir1, adjHan(q[3], q[2], 1 - t), q[3]];
              t = getT4Len(qb, -r);
              anc2 = bezier(qb, t);
              ldir2 = defHan(t,qb,0);
              rdir2 = getPnt(anc2, getRad(ldir2, anc2), r*hanLen);
              rdir1 = adjHan(anc1, rdir1, t);

              pnts.push([anc1, rdir1, ldir1, ptyp],
                        [anc2, rdir2, ldir2, ptyp]);
            }
          }
          redrawFlg = true;
        }
      }
    }
    if(adjRdirAtEnd > 0){
      pnts[pnts.length - 1][1] = adjHan(p[0].anchor, p[0].rightDirection, adjRdirAtEnd);
    }
    
    if(redrawFlg){
      // redraw
      for(i = p.length-1; i > 0; i--) p[i].remove();
      
      for(i = 0; i < pnts.length; i++){
        pt = i > 0 ? p.add() : p[0];
        with(pt){
          anchor         = pnts[i][0];
          rightDirection = pnts[i][1];
          leftDirection  = pnts[i][2];
          pointType      = pnts[i][3];
        }
      }
    }
  }
  activeDocument.selection = s;
  // alert(new Date() - tim);
}

// ------------------------------------------------
function getRadius(pi, check_circle){
  with(pi){
    var gb = geometricBounds;
    var w = (gb[2] - gb[0]);
    var h = (gb[1] - gb[3]);
    if(check_circle && Math.abs(w - h) > 1
       && !confirm("There's a difference between width and\n"
                   + "height of foreground path. Continue?")){
      return -1;
    }
    return w / 2;
  }
}

// ------------------------------------------------
// return [x,y] of the distance "len" and the angle "rad"(in radian)
// from "pt"=[x,y]
function getPnt(pt, rad, len){
  return [pt[0] + Math.cos(rad) * len,
          pt[1] + Math.sin(rad) * len];
}

// ------------------------------------------------
// return the [x, y] coordinate of the handle of the point on the bezier curve
// that corresponds to the parameter "t"
// n=0:leftDir, n=1:rightDir
function defHan(t, q, n){
  return [t * (t * (q[n][0] - 2 * q[n+1][0] + q[n+2][0]) + 2 * (q[n+1][0] - q[n][0])) + q[n][0],
          t * (t * (q[n][1] - 2 * q[n+1][1] + q[n+2][1]) + 2 * (q[n+1][1] - q[n][1])) + q[n][1]];
}

// -----------------------------------------------
// return the [x, y] coordinate on the bezier curve
// that corresponds to the paramter "t"
function bezier(q, t) {
  var u = 1 - t;
  return [u*u*u * q[0][0] + 3*u*t*(u* q[1][0] + t* q[2][0]) + t*t*t * q[3][0],
          u*u*u * q[0][1] + 3*u*t*(u* q[1][1] + t* q[2][1]) + t*t*t * q[3][1]];
}

// ------------------------------------------------
// adjust the length of the handle "dir"
// by the magnification ratio "m",
// returns the modified [x, y] coordinate of the handle
// "anc" is the anchor [x, y]
function adjHan(anc, dir, m){
  return [anc[0] + (dir[0] - anc[0]) * m,
          anc[1] + (dir[1] - anc[1]) * m];
}

// ------------------------------------------------
// return true if the pathPoints "p[idx]" is a corner
function isCorner(p, idx){
  var pnt0 = getAnglePnt(p, idx, -1);
  var pnt1 = getAnglePnt(p, idx,  1);
  if(! pnt0 || ! pnt1) return false;                    // at the end of a open-path
  if(pnt0.length < 1 || pnt1.length<1) return false;   // anchor is overlapping, so cannot determine the angle
  var rad = getRad2(pnt0, p[idx].anchor, pnt1, true);
  if(rad > Math.PI - 0.1) return false;   // set the angle tolerance here
  return true;
}
// ------------------------------------------------
// "p"=pathPoints, "idx1"=index of pathpoint
// "dir" = -1, returns previous point [x,y] to get the angle of tangent at pathpoints[idx1]
// "dir" =  1, returns next ...
function getAnglePnt(p, idx1, dir){
  if(!dir) dir = -1;
  var idx2 = parseIdx(p, idx1 + dir);
  if(idx2 < 0) return null;  // at the end of a open-path
  var p2 = p[idx2];
  with(p[idx1]){
    if(dir<0){
      if(arrEq(leftDirection, anchor)){
        if(arrEq(p2.anchor, anchor)) return [];
        if(arrEq(p2.anchor, p2.rightDirection)
           || arrEq(p2.rightDirection, anchor)) return p2.anchor;
        else return p2.rightDirection;
      } else {
        return leftDirection;
      }
    } else {
      if(arrEq(anchor, rightDirection)){
        if(arrEq(anchor, p2.anchor)) return [];
        if(arrEq(p2.anchor, p2.leftDirection)
           || arrEq(anchor, p2.leftDirection)) return p2.anchor;
        else return p2.leftDirection;
      } else {
        return rightDirection;
      }
    }
  }
}
// --------------------------------------
// if the contents of both arrays are equal, return true (lengthes must be same)
function arrEq(arr1, arr2) {
  for(var i = 0; i < arr1.length; i++){
    if (arr1[i] != arr2[i]) return false;
  }
  return true;
}

// ------------------------------------------------
// return the distance between p1=[x,y] and p2=[x,y]
function dist(p1, p2) {
  return Math.sqrt(Math.pow(p1[0] - p2[0], 2)
                   + Math.pow(p1[1] - p2[1], 2));
}
// ------------------------------------------------
// return the squared distance between p1=[x,y] and p2=[x,y]
function dist2(p1, p2) {
  return Math.pow(p1[0] - p2[0],2)
       + Math.pow(p1[1] - p2[1],2);
}
// --------------------------------------
// return the angle in radian
// of the line drawn from p1=[x,y] from p2
function getRad(p1,p2) {
  return Math.atan2(p2[1] - p1[1],
                    p2[0] - p1[0]);
}

// --------------------------------------
// return the angle between two line segments
// o-p1 and o-p2 ( 0 - Math.PI)
function getRad2(p1, o, p2){
  var v1 = normalize(p1, o);
  var v2 = normalize(p2, o);
  return Math.acos(v1[0] * v2[0] + v1[1] * v2[1]);
}
// ------------------------------------------------
function normalize(p, o){
  var d = dist(p, o);
  return d == 0 ? [0, 0] : [(p[0] - o[0]) / d,
                            (p[1] - o[1]) / d];
}

// ------------------------------------------------
// return the bezier curve parameter "t"
// at the point which the length of the bezier curve segment
// (from the point start drawing) is "len"
// when "len" is 0, return the length of whole this segment.
function getT4Len(q, len){
  var m = [ q[3][0] - q[0][0] + 3 * (q[1][0] - q[2][0]),
            q[0][0] - 2 * q[1][0] + q[2][0],
            q[1][0] - q[0][0] ];
  var n = [ q[3][1] - q[0][1] + 3 * (q[1][1] - q[2][1]),
            q[0][1] - 2 * q[1][1] + q[2][1],
            q[1][1] - q[0][1] ];
  var k = [ m[0] * m[0] + n[0] * n[0],
            4 * (m[0] * m[1] + n[0] * n[1]),
            2 * ((m[0] * m[2] + n[0] * n[2]) + 2 * (m[1] * m[1] + n[1] * n[1])),
            4 * (m[1] * m[2] + n[1] * n[2]),
            m[2] * m[2] + n[2] * n[2] ];
  
   var fullLen = getLength(k, 1);

  if(len == 0){
    return fullLen;
    
  } else if(len < 0){
    len += fullLen;
    if(len < 0) return 0;

  } else if(len > fullLen){
    return 1;
  }
  
  var t, d;
  var t0 = 0;
  var t1 = 1;
  var torelance = 0.001;
  
  for(var h = 1; h < 30; h++){
    t = t0 + (t1 - t0) / 2;
    d = len - getLength(k, t);
    if(Math.abs(d) < torelance) break;
    else if(d < 0) t1 = t;
    else t0 = t;
  }
  return t;
}

// ------------------------------------------------
// return the length of bezier curve segment
// in range of parameter from 0 to "t"
function getLength(k, t){
  var h = t / 128;
  var hh = h * 2;
  var fc = function(t, k){
    return Math.sqrt(t * (t * (t * (t * k[0] + k[1]) + k[2]) + k[3]) + k[4]) || 0 };
  var total = (fc(0, k) - fc(t, k)) / 2;
  for(var i = h; i < t; i += hh) total += 2 * fc(i, k) + fc(i + h, k);
  return total * hh;
}

// ------------------------------------------------
// extract PathItems from the selection which length of PathPoints
// is greater than "n"
function getPathItemsInSelection(n, pathes){
  if(documents.length < 1) return;
  
  var s = activeDocument.selection;
  
  if (!(s instanceof Array) || s.length < 1) return;

  extractPathes(s, n, pathes);
}

// --------------------------------------
// extract PathItems from "s" (Array of PageItems -- ex. selection),
// and put them into an Array "pathes".  If "pp_length_limit" is specified,
// this function extracts PathItems which PathPoints length is greater
// than this number.
function extractPathes(s, pp_length_limit, pathes){
  for(var i = 0; i < s.length; i++){
    if(s[i].typename == "PathItem"){
      if(pp_length_limit
         && s[i].pathPoints.length <= pp_length_limit){
        continue;
      }
      pathes.push(s[i]);
      
    } else if(s[i].typename == "GroupItem"){
      // search for PathItems in GroupItem, recursively
      extractPathes(s[i].pageItems, pp_length_limit, pathes);
      
    } else if(s[i].typename == "CompoundPathItem"){
      // searches for pathitems in CompoundPathItem, recursively
      // ( ### Grouped PathItems in CompoundPathItem are ignored ### )
      extractPathes(s[i].pathItems, pp_length_limit , pathes);
    }
  }
}

// --------------------------------------
// merge nearly overlapped anchor points 
// return the length of pathpoints after merging
function readjustAnchors(p){
  // Settings ==========================

  // merge the anchor points when the distance between
  // 2 points is within ### square root ### of this value (in point)
  var minDist = 0.0025; 
  
  // ===================================
  if(p.length < 2) return 1;
  var i;

  if(p.parent.closed){
    for(i = p.length - 1; i >= 1; i--){
      if(dist2(p[0].anchor, p[i].anchor) < minDist){
        p[0].leftDirection = p[i].leftDirection;
        p[i].remove();
      } else {
        break;
      }
    }
  }
  
  for(i = p.length - 1; i >= 1; i--){
    if(dist2(p[i].anchor, p[i - 1].anchor) < minDist){
      p[i - 1].rightDirection = p[i].rightDirection;
      p[i].remove();
    }
  }
  
  return p.length;
}
// -----------------------------------------------
// return pathpoint's index. when the argument is out of bounds,
// fixes it if the path is closed (ex. next of last index is 0),
// or return -1 if the path is not closed.
function parseIdx(p, n){ // PathPoints, number for index
  var len = p.length;
  if(p.parent.closed){
    return n >= 0 ? n % len : len - Math.abs(n % len);
  } else {
    return (n < 0 || n > len - 1) ? -1 : n;
  }
}
// -----------------------------------------------
function getDat(p){ // pathPoint
  with(p) return [anchor, rightDirection, leftDirection, pointType];
}
// -----------------------------------------------
function isSelected(p){ // PathPoint
  return p.selected == PathPointSelection.ANCHORPOINT;
}
