// Three-dimensional graphing routines

private import math;
import graph;
import three;

typedef triple direction3(real);
direction3 Dir(triple dir) {return new triple(real) {return dir;};}

ticklocate ticklocate(real a, real b, autoscaleT S=defaultS,
                      real tickmin=-infinity, real tickmax=infinity,
                      real time(real)=null, direction3 dir)
{
  if((valuetime) time == null) time=linear(S.T(),a,b);
  ticklocate locate;
  locate.a=a;
  locate.b=b;
  locate.S=S.copy();
  if(finite(tickmin)) locate.S.tickMin=tickmin;
  if(finite(tickmax)) locate.S.tickMax=tickmax;
  locate.time=time;
  locate.dir=zero;
  locate.dir3=dir;
  return locate;
}

private struct locateT {
  real t;         // tick location time
  triple V;       // tick location in frame coordinates
  triple pathdir; // path direction in frame coordinates
  triple dir;     // tick direction in frame coordinates

  void dir(transform3 T, path3 g, ticklocate locate, real t) {
    pathdir=unit(shiftless(T)*dir(g,t));
    triple Dir=locate.dir3(t);
    dir=unit(Dir);
  }
  // Locate the desired position of a tick along a path.
  void calc(transform3 T, path3 g, ticklocate locate, real val) {
    t=locate.time(val);
    V=T*point(g,t);
    dir(T,g,locate,t);
  }
}

void drawtick(picture pic, transform3 T, path3 g, path3 g2,
              ticklocate locate, real val, real Size, int sign, pen p,
              bool extend)
{
  locateT locate1,locate2;
  locate1.calc(T,g,locate,val);
  path3 G;
  if(extend && size(g2) > 0) {
    locate2.calc(T,g2,locate,val);
    G=locate1.V--locate2.V;
  } else
    G=(sign == 0) ?
      locate1.V-Size*locate1.dir--locate1.V+Size*locate1.dir :
      locate1.V--locate1.V+Size*sign*locate1.dir;
  draw(pic,G,p,name="tick");
}

triple ticklabelshift(triple align, pen p=currentpen)
{
  return 0.25*unit(align)*labelmargin(p);
}

// Signature of routines that draw labelled paths with ticks and tick labels.
typedef void ticks3(picture, transform3, Label, path3, path3, pen,
                    arrowbar3, margin3, ticklocate, int[], bool opposite=false,
                    bool primary=true, projection P);

// Label a tick on a frame.
void labeltick(picture pic, transform3 T, path3 g,
               ticklocate locate, real val, int sign, real Size,
               ticklabel ticklabel, Label F, real norm=0)
{
  locateT locate1;
  locate1.calc(T,g,locate,val);
  triple align=F.align.dir3;
  if(align == O) align=sign*locate1.dir;

  triple shift=align*labelmargin(F.p);
  if(dot(align,sign*locate1.dir) >= 0)
    shift=sign*(Size)*locate1.dir;

  real label;
  if(locate.S.scale.logarithmic)
    label=locate.S.scale.Tinv(val);
  else {
    label=val;
    if(abs(label) < zerotickfuzz*norm) label=0;
    // Fix epsilon errors at +/-1e-4
    // default format changes to scientific notation here
    if(abs(abs(label)-1e-4) < epsilon) label=sgn(label)*1e-4;
  }

  string s=ticklabel(label);
  triple v=locate1.V+shift;
  if(s != "")
    label(pic,F.defaulttransform3 ? baseline(s,baselinetemplate) : F.T3*s,v,
          align,F.p);
}

// Add axis label L to frame f.
void labelaxis(picture pic, transform3 T, Label L, path3 g,
               ticklocate locate=null, int sign=1, bool ticklabels=false)
{
  triple m=pic.min(identity4);
  triple M=pic.max(identity4);
  triple align=L.align.dir3;
  Label L=L.copy();

  pic.add(new void(frame f, transform3 T, picture pic2, projection P) {
      path3 g=T*g;
      real t=relative(L,g);
      triple v=point(g,t);
      picture F;
      if(L.align.dir3 == O)
        align=unit(invert(L.align.dir,v,P))*abs(L.align.dir);

      if(ticklabels && locate != null && piecewisestraight(g)) {
        locateT locate1;
        locate1.dir(T,g,locate,t);
        triple pathdir=locate1.pathdir;

        triple perp=cross(pathdir,P.normal);
        if(align == O)
          align=unit(sgn(dot(sign*locate1.dir,perp))*perp);
        path[] g=project(box(T*m,T*M),P);
        pair z=project(v,P);
        pair Ppathdir=project(v+pathdir,P)-z;
        pair Perp=unit(I*Ppathdir);
        real angle=degrees(Ppathdir,warn=false);
        transform S=rotate(-angle,z);
        path[] G=S*g;
        pair Palign=project(v+align,P)-z;
        pair Align=rotate(-angle)*dot(Palign,Perp)*Perp;
        pair offset=unit(Palign)*
          abs((Align.y >= 0 ? max(G).y : (Align.y < 0 ? min(G).y : 0))-z.y);
        triple normal=cross(pathdir,align);
        if(normal != O) v=invert(z+offset,normal,v,P);
      }

      label(F,L,v);
      add(f,F.fit3(identity4,pic2,P));
    },exact=false);

  path3[] G=path3(texpath(L,bbox=true));
  if(G.length > 0) {
    G=L.align.is3D ? align(G,O,align,L.p) : L.T3*G;
    triple v=point(g,relative(L,g));
    pic.addBox(v,v,min(G),max(G));
  }
}

// Tick construction routine for a user-specified array of tick values.
ticks3 Ticks3(int sign, Label F="", ticklabel ticklabel=null,
              bool beginlabel=true, bool endlabel=true,
              real[] Ticks=new real[], real[] ticks=new real[], int N=1,
              bool begin=true, bool end=true,
              real Size=0, real size=0, bool extend=false,
              pen pTick=nullpen, pen ptick=nullpen)
{
  return new void(picture pic, transform3 t, Label L, path3 g, path3 g2, pen p,
                  arrowbar3 arrow, margin3 margin, ticklocate locate,
                  int[] divisor, bool opposite, bool primary,
                  projection P=currentprojection) {
    // Use local copy of context variables:
    int Sign=opposite ? -1 : 1;
    int sign=Sign*sign;
    pen pTick=pTick;
    pen ptick=ptick;
    ticklabel ticklabel=ticklabel;

    real Size=Size;
    real size=size;
    if(Size == 0) Size=Ticksize;
    if(size == 0) size=ticksize;

    Label L=L.copy();
    Label F=F.copy();
    L.p(p);
    F.p(p);
    if(pTick == nullpen) pTick=p;
    if(ptick == nullpen) ptick=pTick;

    bool ticklabels=false;
    path3 G=t*g;
    path3 G2=t*g2;

    scalefcn T;

    real a,b;
    if(locate.S.scale.logarithmic) {
      a=locate.S.postscale.Tinv(locate.a);
      b=locate.S.postscale.Tinv(locate.b);
      T=locate.S.scale.T;
    } else {
      a=locate.S.Tinv(locate.a);
      b=locate.S.Tinv(locate.b);
      T=identity;
    }

    if(a > b) {real temp=a; a=b; b=temp;}

    real norm=max(abs(a),abs(b));

    string format=autoformat(F.s,norm...Ticks);
    if(F.s == "%") F.s="";
    if(ticklabel == null) {
      if(locate.S.scale.logarithmic) {
        int base=round(locate.S.scale.Tinv(1));
        ticklabel=format == "%" ? Format("") : DefaultLogFormat(base);
      } else ticklabel=Format(format);
    }

    bool labelaxis=L.s != "" && primary;

    begingroup3(pic,"axis");

    if(primary) draw(pic,margin(G,p).g,p,arrow);
    else draw(pic,G,p);

    for(int i=(begin ? 0 : 1); i < (end ? Ticks.length : Ticks.length-1); ++i) {
      real val=T(Ticks[i]);
      if(val >= a && val <= b)
        drawtick(pic,t,g,g2,locate,val,Size,sign,pTick,extend);
    }
    for(int i=0; i < ticks.length; ++i) {
      real val=T(ticks[i]);
      if(val >= a && val <= b)
        drawtick(pic,t,g,g2,locate,val,size,sign,ptick,extend);
    }

    if(N == 0) N=1;
    if(Size > 0 && primary) {
      for(int i=(beginlabel ? 0 : 1);
          i < (endlabel ? Ticks.length : Ticks.length-1); i += N) {
        real val=T(Ticks[i]);
        if(val >= a && val <= b) {
          ticklabels=true;
          labeltick(pic,t,g,locate,val,Sign,Size,ticklabel,F,norm);
        }
      }
    }
    if(labelaxis)
      labelaxis(pic,t,L,G,locate,Sign,ticklabels);

    endgroup3(pic);
  };
}

// Automatic tick construction routine.
ticks3 Ticks3(int sign, Label F="", ticklabel ticklabel=null,
              bool beginlabel=true, bool endlabel=true,
              int N, int n=0, real Step=0, real step=0,
              bool begin=true, bool end=true, tickmodifier modify=None,
              real Size=0, real size=0, bool extend=false,
              pen pTick=nullpen, pen ptick=nullpen)
{
  return new void(picture pic, transform3 T, Label L,
                  path3 g, path3 g2, pen p,
                  arrowbar3 arrow, margin3 margin=NoMargin3, ticklocate locate,
                  int[] divisor, bool opposite, bool primary,
                  projection P=currentprojection) {
    path3 G=T*g;
    real limit=Step == 0 ? axiscoverage*arclength(G) : 0;
    tickvalues values=modify(generateticks(sign,F,ticklabel,N,n,Step,step,
                                           Size,size,identity(),1,
                                           project(G,P),
                                           limit,p,locate,divisor,
                                           opposite));
    Ticks3(sign,F,ticklabel,beginlabel,endlabel,values.major,values.minor,
           values.N,begin,end,Size,size,extend,pTick,ptick)
      (pic,T,L,g,g2,p,arrow,margin,locate,divisor,opposite,primary,P);
  };
}

ticks3 NoTicks3()
{
  return new void(picture pic, transform3 T, Label L, path3 g,
                  path3, pen p, arrowbar3 arrow, margin3 margin,
                  ticklocate, int[], bool opposite, bool primary,
                  projection P=currentprojection) {
    path3 G=T*g;
    if(primary) draw(pic,margin(G,p).g,p,arrow,margin);
    else draw(pic,G,p);
    if(L.s != "" && primary) {
      Label L=L.copy();
      L.p(p);
      labelaxis(pic,T,L,G,opposite ? -1 : 1);
    }
  };
}

