/*
rot3d_m
  
Scriptographer 2.0.25 script
  
Scriptographer is a plugin for Adobe Illustrator(TM)
created by Juerg Lehni
  
http://www.scriptographer.com/


This script was written by Hiroyuki Sato
http://park12.wakwak.com/~shp/cgi-bin/wiki.cgi/view/Scriptographer_en
  
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 Art ( 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 "getArtSetRect" function
2009-06-28 *removed option "min. radius" from the palette
           *modified to use matrix in actual 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)
  
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:
  
 *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;

// this scripts sets 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_rotEach(xr, yr, zr){
  this.xrot(xr); this.yrot(yr); this.zrot(zr);
  return this;
} Vector3d.prototype.rotEach =Vector3d_rotEach;
// ----------------------------------------------
function Vector3d_rot(mtx){
  var x2 = mtx[0][0] * this.x + mtx[0][1] * this.y + mtx[0][2] * this.z;
  var y2 = mtx[1][0] * this.x + mtx[1][1] * this.y + mtx[1][2] * this.z;
  var z2 = mtx[2][0] * this.x + mtx[2][1] * this.y + mtx[2][2] * this.z;
  this.x = x2; this.y = y2; this.z = z2;
  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;
// ----------------------------------------------
// ----------------------------------------------
function mtxMul(m, n){
  var mtx = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
  var j, k;
  for(var i=0; i<3; i++){
    for(j=0; j<3; j++){
      for(k=0; k<3; k++){
        mtx[i][j] += m[i][k] * n[k][j];
      }
    }
  }
  return mtx;
}
// ----------------------------------------------
function getMatrix(xr, yr, zr){
  var cx = Math.cos(xr);   var sx = Math.sin(xr);
  var cy = Math.cos(yr);   var sy = Math.sin(yr);
  var cz = Math.cos(zr);   var sz = Math.sin(zr);
  var mtx_x = [[1, 0, 0],
               [0, cx, -sx],
               [0, sx, cx]];
  var mtx_y = [[cy, 0, sy],
               [0, 1, 0],
               [-sy, 0, cy]];
  var mtx_z = [[cz, -sz, 0],
               [sz, cz, 0],
               [0, 0, 1]];
  return mtxMul( mtxMul(mtx_x, mtx_y), mtx_z);
}
// ----------------------------------------------
// ----------------------------------------------

var sel, sr, so, so2, inv_radius, vts;
var fpnt, global_depth;
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;
// ----------------------------------------------
function onInit() {
  fpnt = 400;         // distance to focal point
  global_depth = 0;   // default depth
  
  last_pnt = null;
  updated = false;
  sel = [];
  vts = [];
  
  // show dialog at start
  onOptions();
}
// ----------------------------------------------
function onOptions(){
  if( g_myDialog ){
    g_myDialog.dialog.destroy();
    g_myDialog = null;
  }
  
  g_myDialog = myDialog("rot3d_m:",[
    {description:"focal length", value:fpnt},
    {description:"global depth", value:global_depth}
    ]);
}
// ---------------------
function getValuesFromDialog(){
  var opt = myDialog_getValues();
  if( opt != null ){
    fpnt = opt[0];
    global_depth = opt[1];
    return true;
  }
  return false;
}
// ----------------------------------------------
function onMouseDown(event){
  with( activeDocument.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 = activeDocument.getMatchingItems(Path, {selected: true});
  sr = getArtSetRect(sel);
  if(sr == null) return;
  if(so == null) so = sr.center;
  so2 = so.multiply(-1);
  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;
  var mtx;
  
  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)]);
    }
    
    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 ); }
    vts.push(ar);
  }
  
  last_pnt = event.point;
  updated = false;
}
// ----------------------------------------------
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.geometricBounds.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.geometricBounds.right;
      if(sname.match(/R(\d+)/)){ opts.center.x -= (RegExp.$1 - 0); }
    } else { // L
      opts.center.x = obj.geometricBounds.left;
      if(sname.match(/L(\d+)/)){ opts.center.x += (RegExp.$1 - 0); }
    }
  }
  
  // TB & offset
  if(sname.match(/([TB])/)){
    if(RegExp.$1 == "T"){
      opts.center.y = obj.geometricBounds.top;
      if(sname.match(/T(\d+)/)){ opts.center.y -= (RegExp.$1 - 0); }
    } else { // B
      opts.center.y = obj.geometricBounds.bottom;
      if(sname.match(/B(\d+)/)){ opts.center.y += (RegExp.$1 - 0); }
    }
  }

  // Z & offset
  if(sname.match(/Z(-?\d+)/)){ oz = RegExp.$1 - 0; }

  // actual rotation
  if(opts.xr!=0 || opts.yr!=0 || opts.zr!=0){
    rotSegDat2(ar, opts.xr, opts.yr, opts.zr, opts.center.subtract(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 = v0.clone();
    vectHeight.z = RegExp.$1 - 0;
    for(var j = 0; j < ar.length; j++){ ar[j][0].add( vectHeight ); }
  }
  
  if( obj.parent instanceof Art ){
    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 Art ){
    getRotationFromName(obj.parent, opts);
  }
}
// ----------------------------------------------
function toRad(dg){ return (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 vx;
  var v = last_pnt.subtract(so).multiply(inv_radius);
  if(v.getDistance(p0) > 0.999){
    v = v.normalize().multiply(0.999);
  }
  // rotation angles
  var yr = Math.asin(v.x);
  if(isNaN(yr)) return;
  var xr = - Math.asin(v.y / Math.sin(Math.acos(v.x)));
  if(isNaN(xr)) return;
  var zr = 0;
  
  var mtx = getMatrix(xr, yr, zr);
  
  var j, vp, vpr, vpl, 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];
      vpr = vts[i][j][1];
      vpl = vts[i][j][2];
      
      pnt = view3d(vp, mtx, v0, p0);
      seg[j].setPoint( pnt.add(so) );
      seg[j].setHandleIn( view3d(vpr, mtx, vp, pnt) );
      seg[j].setHandleOut( view3d(vpl, mtx, vp, pnt) );
    }
  }
  
  updated = true;
}
// ----------------------------------------------
function view3d(vt, mtx, v2, p){
  var vect = vt.clone().add(v2);
  vect.rot(mtx);
  
  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].rotEach(xr, yr, zr);
    ar[i][1].rotEach(xr, yr, zr).sub(ar[i][0]);
    ar[i][2].rotEach(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].rotEach(xr, yr, zr).add(vo);
    ar[i][1].rotEach(xr, yr, zr).add(vo).sub(ar[i][0]);
    ar[i][2].rotEach(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;
    
    for(var i = sel.length - 1; i >= 0; i--){
      if(Math.max(sel[i].geometricBounds.height,
                  sel[i].geometricBounds.width) > max_size){
        sel[i].remove();
        sel.remove(i);
        errmsg = "some objects were removed due to excessive transform\n";
      }
    }
  }
  
  resetValues();
  if(errmsg != ""){ Dialog.alert(errmsg); }
}
// ----------------------------------------------
function resetValues(){
  //sel = null;
  vts = [];
  so = null;
  sr = null;
  last_pnt = null;
  updated = false;
}
// ----------------------------------------------
function getArtSetRect(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].geometricBounds.center;
      if(set.length > 1){
        set.remove(i);
        continue;
      }
    }
    with(set[i].geometricBounds){
      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(FloatingDialog.OPTION_TABBED);
  this.dialog.title = title
  this.textedits = [];
  this.max_caption_width = 0;

  // parts --------------------------------------
  this.addStatic = function(elem, left, top){
    var st = new Static(this.dialog);
    st.setText(elem["description"]);
    st.setSize(st.bestSize);
    st.setPosition(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, { units:TextValueItem.UNITS_NO });
    te.setText(elem["value"]);
    te.setSize(size);
    te.setPosition(left, top);
    this.textedits.push(te);
  }

  // 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++){
    this.addTextEdit(elems[i], textedit_size, left,
                     vertical_interval * i + margin - 2);
  }
  
  var button_height = 0;
  
  this.dialog.setSize(left + textedit_size.width + margin*2,
                      vertical_interval * elems.length + margin*2
                      + button_height);
  return this;
}
// ------------------------------------
function myDialog_getValues(){
  var ar = [];
  var txt;
  for(var i=0; i<g_myDialog.textedits.length; i++){
    txt = g_myDialog.textedits[i].stringValue;
    try{
      ar.push(eval(txt) - 0);
    }catch(e){
      Dialog.alert(e);
      return null;
    }
  }
  return ar;
}
// ==================================================================

