struct rmf {
 triple p,r,t,s;
 void operator init(triple p, triple r, triple t) {
   this.p=p;
   this.r=r;
   this.t=t;
   s=cross(t,r);
 }

 transform3 transform() {
   return transform3(r,s,t);
 }
}

// Rotation minimizing frame
// http://www.cs.hku.hk/research/techreps/document/TR-2007-07.pdf
rmf[] rmf(path3 g, real[] t, triple perp=O)
{
 triple T=dir(g,0);
 triple Tp=abs(perp) < sqrtEpsilon ? perp(T) : unit(perp);
 rmf[] R=new rmf[t.length];
 R[0]=rmf(point(g,0),Tp,T);
 for(int i=1; i < t.length; ++i) {
   rmf Ri=R[i-1];
   real t=t[i];
   triple p=point(g,t);
   triple v1=p-Ri.p;
   if(v1 != O) {
     triple r=Ri.r;
     triple u1=unit(v1);
     triple ti=Ri.t;
     triple tp=ti-2*dot(u1,ti)*u1;
     ti=dir(g,t);
     triple rp=r-2*dot(u1,r)*u1;
     triple u2=unit(ti-tp);
     rp=rp-2*dot(u2,rp)*u2;
     R[i]=rmf(p,unit(rp),unit(ti));
   } else
     R[i]=R[i-1];
 }
 return R;
}

rmf[] rmf(triple z0, triple c0, triple c1, triple z1, real[] t, triple perp=O)
{
 static triple s0;

 real norm=sqrtEpsilon*max(abs(z0),abs(c0),abs(c1),abs(z1));

 // Special case of dir for t in (0,1].
 triple dir(real t) {
   if(t == 1) {
     triple dir=z1-c1;
     if(abs(dir) > norm) return unit(dir);
     dir=2.0*c1-c0-z1;
     if(abs(dir) > norm) return unit(dir);
     return unit(z1-z0+3.0*(c0-c1));
   }
   triple a=z1-z0+3.0*(c0-c1);
   triple b=2.0*(z0+c1)-4.0*c0;
   triple c=c0-z0;
   triple dir=a*t*t+b*t+c;
   if(abs(dir) > norm) return unit(dir);
   dir=2.0*a*t+b;
   if(abs(dir) > norm) return unit(dir);
   return unit(a);
 }

 triple T=c0-z0;
 if(abs(T) < norm) {
   T=z0-2*c0+c1;
   if(abs(T) < norm)
     T=z1-z0+3.0*(c0-c1);
 }
 T=unit(T);
 triple Tp=perp == O ? cross(s0,T) : perp;
 Tp=abs(Tp) < sqrtEpsilon ? perp(T) : unit(Tp);
 rmf[] R=new rmf[t.length];
 R[0]=rmf(z0,Tp,T);

 for(int i=1; i < t.length; ++i) {
   rmf Ri=R[i-1];
   real t=t[i];
   triple p=bezier(z0,c0,c1,z1,t);
   triple v1=p-Ri.p;
   if(v1 != O) {
     triple r=Ri.r;
     triple u1=unit(v1);
     triple ti=Ri.t;
     triple tp=ti-2*dot(u1,ti)*u1;
     ti=dir(t);
     triple rp=r-2*dot(u1,r)*u1;
     triple u2=unit(ti-tp);
     rp=rp-2*dot(u2,rp)*u2;
     R[i]=rmf(p,unit(rp),unit(ti));
   } else
     R[i]=R[i-1];
 }
 s0=R[t.length-1].s;
 return R;
}


drawfcn drawTube(triple[] g, real w, triple min, triple max) {
 return new void(frame f, transform3 t=identity4, material[] m,
                 light light=currentlight, render render=defaultrender)
   {
     material m=material(m[0],light);
     drawTube(f,t*g,w,m.p,m.opacity,m.shininess,m.metallic,m.fresnel0,
              t*min,t*max,m.opacity == 1);
   };
}