ticks3 InTicks(Label format="", ticklabel ticklabel=null,
               bool beginlabel=true, bool endlabel=true,
               int N=0, int n=0, real Step=0, real step=0,
               bool begin=true, bool end=true, tickmodifier modify=None,
               real Size=0, real size=0, bool extend=false,
               pen pTick=nullpen, pen ptick=nullpen)
{
  return Ticks3(-1,format,ticklabel,beginlabel,endlabel,N,n,Step,step,
                begin,end,modify,Size,size,extend,pTick,ptick);
}

ticks3 OutTicks(Label format="", ticklabel ticklabel=null,
                bool beginlabel=true, bool endlabel=true,
                int N=0, int n=0, real Step=0, real step=0,
                bool begin=true, bool end=true, tickmodifier modify=None,
                real Size=0, real size=0, bool extend=false,
                pen pTick=nullpen, pen ptick=nullpen)
{
  return Ticks3(1,format,ticklabel,beginlabel,endlabel,N,n,Step,step,
                begin,end,modify,Size,size,extend,pTick,ptick);
}

ticks3 InOutTicks(Label format="", ticklabel ticklabel=null,
                  bool beginlabel=true, bool endlabel=true,
                  int N=0, int n=0, real Step=0, real step=0,
                  bool begin=true, bool end=true, tickmodifier modify=None,
                  real Size=0, real size=0, bool extend=false,
                  pen pTick=nullpen, pen ptick=nullpen)
{
  return Ticks3(0,format,ticklabel,beginlabel,endlabel,N,n,Step,step,
                begin,end,modify,Size,size,extend,pTick,ptick);
}

ticks3 InTicks(Label format="", ticklabel ticklabel=null,
               bool beginlabel=true, bool endlabel=true,
               real[] Ticks, real[] ticks=new real[],
               real Size=0, real size=0, bool extend=false,
               pen pTick=nullpen, pen ptick=nullpen)
{
  return Ticks3(-1,format,ticklabel,beginlabel,endlabel,
                Ticks,ticks,Size,size,extend,pTick,ptick);
}

ticks3 OutTicks(Label format="", ticklabel ticklabel=null,
                bool beginlabel=true, bool endlabel=true,
                real[] Ticks, real[] ticks=new real[],
                real Size=0, real size=0, bool extend=false,
                pen pTick=nullpen, pen ptick=nullpen)
{
  return Ticks3(1,format,ticklabel,beginlabel,endlabel,
                Ticks,ticks,Size,size,extend,pTick,ptick);
}

ticks3 InOutTicks(Label format="", ticklabel ticklabel=null,
                  bool beginlabel=true, bool endlabel=true,
                  real[] Ticks, real[] ticks=new real[],
                  real Size=0, real size=0, bool extend=false,
                  pen pTick=nullpen, pen ptick=nullpen)
{
  return Ticks3(0,format,ticklabel,beginlabel,endlabel,
                Ticks,ticks,Size,size,extend,pTick,ptick);
}

ticks3 NoTicks3=NoTicks3(),
InTicks=InTicks(),
OutTicks=OutTicks(),
InOutTicks=InOutTicks();

triple tickMin3(picture pic)
{
  return minbound(pic.userMin(),(pic.scale.x.tickMin,pic.scale.y.tickMin,
                                 pic.scale.z.tickMin));
}

triple tickMax3(picture pic)
{
  return maxbound(pic.userMax(),(pic.scale.x.tickMax,pic.scale.y.tickMax,
                                 pic.scale.z.tickMax));
}

axis Bounds(int type=Both, int type2=Both, triple align=O, bool extend=false)
{
  return new void(picture pic, axisT axis) {
    axis.type=type;
    axis.type2=type2;
    axis.position=0.5;
    axis.align=align;
    axis.extend=extend;
  };
}

axis YZEquals(real y, real z, triple align=O, bool extend=false)
{
  return new void(picture pic, axisT axis) {
    axis.type=Value;
    axis.type2=Value;
    axis.value=pic.scale.y.T(y);
    axis.value2=pic.scale.z.T(z);
    axis.position=1;
    axis.align=align;
    axis.extend=extend;
  };
}

axis XZEquals(real x, real z, triple align=O, bool extend=false)
{
  return new void(picture pic, axisT axis) {
    axis.type=Value;
    axis.type2=Value;
    axis.value=pic.scale.x.T(x);
    axis.value2=pic.scale.z.T(z);
    axis.position=1;
    axis.align=align;
    axis.extend=extend;
  };
}

axis XYEquals(real x, real y, triple align=O, bool extend=false)
{
  return new void(picture pic, axisT axis) {
    axis.type=Value;
    axis.type2=Value;
    axis.value=pic.scale.x.T(x);
    axis.value2=pic.scale.y.T(y);
    axis.position=1;
    axis.align=align;
    axis.extend=extend;
  };
}

axis YZZero(triple align=O, bool extend=false)
{
  return new void(picture pic, axisT axis) {
    axis.type=Value;
    axis.type2=Value;
    axis.value=pic.scale.y.T(pic.scale.y.scale.logarithmic ? 1 : 0);
    axis.value2=pic.scale.z.T(pic.scale.z.scale.logarithmic ? 1 : 0);
    axis.position=1;
    axis.align=align;
    axis.extend=extend;
  };
}

axis XZZero(triple align=O, bool extend=false)
{
  return new void(picture pic, axisT axis) {
    axis.type=Value;
    axis.type2=Value;
    axis.value=pic.scale.x.T(pic.scale.x.scale.logarithmic ? 1 : 0);
    axis.value2=pic.scale.z.T(pic.scale.z.scale.logarithmic ? 1 : 0);
    axis.position=1;
    axis.align=align;
    axis.extend=extend;
  };
}

axis XYZero(triple align=O, bool extend=false)
{
  return new void(picture pic, axisT axis) {
    axis.type=Value;
    axis.type2=Value;
    axis.value=pic.scale.x.T(pic.scale.x.scale.logarithmic ? 1 : 0);
    axis.value2=pic.scale.y.T(pic.scale.y.scale.logarithmic ? 1 : 0);
    axis.position=1;
    axis.align=align;
    axis.extend=extend;
  };
}

axis
Bounds=Bounds(),
YZZero=YZZero(),
XZZero=XZZero(),
XYZero=XYZero();

// Draw a general three-dimensional axis.
void axis(picture pic=currentpicture, Label L="", path3 g, path3 g2=nullpath3,
          pen p=currentpen, ticks3 ticks, ticklocate locate,
          arrowbar3 arrow=None, margin3 margin=NoMargin3,
          int[] divisor=new int[], bool above=false, bool opposite=false)
{
  Label L=L.copy();
  real t=reltime(g,0.5);
  if(L.defaultposition) L.position(t);
  divisor=copy(divisor);
  locate=locate.copy();

  pic.add(new void (picture f, transform3 t, transform3 T,
                    projection P, triple, triple) {
      picture d;
      ticks(d,t,L,g,g2,p,arrow,margin,locate,divisor,opposite,true,P);
      add(f,t*T*inverse(t)*d);
    },above=above);

  addPath(pic,g,p);

  if(L.s != "") {
    frame f;
    Label L0=L.copy();
    L0.position(0);
    add(f,L0);
    triple pos=point(g,L.relative()*length(g));
    pic.addBox(pos,pos,min3(f),max3(f));
  }
}

real xtrans(transform3 t, real x)
{
  return (t*(x,0,0)).x;
}

real ytrans(transform3 t, real y)
{
  return (t*(0,y,0)).y;
}

real ztrans(transform3 t, real z)
{
  return (t*(0,0,z)).z;
}

private triple defaultdir(triple X, triple Y, triple Z, bool opposite=false,
                          projection P) {
  triple u=cross(P.normal,Z);
  return abs(dot(u,X)) > abs(dot(u,Y)) ? -X : (opposite ? Y : -Y);
}

// An internal routine to draw an x axis at a particular y value.
void xaxis3At(picture pic=currentpicture, Label L="", axis axis,
              real xmin=-infinity, real xmax=infinity, pen p=currentpen,
              ticks3 ticks=NoTicks3,
              arrowbar3 arrow=None, margin3 margin=NoMargin3, bool above=true,
              bool opposite=false, bool opposite2=false, bool primary=true,
              projection P=currentprojection)
{
  int type=axis.type;
  int type2=axis.type2;
  triple dir=axis.align.dir3 == O ?
    defaultdir(Y,Z,X,opposite^opposite2,P) : axis.align.dir3;
  Label L=L.copy();
  if(L.align.dir3 == O && L.align.dir == 0) L.align(opposite ? -dir : dir);

  real y=axis.value;
  real z=axis.value2;
  real y2,z2;
  int[] divisor=copy(axis.xdivisor);

  pic.add(new void(picture f, transform3 t, transform3 T, projection P,
                   triple lb, triple rt) {
            transform3 tinv=inverse(t);
            triple a=xmin == -infinity ? tinv*(lb.x-min3(p).x,ytrans(t,y),
                                               ztrans(t,z)) : (xmin,y,z);
            triple b=xmax == infinity ? tinv*(rt.x-max3(p).x,ytrans(t,y),
                                              ztrans(t,z)) : (xmax,y,z);
            real y0;
            real z0;
            if(abs(dir.y) < abs(dir.z)) {
              y0=y;
              z0=z2;
            } else {
              y0=y2;
              z0=z;
            }

            triple a2=xmin == -infinity ? tinv*(lb.x-min3(p).x,ytrans(t,y0),
                                                ztrans(t,z0)) : (xmin,y0,z0);
            triple b2=xmax == infinity ? tinv*(rt.x-max3(p).x,ytrans(t,y0),
                                               ztrans(t,z0)) : (xmax,y0,z0);

            if(xmin == -infinity || xmax == infinity) {
              bounds mx=autoscale(a.x,b.x,pic.scale.x.scale);
              pic.scale.x.tickMin=mx.min;
              pic.scale.x.tickMax=mx.max;
              divisor=mx.divisor;
            }

            triple fuzz=X*epsilon*max(abs(a.x),abs(b.x));
            a -= fuzz;
            b += fuzz;

            picture d;
            ticks(d,t,L,a--b,finite(y0) && finite(z0) ? a2--b2 : nullpath3,
                  p,arrow,margin,
                  ticklocate(a.x,b.x,pic.scale.x,Dir(dir)),divisor,
                  opposite,primary,P);
            add(f,t*T*tinv*d);
          },above=above);

  void bounds() {
    if(type == Min)
      y=pic.scale.y.automin() ? tickMin3(pic).y : pic.userMin().y;
    else if(type == Max)
      y=pic.scale.y.automax() ? tickMax3(pic).y : pic.userMax().y;
    else if(type == Both) {
      y2=pic.scale.y.automax() ? tickMax3(pic).y : pic.userMax().y;
      y=opposite ? y2 :
        (pic.scale.y.automin() ? tickMin3(pic).y : pic.userMin().y);
    }

    if(type2 == Min)
      z=pic.scale.z.automin() ? tickMin3(pic).z : pic.userMin().z;
    else if(type2 == Max)
      z=pic.scale.z.automax() ? tickMax3(pic).z : pic.userMax().z;
    else if(type2 == Both) {
      z2=pic.scale.z.automax() ? tickMax3(pic).z : pic.userMax().z;
      z=opposite2 ? z2 :
        (pic.scale.z.automin() ? tickMin3(pic).z : pic.userMin().z);
    }

    real Xmin=finite(xmin) ? xmin : pic.userMin().x;
    real Xmax=finite(xmax) ? xmax : pic.userMax().x;

    triple a=(Xmin,y,z);
    triple b=(Xmax,y,z);
    triple a2=(Xmin,y2,z2);
    triple b2=(Xmax,y2,z2);

    if(finite(a)) {
      pic.addPoint(a,min3(p));
      pic.addPoint(a,max3(p));
    }

    if(finite(b)) {
      pic.addPoint(b,min3(p));
      pic.addPoint(b,max3(p));
    }

    if(finite(a) && finite(b)) {
      picture d;
      ticks(d,pic.scaling3(warn=false),L,
            (a.x,0,0)--(b.x,0,0),(a2.x,0,0)--(b2.x,0,0),p,arrow,margin,
            ticklocate(a.x,b.x,pic.scale.x,Dir(dir)),divisor,
            opposite,primary,P);
      frame f;
      if(L.s != "") {
        Label L0=L.copy();
        L0.position(0);
        add(f,L0);
      }
      triple pos=a+L.relative()*(b-a);
      triple m=min3(d);
      triple M=max3(d);
      pic.addBox(pos,pos,(min3(f).x,m.y,m.z),(max3(f).x,m.y,m.z));
    }
  }

  // Process any queued y and z axes bound calculation requests.
  for(int i=0; i < pic.scale.y.bound.length; ++i)
    pic.scale.y.bound[i]();
  for(int i=0; i < pic.scale.z.bound.length; ++i)
    pic.scale.z.bound[i]();

  pic.scale.y.bound.delete();
  pic.scale.z.bound.delete();

  bounds();

  // Request another x bounds calculation before final picture scaling.
  pic.scale.x.bound.push(bounds);
}

