/*
rot3d
  
Scriptographer 2.6.036 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://lines-about-to-be-generated.blogspot.com/
  
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
-----------------------------------------------------------
  
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)

    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.
*/

var excessive_transform_threshold = 2.0;

// set max( this value, max(selection.width, selection.height)) to virtual sphere's radius
var min_radius = 100;

// ----------------------------------------------
function Vector3d(x,y,z){
  this.x = x; this.y = y; this.z = z;
  return this;
}
// ----------------------------------------------
function Vector3d_clone(){
  return new Vector3d(this.x, this.y, this.z);
} Vector3d.prototype.clone =Vector3d_clone;
// ----------------------------------------------
function Vector3d_add(vt){
  this.x += vt.x; this.y += vt.y; this.z += vt.z;
  return this;
} Vector3d.prototype.add =Vector3d_add;
// ----------------------------------------------
function Vector3d_sub(vt){
  this.x -= vt.x; this.y -= vt.y; this.z -= vt.z;
  return this;
} Vector3d.prototype.sub =Vector3d_sub;
// ----------------------------------------------
function Vector3d_xrot(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;
} Vector3d.prototype.xrot =Vector3d_xrot;
// ----------------------------------------------
function Vector3d_yrot(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;
} Vector3d.prototype.yrot =Vector3d_yrot;
// ----------------------------------------------
function Vector3d_zrot(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;
} Vector3d.prototype.zrot =Vector3d_zrot;
// ----------------------------------------------
function Vector3d_rot(xr, yr, zr){
  this.xrot(xr); this.yrot(yr); this.zrot(zr);
  return this;
} Vector3d.prototype.rot =Vector3d_rot;
// ----------------------------------------------
function Vector3d_to2dz(f){
  this.x = f * this.x / (f - this.z);
  this.y = f * this.y /(f - this.z);
  return this;
} Vector3d.prototype.to2dz =Vector3d_to2dz;
// ----------------------------------------------
// ----------------------------------------------


var sel, sr, so, so2, inv_radius, vts, centers, last_rot;
var fpnt, global_depth, fix_z_order;
var last_pnt, updated;

var hpi = Math.PI / 2;
var wpi = Math.PI * 2;
var mpi = Math.PI;

var p0 = new Point(0,0);
var v0 = new Vector3d(0,0,0);
var g_myDialog;
// ----------------------------------------------

fpnt = 400;         // distance to focal point
global_depth = 0;   // default depth

last_pnt = null;
updated = false;
sel = [];
vts = [];
centers = [];
last_rot = null;
fix_z_order = true;

// show dialog at start
onOptions();

