////////////////////////////////////////////////////////////////////////////////
// Note from the Scriptographer.org Team
//
// In Scriptographer 2.9, we switched to a top-down coordinate system and
// degrees for angle units as an easier alternative to radians.
// 
// For backward compatibility we offer the possibility to still use the old
// bottom-up coordinate system and radians for angle units, by setting the two
// values bellow. Read more about this transition on our website:
// http://scriptographer.org/news/version-2.9.064-arrived/

script.coordinateSystem = 'bottom-up';
script.angleUnits = 'radians';

/*
rot3d
  
Scriptographer 2.9.064 script
  
Scriptographer is a plugin for Adobe Illustrator(TM)
created by Juerg Lehni
http://www.scriptographer.org/

This script was written by Hiroyuki Sato
http://shspage.blogspot.jp/
  
2009-05-24
2009-05-27 *modified applying order:
            self-rotation > height > global-rotation > global-height
           *If the parent of the target is an instance of Item ( unless it is not a Layer),
            -> gets options and applys recursively
2009-05-27a *added L,R,T,B,Z option.
            see "OPTIONS BY THE NAME OF THE TARGETS" section for detail.
            *fixed a bug in "getItemSetRect" function
2009-06-30: *removed "MIN_RADIUS" option
            *added "fix_z_order" option
2009-?-?   *official staff modified to work with Sg 2.5. Thanks!
2010-02-21 *modified to work with Sg 2.6
2010-?-?   *official staff modified to work with Sg 2.9. Thanks!
2013-02-14 *fixed a bug in Group detection
           *simplified the code
2013-02-14 *fixed a bug in rotation
-----------------------------------------------------------
  
HOW TO USE:
  
 *Select pathes and drag.
 *Release mouse button to fix the result.

OPTIONS:
  
  focal length:  distance to focal point
  global depth: default depth (negative value for z-axis)
  fix z-order: (see OTHER NOTE below)
  
BE CAREFULL NOT TO DO SUBSEQUENT DRAGS:
  
 *Once mouse button was released, the targets' z-axis values are
  set to zero, and they return to 2D plane, as distorted shapes.
  So, if you select these transformed targets and drag again,
  the result must be a quite unexpected one.
  If you don't prefer the result, make UNDO first.

OPTIONS BY THE NAME OF THE TARGETS:
  
 *This script gets optional values from the name of the object
  as follows. (N means a number)
    N:   height ( - global depth) (must be at the beginning of its name)
    xN:  x-axis rotation. uses center of the object itself as origin
    xoN: x-axis rotation. uses global origin (default: center of the selection)
    yN, zN: same as "xN" but for y-axis and z-axis
    yoN, zoN: same as "xoN" but for y-axis and z-axis

    LN:  sets left side of the object as the self-rotation center
       if N specified, offsets the center by N toward center of the object.
    RN, TN, BN:  same for right, top, bottom
    ZN:  offsets the center of the self-rotation by N toward z-axis (front/rear)
  
  ex. name="10 x20" -> rotates 20 degree around x-axis, rotation origin is
      the center of the object itself. then offsets the center of this object's
      z-axis value by 10.

 *If there's a path named "origin" in the selection,
  this script sets its center as the rotation center.

OTHER NOTE:
 *If "fix z-order" option is checked, this script changes the z-order
  of the objects according to the z-axis value of the center of the each object.
  It doesn't always work properly, and this process may destruct the groups.
  
 *This script removes the transformed objects that had exceeded
  the value which multiplied the virtual sphere radius by predefined value.
  with a message dialog.
  The default predefined value is 2.0.
  Edit "EXCESSIVE_TRANSFORM_THRESHOLD" value below to change it.
*/

const EXCESSIVE_TRANSFORM_THRESHOLD = 2.0;

// set max( this value, max(selection.width, selection.height)) to virtual sphere's radius
const MIN_RADIUS = 100;