// An internal routine to draw a y axis at a particular value.
void yaxis3At(picture pic=currentpicture, Label L="", axis axis,
              real ymin=-infinity, real ymax=infinity, pen p=currentpen,
              ticks3 ticks=NoTicks3,
              arrowbar3 arrow=None, margin3 margin=NoMargin3, bool above=true,
              bool opposite=false, bool opposite2=false, bool primary=true,
              projection P=currentprojection)
{
  int type=axis.type;
  int type2=axis.type2;
  triple dir=axis.align.dir3 == O ?
    defaultdir(X,Z,Y,opposite^opposite2,P) : axis.align.dir3;
  Label L=L.copy();
  if(L.align.dir3 == O && L.align.dir == 0) L.align(opposite ? -dir : dir);

  real x=axis.value;
  real z=axis.value2;
  real x2,z2;
  int[] divisor=copy(axis.ydivisor);

  pic.add(new void(picture f, transform3 t, transform3 T, projection P,
                   triple lb, triple rt) {
            transform3 tinv=inverse(t);
            triple a=ymin == -infinity ? tinv*(xtrans(t,x),lb.y-min3(p).y,
                                               ztrans(t,z)) : (x,ymin,z);
            triple b=ymax == infinity ? tinv*(xtrans(t,x),rt.y-max3(p).y,
                                              ztrans(t,z)) : (x,ymax,z);
            real x0;
            real z0;
            if(abs(dir.x) < abs(dir.z)) {
              x0=x;
              z0=z2;
            } else {
              x0=x2;
              z0=z;
            }

            triple a2=ymin == -infinity ? tinv*(xtrans(t,x0),lb.y-min3(p).y,
                                                ztrans(t,z0)) : (x0,ymin,z0);
            triple b2=ymax == infinity ? tinv*(xtrans(t,x0),rt.y-max3(p).y,
                                               ztrans(t,z0)) : (x0,ymax,z0);

            if(ymin == -infinity || ymax == infinity) {
              bounds my=autoscale(a.y,b.y,pic.scale.y.scale);
              pic.scale.y.tickMin=my.min;
              pic.scale.y.tickMax=my.max;
              divisor=my.divisor;
            }

            triple fuzz=Y*epsilon*max(abs(a.y),abs(b.y));
            a -= fuzz;
            b += fuzz;

            picture d;
            ticks(d,t,L,a--b,finite(x0) && finite(z0) ? a2--b2 : nullpath3,
                  p,arrow,margin,
                  ticklocate(a.y,b.y,pic.scale.y,Dir(dir)),divisor,
                  opposite,primary,P);
            add(f,t*T*tinv*d);
          },above=above);

  void bounds() {
    if(type == Min)
      x=pic.scale.x.automin() ? tickMin3(pic).x : pic.userMin().x;
    else if(type == Max)
      x=pic.scale.x.automax() ? tickMax3(pic).x : pic.userMax().x;
    else if(type == Both) {
      x2=pic.scale.x.automax() ? tickMax3(pic).x : pic.userMax().x;
      x=opposite ? x2 :
        (pic.scale.x.automin() ? tickMin3(pic).x : pic.userMin().x);
    }

    if(type2 == Min)
      z=pic.scale.z.automin() ? tickMin3(pic).z : pic.userMin().z;
    else if(type2 == Max)
      z=pic.scale.z.automax() ? tickMax3(pic).z : pic.userMax().z;
    else if(type2 == Both) {
      z2=pic.scale.z.automax() ? tickMax3(pic).z : pic.userMax().z;
      z=opposite2 ? z2 :
        (pic.scale.z.automin() ? tickMin3(pic).z : pic.userMin().z);
    }

    real Ymin=finite(ymin) ? ymin : pic.userMin().y;
    real Ymax=finite(ymax) ? ymax : pic.userMax().y;

    triple a=(x,Ymin,z);
    triple b=(x,Ymax,z);
    triple a2=(x2,Ymin,z2);
    triple b2=(x2,Ymax,z2);

    if(finite(a)) {
      pic.addPoint(a,min3(p));
      pic.addPoint(a,max3(p));
    }

    if(finite(b)) {
      pic.addPoint(b,min3(p));
      pic.addPoint(b,max3(p));
    }

    if(finite(a) && finite(b)) {
      picture d;
      ticks(d,pic.scaling3(warn=false),L,
            (0,a.y,0)--(0,b.y,0),(0,a2.y,0)--(0,a2.y,0),p,arrow,margin,
            ticklocate(a.y,b.y,pic.scale.y,Dir(dir)),divisor,
            opposite,primary,P);
      frame f;
      if(L.s != "") {
        Label L0=L.copy();
        L0.position(0);
        add(f,L0);
      }
      triple pos=a+L.relative()*(b-a);
      triple m=min3(d);
      triple M=max3(d);
      pic.addBox(pos,pos,(m.x,min3(f).y,m.z),(m.x,max3(f).y,m.z));
    }
  }

  // Process any queued x and z axis bound calculation requests.
  for(int i=0; i < pic.scale.x.bound.length; ++i)
    pic.scale.x.bound[i]();
  for(int i=0; i < pic.scale.z.bound.length; ++i)
    pic.scale.z.bound[i]();

  pic.scale.x.bound.delete();
  pic.scale.z.bound.delete();

  bounds();

  // Request another y bounds calculation before final picture scaling.
  pic.scale.y.bound.push(bounds);
}

// An internal routine to draw a z axis at a particular value.
void zaxis3At(picture pic=currentpicture, Label L="", axis axis,
              real zmin=-infinity, real zmax=infinity, pen p=currentpen,
              ticks3 ticks=NoTicks3,
              arrowbar3 arrow=None, margin3 margin=NoMargin3, bool above=true,
              bool opposite=false, bool opposite2=false, bool primary=true,
              projection P=currentprojection)
{
  int type=axis.type;
  int type2=axis.type2;
  triple dir=axis.align.dir3 == O ?
    defaultdir(X,Y,Z,opposite^opposite2,P) : axis.align.dir3;
  Label L=L.copy();
  if(L.align.dir3 == O && L.align.dir == 0) L.align(opposite ? -dir : dir);

  real x=axis.value;
  real y=axis.value2;
  real x2,y2;
  int[] divisor=copy(axis.zdivisor);

  pic.add(new void(picture f, transform3 t, transform3 T, projection P,
                   triple lb, triple rt) {
            transform3 tinv=inverse(t);
            triple a=zmin == -infinity ? tinv*(xtrans(t,x),ytrans(t,y),
                                               lb.z-min3(p).z) : (x,y,zmin);
            triple b=zmax == infinity ? tinv*(xtrans(t,x),ytrans(t,y),
                                              rt.z-max3(p).z) : (x,y,zmax);
            real x0;
            real y0;
            if(abs(dir.x) < abs(dir.y)) {
              x0=x;
              y0=y2;
            } else {
              x0=x2;
              y0=y;
            }

            triple a2=zmin == -infinity ? tinv*(xtrans(t,x0),ytrans(t,y0),
                                                lb.z-min3(p).z) : (x0,y0,zmin);
            triple b2=zmax == infinity ? tinv*(xtrans(t,x0),ytrans(t,y0),
                                               rt.z-max3(p).z) : (x0,y0,zmax);

            if(zmin == -infinity || zmax == infinity) {
              bounds mz=autoscale(a.z,b.z,pic.scale.z.scale);
              pic.scale.z.tickMin=mz.min;
              pic.scale.z.tickMax=mz.max;
              divisor=mz.divisor;
            }

            triple fuzz=Z*epsilon*max(abs(a.z),abs(b.z));
            a -= fuzz;
            b += fuzz;

            picture d;
            ticks(d,t,L,a--b,finite(x0) && finite(y0) ? a2--b2 : nullpath3,
                  p,arrow,margin,
                  ticklocate(a.z,b.z,pic.scale.z,Dir(dir)),divisor,
                  opposite,primary,P);
            add(f,t*T*tinv*d);
          },above=above);

  void bounds() {
    if(type == Min)
      x=pic.scale.x.automin() ? tickMin3(pic).x : pic.userMin().x;
    else if(type == Max)
      x=pic.scale.x.automax() ? tickMax3(pic).x : pic.userMax().x;
    else if(type == Both) {
      x2=pic.scale.x.automax() ? tickMax3(pic).x : pic.userMax().x;
      x=opposite ? x2 :
        (pic.scale.x.automin() ? tickMin3(pic).x : pic.userMin().x);
    }

    if(type2 == Min)
      y=pic.scale.y.automin() ? tickMin3(pic).y : pic.userMin().y;
    else if(type2 == Max)
      y=pic.scale.y.automax() ? tickMax3(pic).y : pic.userMax().y;
    else if(type2 == Both) {
      y2=pic.scale.y.automax() ? tickMax3(pic).y : pic.userMax().y;
      y=opposite2 ? y2 :
        (pic.scale.y.automin() ? tickMin3(pic).y : pic.userMin().y);
    }

    real Zmin=finite(zmin) ? zmin : pic.userMin().z;
    real Zmax=finite(zmax) ? zmax : pic.userMax().z;

    triple a=(x,y,Zmin);
    triple b=(x,y,Zmax);
    triple a2=(x2,y2,Zmin);
    triple b2=(x2,y2,Zmax);

    if(finite(a)) {
      pic.addPoint(a,min3(p));
      pic.addPoint(a,max3(p));
    }

    if(finite(b)) {
      pic.addPoint(b,min3(p));
      pic.addPoint(b,max3(p));
    }

    if(finite(a) && finite(b)) {
      picture d;
      ticks(d,pic.scaling3(warn=false),L,
            (0,0,a.z)--(0,0,b.z),(0,0,a2.z)--(0,0,a2.z),p,arrow,margin,
            ticklocate(a.z,b.z,pic.scale.z,Dir(dir)),divisor,
            opposite,primary,P);
      frame f;
      if(L.s != "") {
        Label L0=L.copy();
        L0.position(0);
        add(f,L0);
      }
      triple pos=a+L.relative()*(b-a);
      triple m=min3(d);
      triple M=max3(d);
      pic.addBox(pos,pos,(m.x,m.y,min3(f).z),(m.x,m.y,max3(f).z));
    }
  }

  // Process any queued x and y axes bound calculation requests.
  for(int i=0; i < pic.scale.x.bound.length; ++i)
    pic.scale.x.bound[i]();
  for(int i=0; i < pic.scale.y.bound.length; ++i)
    pic.scale.y.bound[i]();

  pic.scale.x.bound.delete();
  pic.scale.y.bound.delete();

  bounds();

  // Request another z bounds calculation before final picture scaling.
  pic.scale.z.bound.push(bounds);
}