// ----------------------------------------------
function onOptions(){
  if( g_myDialog ){
    g_myDialog.dialog.destroy();
    g_myDialog = null;
  }
  
  g_myDialog = myDialog("rot3d:",[
    {description:"focal length", value:fpnt},
    {description:"global depth", value:global_depth},
    {description:"fix z-order", type:"checkbox", checked:fix_z_order}
    ]);
}
// ---------------------
function getValuesFromDialog(){
  var opt = myDialog_getValues();
  if( opt != null ){
    fpnt = opt[0];
    global_depth = opt[1];
    
    fix_z_order = opt[2];
    return true;
  }
  return false;
}
// ----------------------------------------------
function disposeDialog(){
    if(g_myDialog){
        g_myDialog.dialog.destroy();
        g_myDialog = null;
    }
}
// ----------------------------------------------
function onSelect(){ onOptions(); }
// ----------------------------------------------
function onStop(){ disposeDialog(); }
// ----------------------------------------------
function onDeselect(){ disposeDialog(); }
// ----------------------------------------------
function onMouseDown(event){
  with( document.activeLayer ){
    if( locked || hidden ){
      Dialog.alert("please unlock and show the active layer");
      return; }
  }
  // get values from dialog
  if( ! getValuesFromDialog() ){
    resetValues();
    return;
  }
  
  //activeDocument.redraw();
  sel = document.getItems(Path, {selected: true});
  sr = getItemSetRect(sel);
  if(sr == null) return;
  if(so == null) so = sr.center;
  so2 = -so;
  inv_radius = 1 / (Math.max(Math.max(sr.width, sr.height) / 2, min_radius));
  
  var ar, j, vt;
  var opts = {z:0, xr:0, yr:0, zr:0, xor:0, yor:0, zor:0, center:null}
  var sname, seg;
  var vectHeight = v0.clone();
  vectHeight.z = - global_depth;
  
  for(var i=0; i < sel.length; i++){
    opts.xor = 0;
    opts.yor = 0;
    opts.zor = 0;
        
    ar = [];
    
    for(j = 0; j < sel[i].segments.length; j++){
      seg = sel[i].segments[j];
      ar.push([new Vector3d(seg.point.x - so.x, seg.point.y - so.y, 0),
               new Vector3d(seg.handleIn.x,  seg.handleIn.y, 0),
               new Vector3d(seg.handleOut.x, seg.handleOut.y, 0)]);
    }
    // center
    ar.push([new Vector3d(sel[i].bounds.center.x - so.x,
                          sel[i].bounds.center.y - so.y, 0),
             v0.clone(), v0.clone()]);
    
    getSelfRotationFromName(sel[i], ar, opts);
    
    getRotationFromName(sel[i], opts);
    if(opts.xor!=0 || opts.yor!=0 || opts.zor!=0){
      rotSegDat(ar, opts.xor, opts.yor, opts.zor);
    }

    for(var j = 0; j < ar.length; j++){
        ar[j][0].add( vectHeight );
    }
    centers.push( ar.pop()[0] ); // center
    vts.push(ar);
  }
  
  last_pnt = event.point;
  updated = false;
}
// ----------------------------------------------
function getHeightFromName(obj, opts){
  if( obj instanceof Layer) return;
  
  var sname = obj.name;
  
  if( obj.parent instanceof Item ){
    getHeightFromName(obj.parent, opts);
  }
}
// ----------------------------------------------
function getSelfRotationFromName(obj, ar, opts){
  if( obj instanceof Layer) return;
  
  var sname = obj.name;

  opts.xr = 0;
  opts.yr = 0;
  opts.zr = 0;
  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.xr += toRad(RegExp.$1); }
  if(sname.match(/y(-?\d+)/)){ opts.yr += toRad(RegExp.$1); }
  if(sname.match(/z(-?\d+)/)){ opts.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.xr!=0 || opts.yr!=0 || opts.zr!=0){
    rotSegDat2(ar, opts.xr, opts.yr, opts.zr,
               opts.center - 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 < ar.length; j++){
        ar[j][0].z += vectHeight;
    }
  }
  
  if( obj.parent instanceof Item ){
    getSelfRotationFromName(obj.parent, ar, opts);
  }
}
// ----------------------------------------------
function getRotationFromName(obj, opts){
  if( obj instanceof Layer) return;
  
  var sname = obj.name;
  
  // xoN: x-axis rotation. uses global origin (default: center of the selection)
  if(sname.match(/xo(-?\d+)/)){ opts.xor += toRad(RegExp.$1); }
  if(sname.match(/yo(-?\d+)/)){ opts.yor += toRad(RegExp.$1); }
  if(sname.match(/zo(-?\d+)/)){ opts.zor += toRad(RegExp.$1); }
  
  if( obj.parent instanceof Item ){
    getRotationFromName(obj.parent, opts);
  }
}
// ----------------------------------------------
function toRad(dg){ return ( parseFloat(dg) - 0 ) / 180 * Math.PI; }
// ----------------------------------------------
function onMouseDrag(event){
  if(last_pnt == null || last_pnt.getDistance(event.point) < 1) return;
  last_pnt = event.point;
  
  if(sel.length < 1 || sr == null) return;
  
  var v = (last_pnt - so) * inv_radius;
  if(v.getDistance(p0) > 0.999) v = v.normalize() * 0.999;

  // rotation angles
  var t = Math.asin(v.y);
  if(isNaN(t)) return;
  var xr = -t;
  var yr = Math.asin(v.x / Math.cos(t));
  if(isNaN(yr)) return;
  var zr = 0;
  
  var j, vp, seg, pnt;
  
  for(var i = 0; i< sel.length; i++){
    seg = sel[i].segments;
    
    for(j = 0; j < seg.length; j++){
      vp = vts[i][j][0]; //.clone();
      pnt = view3d(vts[i][j][0], xr, yr, zr, v0, p0);
      seg[j].point = pnt + so;
      seg[j].handleIn = view3d(vts[i][j][1], xr, yr, zr, vp, pnt);
      seg[j].handleOut = view3d(vts[i][j][2], xr, yr, zr, vp, pnt);
    }
  }
  last_rot = [xr, yr, zr];
  
  updated = true;
}
// ----------------------------------------------
function view3d(vt, xr, yr, zr, v2, p){
  var vect = vt.clone().add(v2);
  vect.rot(xr, yr, zr);
  vect.to2dz(fpnt);
  return new Point(vect.x - p.x, vect.y - p.y);
}
// ----------------------------------------------
function rotSegDat(ar, xr, yr, zr){
  for(var i = 0; i < ar.length; i++){
    ar[i][1].add(ar[i][0]);
    ar[i][2].add(ar[i][0]);
    ar[i][0].rot(xr, yr, zr);
    ar[i][1].rot(xr, yr, zr).sub(ar[i][0]);
    ar[i][2].rot(xr, yr, zr).sub(ar[i][0]);
  }
}
// ----------------------------------------------
function rotSegDat2(ar, xr, yr, zr, o, oz){
  var vo = new Vector3d(o.x, o.y, oz);
  for(var i = 0; i < ar.length; i++){
    ar[i][0].sub(vo);
    ar[i][1].add(ar[i][0]);
    ar[i][2].add(ar[i][0]);
    ar[i][0].rot(xr, yr, zr).add(vo);
    ar[i][1].rot(xr, yr, zr).add(vo).sub(ar[i][0]);
    ar[i][2].rot(xr, yr, zr).add(vo).sub(ar[i][0]);
  }
}
// ----------------------------------------------
function onMouseUp(){
  var errmsg = "";
  
  if(sel.length > 0 && updated){
    var max_size = Math.max(sr.width, sr.height) * excessive_transform_threshold;
    var i, j;
    for(i = sel.length - 1; i >= 0; i--){
      if(Math.max(sel[i].bounds.height,
                  sel[i].bounds.width) > max_size){
        sel[i].remove();
        sel.remove(i);
        centers.remove(i);
        errmsg = "some objects were removed due to excessive transform\n";
      }
    }
    // fix z-order
    if(fix_z_order && last_rot != null && sel.length > 0){
      for(i = 0; i < centers.length; i++){
        centers[i].rot(last_rot[0], last_rot[1], last_rot[2]);
      }
      var idx;
      for(i = 0; i < sel.length; i++){
        idx = -1;
        for(j = 0; j < sel.length; j++){
          if(i == j) continue;
          if(centers[i].z > centers[j].z){
            if(idx < 0 || sel[idx].isBelow(sel[j])){
              idx = j;
            }
          }
        }
        if(idx >= 0 && sel[i].isBelow(sel[idx])){
          sel[i].moveAbove(sel[idx]);
        }
      }
    }
  }
  
  resetValues();
  if(errmsg != ""){ Dialog.alert(errmsg); }
}
// ----------------------------------------------
function resetValues(){
  //sel = null;
  vts = [];
  centers = [];
  so = null;
  sr = null;
  last_pnt = null;
  last_rot = null;
  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(so == null && set[i].name == "origin"){
      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]));
}