surface tube(triple z0, triple c0, triple c1, triple z1, real w)
{
 surface s;
 static real[] T={0,1/3,2/3,1};
 rmf[] rmf=rmf(z0,c0,c1,z1,T);

 real aw=a*w;
 triple[] arc={(w,0,0),(w,aw,0),(aw,w,0),(0,w,0)};
 triple[] g={z0,c0,c1,z1};

 void f(transform3 R) {
   triple[][] P=new triple[4][];
   for(int i=0; i < 4; ++i) {
     transform3 T=shift(g[i])*rmf[i].transform()*R;
     P[i]=new triple[] {T*arc[0],T*arc[1],T*arc[2],T*arc[3]};
   }
   s.push(patch(P,copy=false));
 }

 f(identity4);
 f(t1);
 f(t2);
 f(t3);

 s.PRCprimitive=false;
 s.primitive=primitive(drawTube(g,w,min(s),max(s)),
                       new bool(transform3 t) {
                         return unscaled(t,X,Y);
                       });
 return s;
}

real tubethreshold=20;

// Note: casting an array of surfaces to a single surface will disable
// primitive compression.
surface operator cast(surface[] s) {
 surface S;
 for(surface p : s)
   S.append(p);
 return S;
}

struct tube
{
 surface[] s;
 path3 center; // tube axis

 void Null(transform3) {}
 void Null(transform3, bool) {}

 surface[] render(path3 g, real r) {
   triple z0=point(g,0);
   triple c0=postcontrol(g,0);
   triple c1=precontrol(g,1);
   triple z1=point(g,1);
   real norm=sqrtEpsilon*max(abs(z0),abs(c0),abs(c1),abs(z1),r);
   surface[] s;
   void Split(triple z0, triple c0, triple c1, triple z1,
              int depth=mantissaBits) {
     if(depth > 0) {
       pair threshold(triple z0, triple c0, triple c1) {
         triple u=c1-z0;
         triple v=c0-z0;
         real x=abs(v);
         return (x,abs(u*x^2-dot(u,v)*v));
       }

       pair a0=threshold(z0,c0,c1);
       pair a1=threshold(z1,c1,c0);
       real rL=r*arclength(z0,c0,c1,z1)*tubethreshold;
       if((a0.x >= norm && rL*a0.y^2 > a0.x^8) ||
          (a1.x >= norm && rL*a1.y^2 > a1.x^8)) {
         triple m0=0.5*(z0+c0);
         triple m1=0.5*(c0+c1);
         triple m2=0.5*(c1+z1);
         triple m3=0.5*(m0+m1);
         triple m4=0.5*(m1+m2);
         triple m5=0.5*(m3+m4);
         --depth;
         Split(z0,m0,m3,m5,depth);
         Split(m5,m4,m2,z1,depth);
         return;
       }
     }

     s.push(tube(z0,c0,c1,z1,r));
   }
   Split(z0,c0,c1,z1);
   return s;
 }

 void operator init(path3 p, real width) {
   center=p;
   real r=0.5*width;

   void generate(path3 p) {
     int n=length(p);
     for(int i=0; i < n; ++i) {
       if(straight(p,i)) {
         triple v=point(p,i);
         triple u=point(p,i+1)-v;
         transform3 t=shift(v)*align(unit(u))*scale(r,r,abs(u));
         // Draw opaque surfaces with core for better small-scale rendering.
         surface unittube=t*unitcylinder;
         unittube.primitive=primitive(unitcylinderDraw(core=true),
                                      new bool(transform3 t) {
                                        return unscaled(t,X,Y);
                                      });
         s.push(unittube);
       } else
         s.append(render(subpath(p,i,i+1),r));
     }
   }

   transform3 t=scale3(r);
   bool cyclic=cyclic(p);
   int begin=0;
   int n=length(p);
   for(int i=cyclic ? 0 : 1; i < n; ++i)
     if(abs(dir(p,i,1)-dir(p,i,-1)) > sqrtEpsilon) {
       generate(subpath(p,begin,i));
       triple dir=dir(p,i,-1);
       transform3 T=t*align(dir);
       s.push(shift(point(p,i))*T*(straight(p,i-1) && straight(p,i) ?
                                   unithemisphere : unitsphere));
       begin=i;
     }
   path3 g=subpath(p,begin,n);
   generate(g);
 }
}