// Internal routine to autoscale the user limits of a picture.
void autoscale3(picture pic=currentpicture, axis axis)
{
  bool set=pic.scale.set;
  autoscale(pic,axis);

  if(!set) {
    bounds mz;
    if(pic.userSetz()) {
      mz=autoscale(pic.userMin().z,pic.userMax().z,pic.scale.z.scale);
      if(pic.scale.z.scale.logarithmic &&
         floor(pic.userMin().z) == floor(pic.userMax().z)) {
        if(pic.scale.z.automin())
          pic.userMinz3(floor(pic.userMin().z));
        if(pic.scale.z.automax())
          pic.userMaxz3(ceil(pic.userMax().z));
      }
    } else {mz.min=mz.max=0; pic.scale.set=false;}

    pic.scale.z.tickMin=mz.min;
    pic.scale.z.tickMax=mz.max;
    axis.zdivisor=mz.divisor;
  }
}

// Draw an x axis in three dimensions.
void xaxis3(picture pic=currentpicture, Label L="", axis axis=YZZero,
            real xmin=-infinity, real xmax=infinity, pen p=currentpen,
            ticks3 ticks=NoTicks3,
            arrowbar3 arrow=None, margin3 margin=NoMargin3, bool above=false,
            projection P=currentprojection)
{
  if(xmin > xmax) return;

  if(pic.scale.x.automin && xmin > -infinity) pic.scale.x.automin=false;
  if(pic.scale.x.automax && xmax < infinity) pic.scale.x.automax=false;

  if(!pic.scale.set) {
    axis(pic,axis);
    autoscale3(pic,axis);
  }

  bool newticks=false;

  if(xmin != -infinity) {
    xmin=pic.scale.x.T(xmin);
    newticks=true;
  }

  if(xmax != infinity) {
    xmax=pic.scale.x.T(xmax);
    newticks=true;
  }

  if(newticks && pic.userSetx() && ticks != NoTicks3) {
    if(xmin == -infinity) xmin=pic.userMin().x;
    if(xmax == infinity) xmax=pic.userMax().x;
    bounds mx=autoscale(xmin,xmax,pic.scale.x.scale);
    pic.scale.x.tickMin=mx.min;
    pic.scale.x.tickMax=mx.max;
    axis.xdivisor=mx.divisor;
  }

  axis(pic,axis);

  if(xmin == -infinity && !axis.extend) {
    if(pic.scale.set)
      xmin=pic.scale.x.automin() ? pic.scale.x.tickMin :
        max(pic.scale.x.tickMin,pic.userMin().x);
    else xmin=pic.userMin().x;
  }

  if(xmax == infinity && !axis.extend) {
    if(pic.scale.set)
      xmax=pic.scale.x.automax() ? pic.scale.x.tickMax :
        min(pic.scale.x.tickMax,pic.userMax().x);
    else xmax=pic.userMax().x;
  }

  if(L.defaultposition) {
    L=L.copy();
    L.position(axis.position);
  }

  bool back=false;
  if(axis.type == Both) {
    projection P=centered(P,pic);
    triple v=P.normal;
    back=dot((0,pic.userMax().y-pic.userMin().y,0),v)*sgn(v.z) > 0;
  }

  xaxis3At(pic,L,axis,xmin,xmax,p,ticks,arrow,margin,above,false,false,!back);
  if(axis.type == Both)
    xaxis3At(pic,L,axis,xmin,xmax,p,ticks,arrow,margin,above,true,false,back);
  if(axis.type2 == Both) {
    xaxis3At(pic,L,axis,xmin,xmax,p,ticks,arrow,margin,above,false,true,false);
    if(axis.type == Both)
      xaxis3At(pic,L,axis,xmin,xmax,p,ticks,arrow,margin,above,true,true,false);
  }
}

// Draw a y axis in three dimensions.
void yaxis3(picture pic=currentpicture, Label L="", axis axis=XZZero,
            real ymin=-infinity, real ymax=infinity, pen p=currentpen,
            ticks3 ticks=NoTicks3,
            arrowbar3 arrow=None, margin3 margin=NoMargin3, bool above=false,
            projection P=currentprojection)
{
  if(ymin > ymax) return;

  if(pic.scale.y.automin && ymin > -infinity) pic.scale.y.automin=false;
  if(pic.scale.y.automax && ymax < infinity) pic.scale.y.automax=false;

  if(!pic.scale.set) {
    axis(pic,axis);
    autoscale3(pic,axis);
  }

  bool newticks=false;

  if(ymin != -infinity) {
    ymin=pic.scale.y.T(ymin);
    newticks=true;
  }

  if(ymax != infinity) {
    ymax=pic.scale.y.T(ymax);
    newticks=true;
  }

  if(newticks && pic.userSety() && ticks != NoTicks3) {
    if(ymin == -infinity) ymin=pic.userMin().y;
    if(ymax == infinity) ymax=pic.userMax().y;
    bounds my=autoscale(ymin,ymax,pic.scale.y.scale);
    pic.scale.y.tickMin=my.min;
    pic.scale.y.tickMax=my.max;
    axis.ydivisor=my.divisor;
  }

  axis(pic,axis);

  if(ymin == -infinity && !axis.extend) {
    if(pic.scale.set)
      ymin=pic.scale.y.automin() ? pic.scale.y.tickMin :
        max(pic.scale.y.tickMin,pic.userMin().y);
    else ymin=pic.userMin().y;
  }


  if(ymax == infinity && !axis.extend) {
    if(pic.scale.set)
      ymax=pic.scale.y.automax() ? pic.scale.y.tickMax :
        min(pic.scale.y.tickMax,pic.userMax().y);
    else ymax=pic.userMax().y;
  }

  if(L.defaultposition) {
    L=L.copy();
    L.position(axis.position);
  }

  bool back=false;
  if(axis.type == Both) {
    projection P=centered(P,pic);
    triple v=P.normal;
    back=dot((pic.userMax().x-pic.userMin().x,0,0),v)*sgn(v.z) > 0;
  }

  yaxis3At(pic,L,axis,ymin,ymax,p,ticks,arrow,margin,above,false,false,!back);

  if(axis.type == Both)
    yaxis3At(pic,L,axis,ymin,ymax,p,ticks,arrow,margin,above,true,false,back);
  if(axis.type2 == Both) {
    yaxis3At(pic,L,axis,ymin,ymax,p,ticks,arrow,margin,above,false,true,false);
    if(axis.type == Both)
      yaxis3At(pic,L,axis,ymin,ymax,p,ticks,arrow,margin,above,true,true,false);
  }
}
// Draw a z axis in three dimensions.
void zaxis3(picture pic=currentpicture, Label L="", axis axis=XYZero,
            real zmin=-infinity, real zmax=infinity, pen p=currentpen,
            ticks3 ticks=NoTicks3,
            arrowbar3 arrow=None, margin3 margin=NoMargin3, bool above=false,
            projection P=currentprojection)
{
  if(zmin > zmax) return;

  if(pic.scale.z.automin && zmin > -infinity) pic.scale.z.automin=false;
  if(pic.scale.z.automax && zmax < infinity) pic.scale.z.automax=false;

  if(!pic.scale.set) {
    axis(pic,axis);
    autoscale3(pic,axis);
  }

  bool newticks=false;

  if(zmin != -infinity) {
    zmin=pic.scale.z.T(zmin);
    newticks=true;
  }

  if(zmax != infinity) {
    zmax=pic.scale.z.T(zmax);
    newticks=true;
  }

  if(newticks && pic.userSetz() && ticks != NoTicks3) {
    if(zmin == -infinity) zmin=pic.userMin().z;
    if(zmax == infinity) zmax=pic.userMax().z;
    bounds mz=autoscale(zmin,zmax,pic.scale.z.scale);
    pic.scale.z.tickMin=mz.min;
    pic.scale.z.tickMax=mz.max;
    axis.zdivisor=mz.divisor;
  }

  axis(pic,axis);

  if(zmin == -infinity && !axis.extend) {
    if(pic.scale.set)
      zmin=pic.scale.z.automin() ? pic.scale.z.tickMin :
        max(pic.scale.z.tickMin,pic.userMin().z);
    else zmin=pic.userMin().z;
  }

  if(zmax == infinity && !axis.extend) {
    if(pic.scale.set)
      zmax=pic.scale.z.automax() ? pic.scale.z.tickMax :
        min(pic.scale.z.tickMax,pic.userMax().z);
    else zmax=pic.userMax().z;
  }

  if(L.defaultposition) {
    L=L.copy();
    L.position(axis.position);
  }

  bool back=false;
  if(axis.type == Both) {
    projection P=centered(P,pic);
    triple v=P.vector();
    back=dot((pic.userMax().x-pic.userMin().x,0,0),v)*sgn(v.y) > 0;
  }

  zaxis3At(pic,L,axis,zmin,zmax,p,ticks,arrow,margin,above,false,false,!back);
  if(axis.type == Both)
    zaxis3At(pic,L,axis,zmin,zmax,p,ticks,arrow,margin,above,true,false,back);
  if(axis.type2 == Both) {
    zaxis3At(pic,L,axis,zmin,zmax,p,ticks,arrow,margin,above,false,true,false);
    if(axis.type == Both)
      zaxis3At(pic,L,axis,zmin,zmax,p,ticks,arrow,margin,above,true,true,false);
  }
}