// ==================================================================
// function for floating dialog
function myDialog(title, elems){
  this.dialog = new FloatingDialog('tabbed') {
    title: title
  };
  this.textedits = [];
  this.checkboxes = [];
  this.max_caption_width = 0;

  // parts --------------------------------------
  this.addStatic = function(elem, left, top){
    var st = new TextPane(this.dialog) {
      text: elem["description"],
      position: [left, top]
    };
    if(st.size.width > this.max_caption_width){
      this.max_caption_width = st.size.width;
    }
  }

  this.addTextEdit = function(elem, size, left, top){
    var te = new TextEdit(this.dialog) {
      text: elem["value"],
      size: size,
      position: [left, top],
    };
    this.textedits.push(te);
  }

  this.addCheckBox = function(elem, left, top){
    var cb = new CheckBox(this.dialog) {
      checked: elem.checked,
      position: [left, top]
    };
    this.checkboxes.push(cb);
  }
  // layout --------------------------------------
  // constants
  var vertical_interval = 24;
  var caption_and_textedit_interval = 4;
  var margin = 10;
  var left = margin;
  
  var textedit_size = new Size(50, 20);
  var button_size = new Size(50, 20);
  
  // ------------------------
  for(var i=0; i<elems.length; i++){
    this.addStatic(elems[i], left, vertical_interval * i + margin);
  }
  left += this.max_caption_width + caption_and_textedit_interval;
  for(var i=0; i<elems.length; i++){
    if(elems[i].type == "checkbox"){
      this.addCheckBox(elems[i], left,
                       vertical_interval * i + margin);
    } else {
      this.addTextEdit(elems[i], textedit_size, left,
                       vertical_interval * i + margin - 2);
    }
  }
  
  var button_height = 0;
  
  this.dialog.size = new Size(left + textedit_size.width,
                      vertical_interval * elems.length
                      + button_height) + margin * 2;
  return this;
}
// ------------------------------------
function myDialog_getValues(){
  var ar = [];
  var i, val;
  for(i=0; i<g_myDialog.textedits.length; i++){
    val = g_myDialog.textedits[i].stringValue;
    try{
      ar.push(eval(val) - 0);
    }catch(e){
      Dialog.alert(e);
      return null;
    }
  }
  for(i=0; i<g_myDialog.checkboxes.length; i++){
    ar.push(g_myDialog.checkboxes[i].checked);
  }
  return ar;
}
// ==================================================================

