docs: function(options) {
              if (this.sides.length == 0) {
                  // empty!
                  return new CSG();
              }
              var offsetVector = CSG.parseOptionAs3DVector(options, "offset", [0, 0, 1]);
              var twistangle = CSG.parseOptionAsFloat(options, "twistangle", 0);
              var twiststeps = CSG.parseOptionAsInt(options, "twiststeps", CSG.defaultResolution3D);
              if (offsetVector.z == 0) {
                  throw('offset cannot be orthogonal to Z axis');
              }
              if (twistangle == 0 || twiststeps < 1) {
                  twiststeps = 1;
              }
              var normalVector = CSG.Vector3D.Create(0, 1, 0);
  
              var polygons = [];
              // bottom and top
              polygons = polygons.concat(this._toPlanePolygons({translation: [0, 0, 0],
                  normalVector: normalVector, flipped: !(offsetVector.z < 0)}));
              polygons = polygons.concat(this._toPlanePolygons({translation: offsetVector,
                  normalVector: normalVector.rotateZ(twistangle), flipped: offsetVector.z < 0}));
              // walls
              for (var i = 0; i < twiststeps; i++) {
                  var c1 = new CSG.Connector(offsetVector.times(i / twiststeps), [0, 0, offsetVector.z],
                      normalVector.rotateZ(i * twistangle/twiststeps));
                  var c2 = new CSG.Connector(offsetVector.times((i + 1) / twiststeps), [0, 0, offsetVector.z],
                      normalVector.rotateZ((i + 1) * twistangle/twiststeps));
                  polygons = polygons.concat(this._toWallPolygons({toConnector1: c1, toConnector2: c2}));
              }
  
              return CSG.fromPolygons(polygons);
          }