// Set the z limits of a picture.
void zlimits(picture pic=currentpicture, real min=-infinity, real max=infinity,
             bool crop=NoCrop)
{
  if(min > max) return;

  pic.scale.z.automin=min <= -infinity;
  pic.scale.z.automax=max >= infinity;

  bounds mz;
  if(pic.scale.z.automin() || pic.scale.z.automax())
    mz=autoscale(pic.userMin().z,pic.userMax().z,pic.scale.z.scale);

  if(pic.scale.z.automin) {
    if(pic.scale.z.automin()) pic.userMinz(mz.min);
  } else pic.userMinz(min(pic.scale.z.T(min),pic.scale.z.T(max)));

  if(pic.scale.z.automax) {
    if(pic.scale.z.automax()) pic.userMaxz(mz.max);
  } else pic.userMaxz(max(pic.scale.z.T(min),pic.scale.z.T(max)));
}

// Restrict the x, y, and z limits to box(min,max).
void limits(picture pic=currentpicture, triple min, triple max)
{
  xlimits(pic,min.x,max.x);
  ylimits(pic,min.y,max.y);
  zlimits(pic,min.z,max.z);
}

// Draw x, y and z axes.
void axes3(picture pic=currentpicture,
           Label xlabel="", Label ylabel="", Label zlabel="",
           bool extend=false,
           triple min=(-infinity,-infinity,-infinity),
           triple max=(infinity,infinity,infinity),
           pen p=currentpen, arrowbar3 arrow=None, margin3 margin=NoMargin3,
           projection P=currentprojection)
{
  xaxis3(pic,xlabel,YZZero(extend),min.x,max.x,p,arrow,margin,P);
  yaxis3(pic,ylabel,XZZero(extend),min.y,max.y,p,arrow,margin,P);
  zaxis3(pic,zlabel,XYZero(extend),min.z,max.z,p,arrow,margin,P);
}

triple Scale(picture pic=currentpicture, triple v)
{
  return (pic.scale.x.T(v.x),pic.scale.y.T(v.y),pic.scale.z.T(v.z));
}

triple[][] Scale(picture pic=currentpicture, triple[][] P)
{
  triple[][] Q=new triple[P.length][];
  for(int i=0; i < P.length; ++i) {
    triple[] Pi=P[i];
    Q[i]=new triple[Pi.length];
    for(int j=0; j < Pi.length; ++j)
      Q[i][j]=Scale(pic,Pi[j]);
  }
  return Q;
}

real ScaleX(picture pic=currentpicture, real x)
{
  return pic.scale.x.T(x);
}

real ScaleY(picture pic=currentpicture, real y)
{
  return pic.scale.y.T(y);
}

real ScaleZ(picture pic=currentpicture, real z)
{
  return pic.scale.z.T(z);
}

real[][] ScaleZ(picture pic=currentpicture, real[][] P)
{
  real[][] Q=new real[P.length][];
  for(int i=0; i < P.length; ++i) {
    real[] Pi=P[i];
    Q[i]=new real[Pi.length];
    for(int j=0; j < Pi.length; ++j)
      Q[i][j]=ScaleZ(pic,Pi[j]);
  }
  return Q;
}

real[] uniform(real T(real x), real Tinv(real x), real a, real b, int n)
{
  return map(Tinv,uniform(T(a),T(b),n));
}

// Draw a tick of length size at triple v in direction dir using pen p.
void tick(picture pic=currentpicture, triple v, triple dir, real size=Ticksize,
          pen p=currentpen)
{
  triple v=Scale(pic,v);
  pic.add(new void (picture f, transform3 t) {
      triple tv=t*v;
      draw(f,tv--tv+unit(dir)*size,p);
    });
  pic.addPoint(v,p);
  pic.addPoint(v,unit(dir)*size,p);
}

void xtick(picture pic=currentpicture, triple v, triple dir=Y,
           real size=Ticksize, pen p=currentpen)
{
  tick(pic,v,dir,size,p);
}

void xtick3(picture pic=currentpicture, real x, triple dir=Y,
            real size=Ticksize, pen p=currentpen)
{
  tick(pic,(x,pic.scale.y.scale.logarithmic ? 1 : 0,
            pic.scale.z.scale.logarithmic ? 1 : 0),dir,size,p);
}

void ytick(picture pic=currentpicture, triple v, triple dir=X,
           real size=Ticksize, pen p=currentpen)
{
  tick(pic,v,dir,size,p);
}

void ytick3(picture pic=currentpicture, real y, triple dir=X,
            real size=Ticksize, pen p=currentpen)
{
  tick(pic,(pic.scale.x.scale.logarithmic ? 1 : 0,y,
            pic.scale.z.scale.logarithmic ? 1 : 0),dir,size,p);
}

void ztick(picture pic=currentpicture, triple v, triple dir=X,
           real size=Ticksize, pen p=currentpen)
{
  xtick(pic,v,dir,size,p);
}

void ztick3(picture pic=currentpicture, real z, triple dir=X,
            real size=Ticksize, pen p=currentpen)
{
  xtick(pic,(pic.scale.x.scale.logarithmic ? 1 : 0,
             pic.scale.y.scale.logarithmic ? 1 : 0,z),dir,size,p);
}

void tick(picture pic=currentpicture, Label L, real value, triple v,
          triple dir, string format="", real size=Ticksize, pen p=currentpen)
{
  Label L=L.copy();
  L.align(L.align,-dir);
  if(shift(L.T3)*O == O)
    L.T3=shift(dot(dir,L.align.dir3) > 0 ? dir*size :
               ticklabelshift(L.align.dir3,p))*L.T3;
  L.p(p);
  if(L.s == "") L.s=format(format == "" ? defaultformat : format,value);
  L.s=baseline(L.s,baselinetemplate);
  label(pic,L,Scale(pic,v));
  tick(pic,v,dir,size,p);
}

void xtick(picture pic=currentpicture, Label L, triple v, triple dir=Y,
           string format="", real size=Ticksize, pen p=currentpen)
{
  tick(pic,L,v.x,v,dir,format,size,p);
}

void xtick3(picture pic=currentpicture, Label L, real x, triple dir=Y,
            string format="", real size=Ticksize, pen p=currentpen)
{
  xtick(pic,L,(x,pic.scale.y.scale.logarithmic ? 1 : 0,
               pic.scale.z.scale.logarithmic ? 1 : 0),dir,size,p);
}

void ytick(picture pic=currentpicture, Label L, triple v, triple dir=X,
           string format="", real size=Ticksize, pen p=currentpen)
{
  tick(pic,L,v.y,v,dir,format,size,p);
}

void ytick3(picture pic=currentpicture, Label L, real y, triple dir=X,
            string format="", real size=Ticksize, pen p=currentpen)
{
  xtick(pic,L,(pic.scale.x.scale.logarithmic ? 1 : 0,y,
               pic.scale.z.scale.logarithmic ? 1 : 0),dir,format,size,p);
}

void ztick(picture pic=currentpicture, Label L, triple v, triple dir=X,
           string format="", real size=Ticksize, pen p=currentpen)
{
  tick(pic,L,v.z,v,dir,format,size,p);
}

void ztick3(picture pic=currentpicture, Label L, real z, triple dir=X,
            string format="", real size=Ticksize, pen p=currentpen)
{
  xtick(pic,L,(pic.scale.x.scale.logarithmic ? 1 : 0,
               pic.scale.z.scale.logarithmic ? 1 : 0,z),dir,format,size,p);
}

private void label(picture pic, Label L, triple v, real x, align align,
                   string format, pen p)
{
  Label L=L.copy();
  L.align(align);
  L.p(p);
  if(shift(L.T3)*O == O)
    L.T3=shift(ticklabelshift(L.align.dir3,L.p))*L.T3;
  if(L.s == "") L.s=format(format == "" ? defaultformat : format,x);
  L.s=baseline(L.s,baselinetemplate);
  label(pic,L,v);
}

void labelx(picture pic=currentpicture, Label L="", triple v,
            align align=-Y, string format="", pen p=currentpen)
{
  label(pic,L,Scale(pic,v),v.x,align,format,p);
}

void labelx3(picture pic=currentpicture, Label L="", real x,
             align align=-Y, string format="", pen p=currentpen)
{
  labelx(pic,L,(x,pic.scale.y.scale.logarithmic ? 1 : 0,
                pic.scale.z.scale.logarithmic ? 1 : 0),align,format,p);
}

void labely(picture pic=currentpicture, Label L="", triple v,
            align align=-X, string format="", pen p=currentpen)
{
  label(pic,L,Scale(pic,v),v.y,align,format,p);
}

void labely3(picture pic=currentpicture, Label L="", real y,
             align align=-X, string format="", pen p=currentpen)
{
  labely(pic,L,(pic.scale.x.scale.logarithmic ? 1 : 0,y,
                pic.scale.z.scale.logarithmic ? 1 : 0),align,format,p);
}

void labelz(picture pic=currentpicture, Label L="", triple v,
            align align=-X, string format="", pen p=currentpen)
{
  label(pic,L,Scale(pic,v),v.z,align,format,p);
}

void labelz3(picture pic=currentpicture, Label L="", real z,
             align align=-X, string format="", pen p=currentpen)
{
  labelz(pic,L,(pic.scale.x.scale.logarithmic ? 1 : 0,
                pic.scale.y.scale.logarithmic ? 1 : 0,z),align,format,p);
}

typedef guide3 graph(triple F(real), real, real, int);
typedef guide3[] multigraph(triple F(real), real, real, int);

graph graph(interpolate3 join)
{
  return new guide3(triple f(real), real a, real b, int n) {
    real width=b-a;
    return n == 0 ? join(f(a)) :
      join(...sequence(new guide3(int i) {return f(a+(i/n)*width);},n+1));
  };
}