// ----------------------------------------------
var Vector3d = function(x,y,z){
  this.x = x; this.y = y; this.z = z;
}
Vector3d.prototype = {
  clone : function(){
    return new Vector3d(this.x, this.y, this.z);
  },
  add : function(v){
    this.x += v.x; this.y += v.y; this.z += v.z;
    return this;
  },
  sub : function(v){
    this.x -= v.x; this.y -= v.y; this.z -= v.z;
    return this;
  },
  xrot : function(t){
    var c = Math.cos(t); var s = Math.sin(t);
    var y2 = this.y*c - this.z*s;
    var z2 = this.y*s + this.z*c;
    this.y = y2; this.z = z2;
    return this;
  },
  yrot : function(t){
    var c = Math.cos(t); var s = Math.sin(t);
    var x2 = this.x*c + this.z*s;
    var z2 = - this.x*s + this.z*c;
    this.x = x2; this.z = z2;
    return this;
  },
  zrot : function(t){
    var c = Math.cos(t); var s = Math.sin(t);
    var x2 = this.x*c - this.y*s;
    var y2 = this.x*s + this.y*c;
    this.x = x2; this.y = y2;
    return this;
  },
  rot : function(rangle){ // RotationAngle rangle
    this.xrot(rangle.xr);
    this.yrot(rangle.yr);
    this.zrot(rangle.zr);
    return this;
  },
  to2d : function(f){
    this.x = f * this.x / (f - this.z);
    this.y = f * this.y /(f - this.z);
    return this;
  }
}
// ----------------------------------------------
var Vector3dSegment = function(point, handleIn, handleOut){
  this.point = point;
  this.handleIn = handleIn;
  this.handleOut = handleOut;
}
// ----------------------------------------------
var RotationAngle = function(xr, yr, zr){
  this.xr = xr;
  this.yr = yr;
  this.zr = zr;
}
RotationAngle.prototype = {
  reset : function(){
    this.xr = 0; this.yr = 0; this.zr = 0;
    return this;
  },
  nonZero : function(){
    return this.xr != 0 || this.yr != 0 || this.zr != 0;
  }
}
// ----------------------------------------------


var g = {
  sel : [], // selected paths
  sr : null, // rectangle around the selection
  so : null, // center of the selection
  inv_radius : null,
  vpaths : [], // array of Vector3dSegments
  centers : [], // center of the each object. used to fix z order if fix_z_order==true
  last_rot : null, // last rotation config
  last_pnt : null, // previous event.point that processed
  updated : false, // redraw when mouseUp if true
  p0 : new Point(0,0),
  v0 : new Vector3d(0,0,0)
  };
  
// ==================================================================
var values = {
  focal_length: 400,
  global_depth: 0,
  fix_z_order: true
};

var components = {
  focal_length: {
    label: 'focal length'
  },
  global_depth: {
    label: 'global depth'
  },
  fix_z_order: {
    label: 'fix z-order',
    type: 'boolean'
  }
};

var palette = new Palette('rot3d', components, values);