multigraph graph(interpolate3 join, bool3 cond(real))
{
  return new guide3[](triple f(real), real a, real b, int n) {
    real width=b-a;
    if(n == 0) return new guide3[] {join(cond(a) ? f(a) : nullpath3)};
    guide3[] G;
    guide3[] g;
    for(int i=0; i < n+1; ++i) {
      real t=a+(i/n)*width;
      bool3 b=cond(t);
      if(b)
        g.push(f(t));
      else {
        if(g.length > 0) {
          G.push(join(...g));
          g=new guide3[] {};
        }
        if(b == default)
          g.push(f(t));
      }
    }
    if(g.length > 0)
      G.push(join(...g));
    return G;
  };
}

guide3 Straight(... guide3[])=operator --;
guide3 Spline(... guide3[])=operator ..;

guide3 graph(picture pic=currentpicture, real x(real), real y(real),
             real z(real), real a, real b, int n=ngraph,
             interpolate3 join=operator --)
{
  return graph(join)(new triple(real t) {return Scale(pic,(x(t),y(t),z(t)));},
                     a,b,n);
}

guide3[] graph(picture pic=currentpicture, real x(real), real y(real),
               real z(real), real a, real b, int n=ngraph,
               bool3 cond(real), interpolate3 join=operator --)
{
  return graph(join,cond)(new triple(real t) {
      return Scale(pic,(x(t),y(t),z(t)));
    },a,b,n);
}

guide3 graph(picture pic=currentpicture, triple v(real), real a, real b,
             int n=ngraph, interpolate3 join=operator --)
{
  return graph(join)(new triple(real t) {return Scale(pic,v(t));},a,b,n);
}

guide3[] graph(picture pic=currentpicture, triple v(real), real a, real b,
               int n=ngraph, bool3 cond(real), interpolate3 join=operator --)
{
  return graph(join,cond)(new triple(real t) {
      return Scale(pic,v(t));
    },a,b,n);
}

guide3 graph(picture pic=currentpicture, triple[] v,
             interpolate3 join=operator --)
{
  int i=0;
  return graph(join)(new triple(real) {
      triple w=Scale(pic,v[i]);
      ++i;
      return w;
    },0,0,v.length-1);
}

guide3[] graph(picture pic=currentpicture, triple[] v, bool3[] cond,
               interpolate3 join=operator --)
{
  int n=v.length;
  int i=0;
  triple w;
  checkconditionlength(cond.length,n);
  bool3 condition(real) {
    bool b=cond[i];
    if(b) w=Scale(pic,v[i]);
    ++i;
    return b;
  }
  return graph(join,condition)(new triple(real) {return w;},0,0,n-1);
}

guide3 graph(picture pic=currentpicture, real[] x, real[] y, real[] z,
             interpolate3 join=operator --)
{
  int n=x.length;
  checklengths(n,y.length);
  checklengths(n,z.length);
  int i=0;
  return graph(join)(new triple(real) {
      triple w=Scale(pic,(x[i],y[i],z[i]));
      ++i;
      return w;
    },0,0,n-1);
}

guide3[] graph(picture pic=currentpicture, real[] x, real[] y, real[] z,
               bool3[] cond, interpolate3 join=operator --)
{
  int n=x.length;
  checklengths(n,y.length);
  checklengths(n,z.length);
  int i=0;
  triple w;
  checkconditionlength(cond.length,n);
  bool3 condition(real) {
    bool3 b=cond[i];
    if(b != false) w=Scale(pic,(x[i],y[i],z[i]));
    ++i;
    return b;
  }
  return graph(join,condition)(new triple(real) {return w;},0,0,n-1);
}

// The graph of a function along a path.
guide3 graph(triple F(path, real), path p, int n=1,
             interpolate3 join=operator --)
{
  guide3 g=join(...sequence(new guide3(int i) {
        return F(p,i/n);
      },n*length(p)));
  return cyclic(p) ? join(g,cycle) : join(g,F(p,length(p)));
}

guide3 graph(triple F(pair), path p, int n=1, interpolate3 join=operator --)
{
  return graph(new triple(path p, real position)
               {return F(point(p,position));},p,n,join);
}

guide3 graph(picture pic=currentpicture, real f(pair), path p, int n=1,
             interpolate3 join=operator --)
{
  return graph(new triple(pair z) {return Scale(pic,(z.x,z.y,f(z)));},p,n,
               join);
}

guide3 graph(real f(pair), path p, int n=1, real T(pair),
             interpolate3 join=operator --)
{
  return graph(new triple(pair z) {pair w=T(z); return (w.x,w.y,f(w));},p,n,
               join);
}

// Connect points in v into segments corresponding to consecutive true elements
// of b using interpolation operator join.
path3[] segment(triple[] v, bool[] cond, interpolate3 join=operator --)
{
  checkconditionlength(cond.length,v.length);
  int[][] segment=segment(cond);
  return sequence(new path3(int i) {return join(...v[segment[i]]);},
                  segment.length);
}

bool uperiodic(real[][] a) {
  int n=a.length;
  if(n == 0) return false;
  int m=a[0].length;
  real[] a0=a[0];
  real[] a1=a[n-1];
  for(int j=0; j < m; ++j) {
    real norm=0;
    for(int i=0; i < n; ++i)
      norm=max(norm,abs(a[i][j]));
    real epsilon=sqrtEpsilon*norm;
    if(abs(a0[j]-a1[j]) > epsilon) return false;
  }
  return true;
}
bool vperiodic(real[][] a) {
  int n=a.length;
  if(n == 0) return false;
  int m=a[0].length-1;
  for(int i=0; i < n; ++i) {
    real[] ai=a[i];
    real epsilon=sqrtEpsilon*norm(ai);
    if(abs(ai[0]-ai[m]) > epsilon) return false;
  }
  return true;
}

bool uperiodic(triple[][] a) {
  int n=a.length;
  if(n == 0) return false;
  int m=a[0].length;
  triple[] a0=a[0];
  triple[] a1=a[n-1];
  real epsilon=sqrtEpsilon*norm(a);
  for(int j=0; j < m; ++j)
    if(abs(a0[j]-a1[j]) > epsilon) return false;
  return true;
}
bool vperiodic(triple[][] a) {
  int n=a.length;
  if(n == 0) return false;
  int m=a[0].length-1;
  real epsilon=sqrtEpsilon*norm(a);
  for(int i=0; i < n; ++i)
    if(abs(a[i][0]-a[i][m]) > epsilon) return false;
  return true;
}

// return the surface described by a matrix f
surface surface(picture pic=currentpicture, triple[][] f, bool[][] cond={})
{
  if(!rectangular(f)) abort("matrix is not rectangular");

  int nx=f.length-1;
  int ny=nx > 0 ? f[0].length-1 : 0;

  bool all=cond.length == 0;

  int count;
  if(all)
    count=nx*ny;
  else {
    count=0;
    for(int i=0; i < nx; ++i) {
      bool[] condi=cond[i];
      bool[] condp=cond[i+1];
      for(int j=0; j < ny; ++j)
        if(condi[j] && condi[j+1] && condp[j] && condp[j+1]) ++count;
    }
  }

  surface s=surface(count);
  s.index=new int[nx][ny];
  int k=0;
  for(int i=0; i < nx; ++i) {
    bool[] condi,condp;
    if(!all) {
      condi=cond[i];
      condp=cond[i+1];
    }
    triple[] fi=f[i];
    triple[] fp=f[i+1];
    int[] indexi=s.index[i];
    for(int j=0; j < ny; ++j) {
      if(all || (condi[j] && condi[j+1] && condp[j] && condp[j+1])) {
        s.s[k]=patch(new triple[] {
            Scale(pic,fi[j]),
              Scale(pic,fp[j]),
              Scale(pic,fp[j+1]),
              Scale(pic,fi[j+1])});
        indexi[j]=k;
        ++k;
      }
    }
  }

  if(count == nx*ny) {
    if(uperiodic(f)) s.ucyclic(true);
    if(vperiodic(f)) s.vcyclic(true);
  }

  return s;
}

surface bispline(real[][] z, real[][] p, real[][] q, real[][] r,
                 real[] x, real[] y, bool[][] cond={})
{ // z[i][j] is the value at (x[i],y[j])
  // p and q are the first derivatives with respect to x and y, respectively
  // r is the second derivative ddu/dxdy
  int n=x.length-1;
  int m=y.length-1;

  bool all=cond.length == 0;

  int count;
  if(all)
    count=n*m;
  else {
    count=0;
    for(int i=0; i < n; ++i) {
      bool[] condi=cond[i];
      for(int j=0; j < m; ++j)
        if(condi[j]) ++count;
    }
  }

  surface s=surface(count);
  s.index=new int[n][m];
  int k=0;
  for(int i=0; i < n; ++i) {
    int ip=i+1;
    real xi=x[i];
    real xp=x[ip];
    real x1=interp(xi,xp,1/3);
    real x2=interp(xi,xp,2/3);
    real hx=x1-xi;
    real[] zi=z[i];
    real[] zp=z[ip];
    real[] ri=r[i];
    real[] rp=r[ip];
    real[] pi=p[i];
    real[] pp=p[ip];
    real[] qi=q[i];
    real[] qp=q[ip];
    int[] indexi=s.index[i];
    bool[] condi=all ? null : cond[i];
    for(int j=0; j < m; ++j) {
      if(all || condi[j]) {
        real yj=y[j];
        int jp=j+1;
        real yp=y[jp];
        real y1=interp(yj,yp,1/3);
        real y2=interp(yj,yp,2/3);
        real hy=y1-yj;
        real hxy=hx*hy;
        real zij=zi[j];
        real zip=zi[jp];
        real zpj=zp[j];
        real zpp=zp[jp];
        real pij=hx*pi[j];
        real ppj=hx*pp[j];
        real qip=hy*qi[jp];
        real qpp=hy*qp[jp];
        real zippip=zip+hx*pi[jp];
        real zppmppp=zpp-hx*pp[jp];
        real zijqij=zij+hy*qi[j];
        real zpjqpj=zpj+hy*qp[j];

        s.s[k]=patch(new triple[][] {
            {(xi,yj,zij),(xi,y1,zijqij),(xi,y2,zip-qip),(xi,yp,zip)},
              {(x1,yj,zij+pij),(x1,y1,zijqij+pij+hxy*ri[j]),
                  (x1,y2,zippip-qip-hxy*ri[jp]),(x1,yp,zippip)},
                {(x2,yj,zpj-ppj),(x2,y1,zpjqpj-ppj-hxy*rp[j]),
                    (x2,y2,zppmppp-qpp+hxy*rp[jp]),(x2,yp,zppmppp)},
                  {(xp,yj,zpj),(xp,y1,zpjqpj),(xp,y2,zpp-qpp),(xp,yp,zpp)}},
          copy=false);
        indexi[j]=k;
        ++k;
      }
    }
  }

  return s;
}

private real[][][] bispline0(real[][] z, real[][] p, real[][] q, real[][] r,
                             real[] x, real[] y, bool[][] cond={})
{ // z[i][j] is the value at (x[i],y[j])
  // p and q are the first derivatives with respect to x and y, respectively
  // r is the second derivative ddu/dxdy
  int n=x.length-1;
  int m=y.length-1;

  bool all=cond.length == 0;

  int count;
  if(all)
    count=n*m;
  else {
    count=0;
    for(int i=0; i < n; ++i) {
      bool[] condi=cond[i];
      bool[] condp=cond[i+1];
      for(int j=0; j < m; ++j)
        if(all || (condi[j] && condi[j+1] && condp[j] && condp[j+1]))
          ++count;
    }
  }

  real[][][] s=new real[count][][];
  int k=0;
  for(int i=0; i < n; ++i) {
    int ip=i+1;
    real xi=x[i];
    real xp=x[ip];
    real hx=(xp-xi)/3;
    real[] zi=z[i];
    real[] zp=z[ip];
    real[] ri=r[i];
    real[] rp=r[ip];
    real[] pi=p[i];
    real[] pp=p[ip];
    real[] qi=q[i];
    real[] qp=q[ip];
    bool[] condi=all ? null : cond[i];
    bool[] condp=all ? null : cond[i+1];
    for(int j=0; j < m; ++j) {
      if(all || (condi[j] && condi[j+1] && condp[j] && condp[j+1])) {
        real yj=y[j];
        int jp=j+1;
        real yp=y[jp];
        real hy=(yp-yj)/3;
        real hxy=hx*hy;
        real zij=zi[j];
        real zip=zi[jp];
        real zpj=zp[j];
        real zpp=zp[jp];
        real pij=hx*pi[j];
        real ppj=hx*pp[j];
        real qip=hy*qi[jp];
        real qpp=hy*qp[jp];
        real zippip=zip+hx*pi[jp];
        real zppmppp=zpp-hx*pp[jp];
        real zijqij=zij+hy*qi[j];
        real zpjqpj=zpj+hy*qp[j];

        s[k]=new real[][] {{zij,zijqij,zip-qip,zip},
                                        {zij+pij,zijqij+pij+hxy*ri[j],
                                            zippip-qip-hxy*ri[jp],zippip},
                                          {zpj-ppj,zpjqpj-ppj-hxy*rp[j],
                                              zppmppp-qpp+hxy*rp[jp],zppmppp},
                                            {zpj,zpjqpj,zpp-qpp,zpp}};
        ++k;
      }
    }
  }

  return s;
}

// return the surface values described by a real matrix f, interpolated with
// xsplinetype and ysplinetype.
real[][][] bispline(real[][] f, real[] x, real[] y,
                    splinetype xsplinetype=null,
                    splinetype ysplinetype=xsplinetype, bool[][] cond={})
{
  real epsilon=sqrtEpsilon*norm(y);
  if(xsplinetype == null)
    xsplinetype=(abs(x[0]-x[x.length-1]) <= epsilon) ? periodic : notaknot;
  if(ysplinetype == null)
    ysplinetype=(abs(y[0]-y[y.length-1]) <= epsilon) ? periodic : notaknot;
  int n=x.length; int m=y.length;
  real[][] ft=transpose(f);
  real[][] tp=new real[m][];
  for(int j=0; j < m; ++j)
    tp[j]=xsplinetype(x,ft[j]);
  real[][] q=new real[n][];
  for(int i=0; i < n; ++i)
    q[i]=ysplinetype(y,f[i]);
  real[][] qt=transpose(q);
  real[] d1=xsplinetype(x,qt[0]);
  real[] d2=xsplinetype(x,qt[m-1]);
  real[][] r=new real[n][];
  real[][] p=transpose(tp);
  for(int i=0; i < n; ++i)
    r[i]=clamped(d1[i],d2[i])(y,p[i]);
  return bispline0(f,p,q,r,x,y,cond);
}

// return the surface described by a real matrix f, interpolated with
// xsplinetype and ysplinetype.
surface surface(picture pic=currentpicture, real[][] f, real[] x, real[] y,
                splinetype xsplinetype=null,
                splinetype ysplinetype=xsplinetype,
                bool[][] cond={})
{
  if(xsplinetype == linear && ysplinetype == linear) {
    int nx=f.length-1;
    int ny=nx > 0 ? f[0].length-1 : 0;

    if(nx == 0 || ny == 0) return nullsurface;

    bool all=cond.length == 0;

    triple[][] v=new triple[nx+1][ny+1];

    for(int i=0; i <= nx; ++i) {
      bool[] condi=all ? null : cond[i];
      real xi=x[i];
      real[] fi=f[i];
      triple[] vi=v[i];
        for(int j=0; j <= ny; ++j)
          vi[j]=(xi,y[j],fi[j]);
    }
    return surface(pic,v,cond);
  }

  real[][] f=ScaleZ(pic,f);
  real[] x=map(pic.scale.x.T,x);
  real[] y=map(pic.scale.y.T,y);

  real epsilon=sqrtEpsilon*norm(y);
  if(xsplinetype == null)
    xsplinetype=(abs(x[0]-x[x.length-1]) <= epsilon) ? periodic : notaknot;
  if(ysplinetype == null)
    ysplinetype=(abs(y[0]-y[y.length-1]) <= epsilon) ? periodic : notaknot;
  int n=x.length; int m=y.length;
  real[][] ft=transpose(f);
  real[][] tp=new real[m][];
  for(int j=0; j < m; ++j)
    tp[j]=xsplinetype(x,ft[j]);
  real[][] q=new real[n][];
  for(int i=0; i < n; ++i)
    q[i]=ysplinetype(y,f[i]);
  real[][] qt=transpose(q);
  real[] d1=xsplinetype(x,qt[0]);
  real[] d2=xsplinetype(x,qt[m-1]);
  real[][] r=new real[n][];
  real[][] p=transpose(tp);
  for(int i=0; i < n; ++i)
    r[i]=clamped(d1[i],d2[i])(y,p[i]);
  surface s=bispline(f,p,q,r,x,y,cond);
  if(xsplinetype == periodic) s.ucyclic(true);
  if(ysplinetype == periodic) s.vcyclic(true);
  return s;
}

// return the surface described by a real matrix f, interpolated with
// xsplinetype and ysplinetype.
surface surface(picture pic=currentpicture, real[][] f, pair a, pair b,
                splinetype xsplinetype, splinetype ysplinetype=xsplinetype,
                bool[][] cond={})
{
  if(!rectangular(f)) abort("matrix is not rectangular");

  int nx=f.length-1;
  int ny=nx > 0 ? f[0].length-1 : 0;

  if(nx == 0 || ny == 0) return nullsurface;

  real[] x=uniform(pic.scale.x.T,pic.scale.x.Tinv,a.x,b.x,nx);
  real[] y=uniform(pic.scale.y.T,pic.scale.y.Tinv,a.y,b.y,ny);
  return surface(pic,f,x,y,xsplinetype,ysplinetype,cond);
}

// return the surface described by a real matrix f, interpolated linearly.
surface surface(picture pic=currentpicture, real[][] f, pair a, pair b,
                bool[][] cond={})
{
  if(!rectangular(f)) abort("matrix is not rectangular");

  int nx=f.length-1;
  int ny=nx > 0 ? f[0].length-1 : 0;

  if(nx == 0 || ny == 0) return nullsurface;

  bool all=cond.length == 0;

  triple[][] v=new triple[nx+1][ny+1];

  pair a=Scale(pic,a);
  pair b=Scale(pic,b);
  for(int i=0; i <= nx; ++i) {
    real x=pic.scale.x.Tinv(interp(a.x,b.x,i/nx));
    bool[] condi=all ? null : cond[i];
    triple[] vi=v[i];
    real[] fi=f[i];
    for(int j=0; j <= ny; ++j)
      if(all || condi[j])
        vi[j]=(x,pic.scale.y.Tinv(interp(a.y,b.y,j/ny)),fi[j]);
  }
  return surface(pic,v,cond);
}

// return the surface described by a parametric function f over box(a,b),
// interpolated linearly.
surface surface(picture pic=currentpicture, triple f(pair z), pair a, pair b,
                int nu=nmesh, int nv=nu, bool cond(pair z)=null)
{
  if(nu <= 0 || nv <= 0) return nullsurface;

  bool[][] active;
  bool all=cond == null;
  if(!all) active=new bool[nu+1][nv+1];

  real du=1/nu;
  real dv=1/nv;
  pair Idv=(0,dv);
  pair dz=(du,dv);

  triple[][] v=new triple[nu+1][nv+1];

  pair a=Scale(pic,a);
  pair b=Scale(pic,b);
  for(int i=0; i <= nu; ++i) {
    real x=pic.scale.x.Tinv(interp(a.x,b.x,i*du));
    bool[] activei=all ? null : active[i];
    triple[] vi=v[i];
    for(int j=0; j <= nv; ++j) {
      pair z=(x,pic.scale.y.Tinv(interp(a.y,b.y,j*dv)));
      if(all || (activei[j]=cond(z))) vi[j]=f(z);
    }
  }
  return surface(pic,v,active);
}

// return the surface described by a parametric function f evaluated at u and v
// and interpolated with usplinetype and vsplinetype.
surface surface(picture pic=currentpicture, triple f(pair z),
                real[] u, real[] v, splinetype[] usplinetype,
                splinetype[] vsplinetype=Spline, bool cond(pair z)=null)
{
  int nu=u.length-1;
  int nv=v.length-1;
  real[] ipt=sequence(u.length);
  real[] jpt=sequence(v.length);
  real[][] fx=new real[u.length][v.length];
  real[][] fy=new real[u.length][v.length];
  real[][] fz=new real[u.length][v.length];

  bool[][] active;
  bool all=cond == null;
  if(!all) active=new bool[u.length][v.length];

  for(int i=0; i <= nu; ++i) {
    real ui=u[i];
    real[] fxi=fx[i];
    real[] fyi=fy[i];
    real[] fzi=fz[i];
    bool[] activei=all ? null : active[i];
    for(int j=0; j <= nv; ++j) {
      pair z=(ui,v[j]);
      if(!all) activei[j]=cond(z);
      triple f=Scale(pic,f(z));
      fxi[j]=f.x;
      fyi[j]=f.y;
      fzi[j]=f.z;
    }
  }

  if(usplinetype.length == 0) {
    usplinetype=new splinetype[] {uperiodic(fx) ? periodic : notaknot,
                                  uperiodic(fy) ? periodic : notaknot,
                                  uperiodic(fz) ? periodic : notaknot};
  } else if(usplinetype.length != 3) abort("usplinetype must have length 3");

  if(vsplinetype.length == 0) {
    vsplinetype=new splinetype[] {vperiodic(fx) ? periodic : notaknot,
                                  vperiodic(fy) ? periodic : notaknot,
                                  vperiodic(fz) ? periodic : notaknot};
  } else if(vsplinetype.length != 3) abort("vsplinetype must have length 3");

  real[][][] sx=bispline(fx,ipt,jpt,usplinetype[0],vsplinetype[0],active);
  real[][][] sy=bispline(fy,ipt,jpt,usplinetype[1],vsplinetype[1],active);
  real[][][] sz=bispline(fz,ipt,jpt,usplinetype[2],vsplinetype[2],active);

  surface s=surface(sx.length);
  s.index=new int[nu][nv];
  int k=0;
  for(int i=0; i < nu; ++i) {
    int[] indexi=s.index[i];
    for(int j=0; j < nv; ++j) {
      indexi[j]=k;
      ++k;
    }
  }

  for(int k=0; k < sx.length; ++k) {
    triple[][] Q=new triple[4][];
    real[][] Px=sx[k];
    real[][] Py=sy[k];
    real[][] Pz=sz[k];
    for(int i=0; i < 4 ; ++i) {
      real[] Pxi=Px[i];
      real[] Pyi=Py[i];
      real[] Pzi=Pz[i];
      Q[i]=new triple[] {(Pxi[0],Pyi[0],Pzi[0]),
                         (Pxi[1],Pyi[1],Pzi[1]),
                         (Pxi[2],Pyi[2],Pzi[2]),
                         (Pxi[3],Pyi[3],Pzi[3])};
    }
    s.s[k]=patch(Q);
  }

  if(usplinetype[0] == periodic && usplinetype[1] == periodic &&
     usplinetype[1] == periodic) s.ucyclic(true);

  if(vsplinetype[0] == periodic && vsplinetype[1] == periodic &&
     vsplinetype[1] == periodic) s.vcyclic(true);

  return s;
}