// ----------------------------------------------
function onMouseDown(event){
  with( document.activeLayer ){
    if( locked || hidden ){
      Dialog.alert("please unlock and show the active layer");
      return; }
  }

  // get selected paths
  g.sel = document.getItems({ type:Path, selected: true});
  
  g.sr = getItemSetRect(g.sel);
  if(g.sr == null) return;
  if(g.so == null) g.so = g.sr.center;
  g.inv_radius = 1 / (Math.max(Math.max(g.sr.width, g.sr.height) / 2, MIN_RADIUS));
  
  var opts = { z:0, r:new RotationAngle(0,0,0), or:new RotationAngle(0,0,0), center:null};
  var vectHeight = g.v0.clone();
  vectHeight.z = - values.global_depth;
  
  for(var i=0; i < g.sel.length; i++){
    opts.or.reset();
    
    var vsegs = []; // array of Vector3dSegment
    
    for(var j = 0; j < g.sel[i].segments.length; j++){
      var seg = g.sel[i].segments[j];
      vsegs.push( new Vector3dSegment(
        new Vector3d(seg.point.x - g.so.x, seg.point.y - g.so.y, 0),
        new Vector3d(seg.handleIn.x,  seg.handleIn.y, 0),
        new Vector3d(seg.handleOut.x, seg.handleOut.y, 0)));
    }
    
    // push the coord for center at the last of the array
    vsegs.push( new Vector3dSegment(
      new Vector3d(g.sel[i].bounds.center.x - g.so.x,
                   g.sel[i].bounds.center.y - g.so.y, 0),
      g.v0.clone(), g.v0.clone()))
    
    getSelfRotationFromName(g.sel[i], vsegs, opts);
    
    getRotationFromName(g.sel[i], opts);
    if(opts.or.nonZero()){
      rotSegDat(vsegs, opts.or);
    }

    for(var j=0; j < vsegs.length; j++){
        vsegs[j].point.add( vectHeight );
    }
    g.centers.push( vsegs.pop().point ); // center
    g.vpaths.push(vsegs);
  }
  
  g.last_pnt = event.point;
  g.updated = false;
}
// ----------------------------------------------
function getSelfRotationFromName(obj, vsegs, opts){
  if( obj instanceof Layer ) return;
  
  var sname = obj.name || "";
  
  if( sname != "" ){
    opts.r.reset();
    opts.center = obj.bounds.center;
    oz = 0;
      
    // xN:  x-axis rotation. uses center of the object itself as origin
    if(sname.match(/x(-?\d+)/)){ opts.r.xr += toRad(RegExp.$1); }
    if(sname.match(/y(-?\d+)/)){ opts.r.yr += toRad(RegExp.$1); }
    if(sname.match(/z(-?\d+)/)){ opts.r.zr += toRad(RegExp.$1); }
    
    // LR & offset
    if(sname.match(/([RL])/)){
      if(RegExp.$1 == "R"){
        opts.center.x = obj.bounds.right;
        if(sname.match(/R(\d+)/)){ opts.center.x -= parseFloat(RegExp.$1); }
      } else { // L
        opts.center.x = obj.bounds.left;
        if(sname.match(/L(\d+)/)){ opts.center.x += parseFloat(RegExp.$1); }
      }
    }
    
    // TB & offset
    if(sname.match(/([TB])/)){
      if(RegExp.$1 == "T"){
        opts.center.y = obj.bounds.top;
        if(sname.match(/T(\d+)/)){ opts.center.y -= parseFloat(RegExp.$1); }
      } else { // B
        opts.center.y = obj.bounds.bottom;
        if(sname.match(/B(\d+)/)){ opts.center.y += parseFloat(RegExp.$1); }
      }
    }
    
    // Z & offset
    if(sname.match(/Z(-?\d+)/)){ oz = parseFloat(RegExp.$1); }
    
    // actual rotation
    if(opts.r.nonZero()){
      rotSegDat2(vsegs, opts.r, opts.center - g.so, oz);
    }
    
    // option by the name of the object. N means a number
    // N: height ( - global depth) (must be at the beginning of its name)
    if( sname.match(/^([\d\.-]+)/) && !isNaN(RegExp.$1) ){
      var vectHeight = parseFloat(RegExp.$1);
      for(var j = 0; j < vsegs.length; j++){
          vsegs[j].point.z += vectHeight;
      }
    }
  }
  
  if( obj.parent instanceof Item ){
    getSelfRotationFromName(obj.parent, vsegs, opts);
  }
}
// ----------------------------------------------
function getRotationFromName(obj, opts){
  if( obj instanceof Layer) return;
  
  var sname = obj.name || "";
  
  if( sname != "" ){
    // xoN: x-axis rotation. uses global origin (default: center of the selection)
    if(sname.match(/xo(-?\d+)/)){ opts.or.xr += toRad(RegExp.$1); }
    if(sname.match(/yo(-?\d+)/)){ opts.or.yr += toRad(RegExp.$1); }
    if(sname.match(/zo(-?\d+)/)){ opts.or.zr += toRad(RegExp.$1); }
  }
  
  if( obj.parent instanceof Item ){
    getRotationFromName(obj.parent, opts);
  }
}
// ----------------------------------------------
function toRad(deg){
  return ( parseFloat(deg) - 0 ) * 0.0174532925199433; // * (PI / 180);
}
// ----------------------------------------------
function onMouseDrag(event){
  if(g.last_pnt == null || g.last_pnt.getDistance(event.point) < 1) return;
  g.last_pnt = event.point;
  
  if(g.sel.length < 1 || g.sr == null) return;
  
  var v = (g.last_pnt - g.so) * g.inv_radius;
  if(v.getDistance(g.p0) > 0.999) v = v.normalize() * 0.999;

  // rotation angles
  var t = Math.asin(v.y);
  if(isNaN(t)) return;

  if(g.last_rot == null) g.last_rot = new RotationAngle(0,0,0);
  
  g.last_rot.xr = -t;
  g.last_rot.yr = Math.asin(v.x / Math.cos(t));
  if(isNaN( g.last_rot.yr )) return;

  g.last_rot.zr = 0;
  
  for(var i = 0; i< g.sel.length; i++){
    var seg = g.sel[i].segments;
    
    for(var j = 0; j < seg.length; j++){
      var vp = g.vpaths[i][j].point;
      var pnt = view3d(g.vpaths[i][j].point, g.v0, g.p0);
      seg[j].point = pnt + g.so;
      seg[j].handleIn = view3d(g.vpaths[i][j].handleIn, vp, pnt);
      seg[j].handleOut = view3d(g.vpaths[i][j].handleOut, vp, pnt);
    }
  }
  
  g.updated = true;
}
// ----------------------------------------------
function view3d(vt, v2, p){
  var vect = vt.clone().add(v2);
  vect.rot(g.last_rot);
  vect.to2d(values.focal_length);
  return new Point(vect.x - p.x, vect.y - p.y);
}
// ----------------------------------------------
function rotSegDat(vsegs, rangle){
  for(var i = 0; i < vsegs.length; i++){
    vsegs[i].handleIn.add(vsegs[i].point);
    vsegs[i].handleOut.add(vsegs[i].point);
    vsegs[i].point.rot(rangle);
    vsegs[i].handleIn.rot(rangle).sub(vsegs[i].point);
    vsegs[i].handleOut.rot(rangle).sub(vsegs[i].point);
  }
}
// ----------------------------------------------
function rotSegDat2(vsegs, rangle, o, oz){
  var vo = new Vector3d(o.x, o.y, oz);
  for(var i = 0; i < vsegs.length; i++){
    vsegs[i].point.sub(vo);
    vsegs[i].handleIn.add(vsegs[i].point);
    vsegs[i].handleOut.add(vsegs[i].point);
    vsegs[i].point.rot(rangle).add(vo);
    vsegs[i].handleIn.rot(rangle).add(vo).sub(vsegs[i].point);
    vsegs[i].handleOut.rot(rangle).add(vo).sub(vsegs[i].point);
  }
}
// ----------------------------------------------
function onMouseUp(){
  var errmsg = "";
  
  if(g.sel.length > 0 && g.updated){
    var max_size = Math.max(g.sr.width, g.sr.height) * EXCESSIVE_TRANSFORM_THRESHOLD;
    var i, j;
    for(i = g.sel.length - 1; i >= 0; i--){
      if(Math.max(g.sel[i].bounds.height,
                  g.sel[i].bounds.width) > max_size){
        g.sel[i].remove();
        g.sel.remove(i);
        g.centers.remove(i);
        errmsg = "some objects were removed due to excessive transform\n";
      }
    }
    
    // fix z-order
    if(values.fix_z_order && g.last_rot != null && g.sel.length > 0){
      for(i = 0; i < g.centers.length; i++){
        g.centers[i].rot(g.last_rot);
      }
      var idx;
      for(i = 0; i < g.sel.length; i++){
        idx = -1;
        for(j = 0; j < g.sel.length; j++){
          if(i == j) continue;
          if(g.centers[i].z > g.centers[j].z){
            if(idx < 0 || g.sel[idx].isBelow(g.sel[j])){
              idx = j;
            }
          }
        }
        if(idx >= 0 && g.sel[i].isBelow(g.sel[idx])){
          g.sel[i].moveAbove(g.sel[idx]);
        }
      }
    }
  }
  
  resetValues();
  if(errmsg != ""){ Dialog.alert(errmsg); }
}
// ----------------------------------------------
function resetValues(){
  //g.sel = null;
  g.vpaths = [];
  g.centers = [];
  g.so = null;
  g.sr = null;
  g.last_pnt = null;
  g.last_rot = null;
  g.updated = false;
}
// ----------------------------------------------
function getItemSetRect(set){
  if(set.length < 1) return null;
  
  var tmp_rect = null;
  
  for(var i = set.length - 1; i >= 0; i--){
    
    // if there's a path named "origin" in the selection,
    // set its center as the rotation center
    
    if(g.so == null && set[i].name == "origin"){
      g.so = set[i].bounds.center;
      if(set.length > 1){
        set.remove(i);
        continue;
      }
    }
    with(set[i].bounds){
      if(tmp_rect == null){
        tmp_rect = [top, right, bottom, left];
      } else {
        if(top    > tmp_rect[0])  tmp_rect[0] = top;
        if(right  > tmp_rect[1])  tmp_rect[1] = right;
        if(bottom < tmp_rect[2])  tmp_rect[2] = bottom;
        if(left   < tmp_rect[3])  tmp_rect[3] = left;
      }
    }
  }
  return new Rectangle(new Point(tmp_rect[3], tmp_rect[0]),
                       new Point(tmp_rect[1], tmp_rect[2]));
}