// return the surface described by a parametric function f over box(a,b),
// interpolated with usplinetype and vsplinetype.
surface surface(picture pic=currentpicture, triple f(pair z), pair a, pair b,
                int nu=nmesh, int nv=nu,
                splinetype[] usplinetype, splinetype[] vsplinetype=Spline,
                bool cond(pair z)=null)
{
  real[] x=uniform(pic.scale.x.T,pic.scale.x.Tinv,a.x,b.x,nu);
  real[] y=uniform(pic.scale.y.T,pic.scale.y.Tinv,a.y,b.y,nv);
  return surface(pic,f,x,y,usplinetype,vsplinetype,cond);
}

// return the surface described by a real function f over box(a,b),
// interpolated linearly.
surface surface(picture pic=currentpicture, real f(pair z), pair a, pair b,
                int nx=nmesh, int ny=nx, bool cond(pair z)=null)
{
  return surface(pic,new triple(pair z) {return (z.x,z.y,f(z));},a,b,nx,ny,
                 cond);
}

// return the surface described by a real function f over box(a,b),
// interpolated with xsplinetype and ysplinetype.
surface surface(picture pic=currentpicture, real f(pair z), pair a, pair b,
                int nx=nmesh, int ny=nx, splinetype xsplinetype,
                splinetype ysplinetype=xsplinetype, bool cond(pair z)=null)
{
  bool[][] active;
  bool all=cond == null;
  if(!all) active=new bool[nx+1][ny+1];

  real dx=1/nx;
  real dy=1/ny;
  pair Idy=(0,dy);
  pair dz=(dx,dy);

  real[][] F=new real[nx+1][ny+1];
  real[] x=uniform(pic.scale.x.T,pic.scale.x.Tinv,a.x,b.x,nx);
  real[] y=uniform(pic.scale.y.T,pic.scale.y.Tinv,a.y,b.y,ny);
  for(int i=0; i <= nx; ++i) {
    bool[] activei=all ? null : active[i];
    real[] Fi=F[i];
    real x=x[i];
    for(int j=0; j <= ny; ++j) {
      pair z=(x,y[j]);
      Fi[j]=f(z);
      if(!all) activei[j]=cond(z);
    }
  }
  return surface(pic,F,x,y,xsplinetype,ysplinetype,active);
}

guide3[][] lift(real f(real x, real y), guide[][] g,
                interpolate3 join=operator --)
{
  guide3[][] G=new guide3[g.length][];
  for(int cnt=0; cnt < g.length; ++cnt) {
    guide[] gcnt=g[cnt];
    guide3[] Gcnt=new guide3[gcnt.length];
    for(int i=0; i < gcnt.length; ++i) {
      guide gcnti=gcnt[i];
      guide3 Gcnti=join(...sequence(new guide3(int j) {
            pair z=point(gcnti,j);
            return (z.x,z.y,f(z.x,z.y));
          },size(gcnti)));
      if(cyclic(gcnti)) Gcnti=Gcnti..cycle;
      Gcnt[i]=Gcnti;
    }
    G[cnt]=Gcnt;
  }
  return G;
}

guide3[][] lift(real f(pair z), guide[][] g, interpolate3 join=operator --)
{
  return lift(new real(real x, real y) {return f((x,y));},g,join);
}

void draw(picture pic=currentpicture, Label[] L=new Label[],
          guide3[][] g, pen[] p, light light=currentlight, string name="",
          render render=defaultrender,
          interaction interaction=LabelInteraction())
{
  pen thin=is3D() ? thin() : defaultpen;
  bool group=g.length > 1 && (name != "" || render.defaultnames);
  if(group)
    begingroup3(pic,name == "" ? "contours" : name,render);
  for(int cnt=0; cnt < g.length; ++cnt) {
    guide3[] gcnt=g[cnt];
    pen pcnt=thin+p[cnt];
    for(int i=0; i < gcnt.length; ++i)
      draw(pic,gcnt[i],pcnt,light,name);
    if(L.length > 0) {
      Label Lcnt=L[cnt];
      for(int i=0; i < gcnt.length; ++i) {
        if(Lcnt.s != "" && size(gcnt[i]) > 1)
          label(pic,Lcnt,gcnt[i],pcnt,name,interaction);
      }
    }
  }
  if(group)
    endgroup3(pic);
}

void draw(picture pic=currentpicture, Label[] L=new Label[],
          guide3[][] g, pen p=currentpen, light light=currentlight,
          string name="", render render=defaultrender,
          interaction interaction=LabelInteraction())
{
  draw(pic,L,g,sequence(new pen(int) {return p;},g.length),light,name,
       render,interaction);
}

real maxlength(triple f(pair z), pair a, pair b, int nu, int nv)
{
  return min(abs(f((b.x,a.y))-f(a))/nu,abs(f((a.x,b.y))-f(a))/nv);
}

// return a vector field on a parametric surface f over box(a,b).
picture vectorfield(path3 vector(pair v), triple f(pair z), pair a, pair b,
                    int nu=nmesh, int nv=nu, bool truesize=false,
                    real maxlength=truesize ? 0 : maxlength(f,a,b,nu,nv),
                    bool cond(pair z)=null, pen p=currentpen,
                    arrowbar3 arrow=Arrow3, margin3 margin=PenMargin3,
                    string name="", render render=defaultrender)
{
  picture pic;
  real du=(b.x-a.x)/(nu-1);
  real dv=(b.y-a.y)/(nv-1);
  bool all=cond == null;
  real scale;

  if(maxlength > 0) {
    real size(pair z) {
      path3 g=vector(z);
      triple w=point(g,size(g)-1)-point(g,0);
      return max(w.x,w.y,w.z);
    }
    real max=size(a);
    for(int i=0; i <= nu; ++i) {
      real u=a.x+i*du;
      for(int j=0; j < nv; ++j) {
        real v=a.y+j*dv;
        max=max(max,size((u,v)));
      }
    }
    scale=max > 0 ? maxlength/max : 1;
  } else scale=1;

  bool group=name != "" || render.defaultnames;
  if(group)
    begingroup3(pic,name == "" ? "vectorfield" : name,render);
  for(int i=0; i <= nu; ++i) {
    real u=a.x+i*du;
    for(int j=0; j <= nv; ++j) {
      real v=a.y+j*dv;
      pair z=(u,v);
      if(all || cond(z)) {
        path3 g=scale3(scale)*vector(z);
        string name="vector";
        if(truesize) {
          picture opic;
          draw(opic,g,p,arrow,margin,name,render);
          add(pic,opic,f(z));
        } else
          draw(pic,shift(f(z))*g,p,arrow,margin,name,render);
      }
    }
  }
  if(group)
    endgroup3(pic);
  return pic;
}

triple polar(real r, real theta, real phi)
{
  return r*expi(theta,phi);
}

guide3 polargraph(real r(real,real), real theta(real), real phi(real),
                  int n=ngraph, interpolate3 join=operator --)
{
  return graph(join)(new triple(real t) {
      return polar(r(theta(t),phi(t)),theta(t),phi(t));
    },0,1,n);
}

// True arc
path3 Arc(triple c, triple v1, triple v2, triple normal=O, bool direction=CCW,
          int n=nCircle)
{
  v1 -= c;
  real r=abs(v1);
  v1=unit(v1);
  v2=unit(v2-c);

  if(normal == O) {
    normal=cross(v1,v2);
    if(normal == O) abort("explicit normal required for these endpoints");
  }

  transform3 T=align(unit(normal));
  transform3 Tinv=transpose(T);
  v1=Tinv*v1;
  v2=Tinv*v2;

  real fuzz=sqrtEpsilon*max(abs(v1),abs(v2));
  if(abs(v1.z) > fuzz || abs(v2.z) > fuzz)
    abort("invalid normal vector");

  real phi1=radians(longitude(v1,warn=false));
  real phi2=radians(longitude(v2,warn=false));
  if(direction) {
    if(phi1 >= phi2) phi1 -= 2pi;
  } else if(phi2 >= phi1) phi2 -= 2pi;

  static real piby2=pi/2;
  return shift(c)*T*polargraph(new real(real theta, real phi) {return r;},
                               new real(real t) {return piby2;},
                               new real(real t) {return interp(phi1,phi2,t);},
                               n,operator ..);
}

path3 Arc(triple c, real r, real theta1, real phi1, real theta2, real phi2,
          triple normal=O, bool direction, int n=nCircle)
{
  return Arc(c,c+r*dir(theta1,phi1),c+r*dir(theta2,phi2),normal,direction,n);
}

path3 Arc(triple c, real r, real theta1, real phi1, real theta2, real phi2,
          triple normal=O, int n=nCircle)
{
  return Arc(c,r,theta1,phi1,theta2,phi2,normal,
             theta2 > theta1 || (theta2 == theta1 && phi2 >= phi1) ? CCW : CW,
             n);
}

// True circle
path3 Circle(triple c, real r, triple normal=Z, int n=nCircle)
{
  static real piby2=pi/2;
  return shift(c)*align(unit(normal))*
    polargraph(new real(real theta, real phi) {return r;},
               new real(real t) {return piby2;},
               new real(real t) {return interp(0,2pi,t);},n,operator ..);

}
