Journey from Twist to Spring to a new Creator

This post is filled with new ShapeJS features, twist and turns in development and the birth of a new creator. It all started with some prototyping work for this years art project. We’ve been playing with some triple helix designs. Early in the process having some 3D printed prototypes in hand can really help communicate your designs. One way to make a triple helix is using a Twist transform.  A Twist rotates the object around the z-axis.  In this first example we take the union of three boxes and twist them around into a triple helix structure.


function main(args) {

  var vs = 0.1*MM;
  var r = 1*MM;
  var R = 5*MM;
  var length = 30*MM;

  var w = R + r + vs;  
  var sizeZ = length + 4*vs;

  var grid = createGrid(-w,w,-w,w,-sizeZ/2,sizeZ/2,vs);
  var maker = new GridMaker();

  var union = new Union();

  var part1 = new Box(R, 0, 0, r, 4*r, length);

  var part2 = new Box(R, 0, 0, r, 4*r, length);
  part2.setTransform(new Rotation(0,0,1,PI*2/3));

  var part3 = new Box(R, 0, 0, r, 4*r, length);
  part3.setTransform(new Rotation(0,0,1,-PI*2/3));

  union.setTransform(new Twist(period));

  meshSmoothingWidth = 2;

  return grid;

This technique worked well for a few turns. But a twist operation starts to look weird as you crank it. If you want to make a true Spring or Helix for many turns then you need something else. This time we created a new datasource called Spring. It’s a parametric implementation of an infinite spring.


function main(args) {

  var vs = 0.1*MM;
  var r = 1.47*MM;
  var R = 14.7*MM;
  var springPeriod = 50*MM;
  var springLength = 120*MM;
  var w = R + r + vs;
  var sizeZ = springLength + 2*r + 4*vs;
  var baseHeight = 2*MM;

  var grid = createGrid(-w,w,-w,w,-sizeZ/2,sizeZ/2,vs);
  var maker = new GridMaker();

  var spring1 = new Spring(R,r,springPeriod, springLength);
  var spring2 = new Spring(R,r,springPeriod, springLength);
  var spring3 = new Spring(R,r,springPeriod, springLength);

  spring2.setTransform(new Rotation(0,0,1,2*Math.PI/3));
  spring3.setTransform(new Rotation(0,0,1,2*2*Math.PI/3));
  var union = new Union();

  union.add(new Cylinder(new Vector3d(0,0,-springLength / 2), new Vector3d(0,0,-springLength / 2 - baseHeight), R+r));

  return grid;

This is where our story takes a twist. One of the original creators I made for Shapeways is called the Statement Vase. This creator took a sentence and wrapped it around a cylinder. As we played with these twists and springs I thought it would make a nice variant of that idea. This creator takes an image tile and then wraps it around a cylinder with a specified number of bands. Here is the item 3D printed:


var voxelSize = 0.15*MM;

function makeTile(path, width, height, thickness){
    var img = new ImageBitmap(path, width, height, thickness);
    return img;

// makes rectangular tiling of a plane 
function getSymmetry(width, height){

    var splanes = new Array();
    var count = 0;

    splanes[count++] = new ReflectionSymmetry.getPlane(new Vector3d(-1,0,0),width/2);
    splanes[count++] = new ReflectionSymmetry.getPlane(new Vector3d(0,-1,0),height/2);
    splanes[count++] = new ReflectionSymmetry.getPlane(new Vector3d(1,0,0),width/2);
    splanes[count++] = new ReflectionSymmetry.getPlane(new Vector3d(0,1,0),height/2);

    return new ReflectionSymmetry(splanes);

function main(args) {

    var image = args[0];
    var thickness = 2.2*MM + voxelSize / 2.0;
    var baseThickness = 2.3*MM + voxelSize;
    var tileWidth = 32 *MM;
    var tileHeight = 32 *MM;
    var height = 101.6*MM;

    var wraps = 4; // how many bands
    var tilt = 6;  // how much tilt the bands (should be integer)
    var gapWidth = 0.5; // relative width of gap between bands

    var aspect = 1+ gapWidth;
    var dx = wraps*aspect*tileWidth;
    var dy = tilt*tileHeight;
    var alpha = Math.atan(dy/dx);
    var R = Math.sqrt(dx*dx + dy*dy)/(2*PI); // radius of wrap

    var w = R + thickness + voxelSize;
    var grid = createGrid(-w,w, -height/2, height/2 + baseThickness, -w, w, voxelSize);
    var tile = makeTile(image, tileWidth, tileHeight, thickness);
    var imageTransform = new CompositeTransform();

    imageTransform.add(getSymmetry(aspect*tileWidth, tileHeight));
    imageTransform.add(new Rotation(new Vector3d(0,0,1), alpha));


    var union = new Union();


    // add base
    union.add(new Cylinder(new Vector3d(0,-height/2,0), new Vector3d(0,-height/2 + baseThickness,0), R+thickness/2));

    // add top
    var rim = new Subtraction(new Cylinder(new Vector3d(0,height/2,0), new Vector3d(0,height/2 + baseThickness,0), R+thickness/2),
        new Cylinder(new Vector3d(0,height/2,0), new Vector3d(0,height/2 + baseThickness,0), R-thickness/2));

    var maker = new GridMaker();

    meshSmoothingWidth = 1;

    return grid;

Now if your watching closely you’ll see that I didn’t actually use Twist or Spring in this new creator. Turns out our RingWarp and Symmetry engine was a better fit for this creator. This allowed us to layout the tiles without any distortion. So there’s the twisted journey, I hope you enjoyed it.

This entry was posted in Uncategorized. Bookmark the permalink.

8 Responses to Journey from Twist to Spring to a new Creator

  1. Jbc says:

    I’m looking forward to trying this amazing piece of software next week when home.
    Thanks for the examples.

  2. jbc says:

    Wow. I’d love to try this software out but really need help getting there.
    I cant figure out how to build ant. Ive tried everything but dont have the depth to figure it out.
    Are there any videos that can show me hot to launch this great software? An older video shows stuff that is out of date.
    Any help would be appreciated. I’ve been trying to get this to build ant for the last 10 days. Thanks in advance.

  3. jbc says:

    “From the main directory type: ant build” ?????
    This part has me stumped. Command prompt? I tried that and even tried changing directory to apache-ant-1.9.3.

    Any help would be greatly appreciated.

    • Alan Hudson says:

      I’d suggest starting from That is the most user friendly place to begin. If you get interested in changing the code then you can worry about compiling the library.

  4. Will says:

    Hi Alan,

    I have built some objects in shapejs using the editor and viz at I would like to run my shapejs locally and have built AbFab3d locally and run some of the examples.

    I want to build a webpage that users simply enter a number of values, then my shapejs creates an object using abfab3d and the user then gets an .x3d file to view in the browser and then can print it.

    Is this possible? The examples are about object creation – I just want to take my shapejs that i made on shapeways and have that run locally “behind the scenes”.


    • Alan Hudson says:

      Yes its possible but perhaps not easy currently. You’ll need to know Java and how to deploy your own server.

      We have a new system coming online in April that should make this process a lot easier. If you want to try the current Java code and I give some pointers or I’ll post when the new system is available.

      • Will says:

        Hi Alan thanks for the reply! I am familiar with both – It would be ideal if I could make a proof of concept this week using the existing, and then maybe migrate to your new system later.

        I found shapeJS in the “apps” folder but can’t work out how it works as I can’t see the source /docs for this.

        I can’t email you through here but I guess you have my email address from the post – would be brilliant to get some pointers!


  5. Alan Hudson says:

    Check apps/volumesculptor for the main code. I don’t have a lot of time this week so I’ll try to outline it briefly. Here is the our servlet code to execute a script. It using a class apps/volumesculptor/src/java/volumesculptor/ This use as a class called Main. It has some convoluted code from the ecmascript project I borrowed from, sorry.

    I’m not too happy with any of this code, lots of much prettier patterns in the next rev. Another option is to just exec call such as the ant runShell task. Ie you’d give it the params, it would generate the file you want. That might be more self contained for a prototype.

    protected int executeJava(String name, ParamMap params, File dir, ParamMap output) {
    String script_content = params.getString(“script”);

    // If script contents are null, try contents of scriptURL
    // scriptURL should be path to a local file at this point
    if (script_content == null || script_content.length() == 0) {
    String script_file = params.getString(“scriptURL”);

    if (script_file == null) {
    LOG.warn(“Missing both script and scriptURL”);
    return ExitCode.INVALID_ARGUMENTS.getCode();

    // Put contents of the script file into the parameter “script”
    try {
    params.put(“script”, FileUtils.readFileToString(new File(script_file)));
    } catch (Exception e) {
    LOG.error(“Failed to open script file: ” + script_file);
    return ExitCode.ABNORMAL_CRASH.getCode();

    String val = (String) params.get(ENCODING);

    X3DEncoding encoding = X3DEncoding.BINARY;
    if (val != null) {
    encoding = X3DEncoding.valueOf(val);
    } else {
    // TODO: Should service handler do this for us?
    encoding = X3DEncoding.XML;

    val = (String) params.get(ACCURACY);

    Accuracy accuracy = null;
    if (val != null) {
    accuracy = Accuracy.valueOf(val);
    } else {
    // TODO: Should service handler do this for us?
    accuracy = Accuracy.VISUAL;

    Boolean excludeHeaders = (Boolean) params.get(EXCLUDE_HEADERS);
    if (excludeHeaders == null) {
    excludeHeaders = false;

    int timeout = EXECUTE_TIMEOUT;
    Double timeout_mins = (Double) params.get(“maxRunTime”);
    if (timeout_mins != null) timeout = (int) (timeout_mins * 60 * 1000);

    ThreadKiller champ = new ThreadKiller(Thread.currentThread(), timeout);

    try {
    VolumeSculptorKernel kernel = new VolumeSculptorKernel();

    Map kparams = params.getStringMap();

    Map parsed_params = ParameterUtil.parseParams(kernel.getParams(), kparams);
    // Put the geometry in a byte array, let wrapper write to disk if desired
    ByteArrayOutputStream baos = new ByteArrayOutputStream(8 * 1024);
    BufferedOutputStream bos = new BufferedOutputStream(baos);

    PlainTextErrorReporter console = new PlainTextErrorReporter();

    BinaryContentHandler writer = null;

    switch (encoding) {
    case BINARY:
    writer = (BinaryContentHandler) new X3DBinaryRetainedDirectExporter(bos,
    3, 2, console,
    0.001f, true);
    case XML:
    // TODO: change sig based on accuracy?
    writer = new X3DXMLRetainedExporter(bos, 3, 2, console, 6);

    if (excludeHeaders) {
    ((X3DXMLRetainedExporter) writer).setPrintDocType(false);
    ((X3DXMLRetainedExporter) writer).setPrintXML(false);
    case CLASSIC:
    // TODO: change sig based on accuracy?
    writer = new X3DClassicRetainedExporter(bos, 3, 2, console, 6);
    throw new IllegalArgumentException(“Unsupported Encoding: ” + encoding);

    writer.startDocument(“”, “”, “utf8”, “#X3D”, “V3.2”, “”);
    writer.startNode(“NavigationInfo”, null);
    writer.fieldValue(new float[]{0.01f, 1.6f, 0.75f}, 3);

    GeometryKernel.Accuracy acc = null;
    switch (accuracy) {
    case PRINT:
    acc = GeometryKernel.Accuracy.PRINT;
    case VISUAL:
    acc = GeometryKernel.Accuracy.VISUAL;
    }“Running kernel: ” + params + ” kernel: ” + kernel);
    KernelResults results = kernel.generate(parsed_params, acc, writer);

    if (results.getFailureCode() == 0) {
    CreatorUtils.generateViewpoint(writer, results.getMinBounds(), results.getMaxBounds(), “Global”);



    String output_st = getOutput();

    StringTokenizer lineTokenizer = new StringTokenizer(output_st, “\n”);

    while (lineTokenizer.hasMoreTokens()) {
    String line = lineTokenizer.nextToken().trim();

    // Unexplained error that seems to happen
    if (line.contains(ERROR_MARKER)) {
    System.out.println(“Found the Sphere missing problem.”);
    // rerun with external JVM
    throw new InternalJVMException(“VolumeSculptor Found: ” + ERROR_MARKER);

    Map out_params = results.getOutput();
    String prints = (String) out_params.get(“debugPrint”);
    if (prints != null) {
    output.put(DEBUG_PRINT, prints);

    if (results.getFailureCode() != 0) {
    output.put(RUNTIME_ERROR, results.getReason());
    } else {
    URIParameterByteArrayWrapper wrapper = new URIParameterByteArrayWrapper(baos.toByteArray(), encoding.getFileEnding());
    output.put(OUTPUT, wrapper);
    output.put(VOLUME, results.getVolume());
    output.put(AREA, results.getSurfaceArea());
    output.put(REGIONS_REMOVED, results.getRegionsRemoved());

    return results.getFailureCode();
    } catch (IOException ioe) {

    // TODO: Not sure this is always right exit code
    return ExitCode.CANNOT_WRITE_OUTPUT_FILE.getCode();
    } catch(UnsupportedSpecVersionException usve) {
    // I believe this is caused by the classloader losing classes

    LOG.warn(“Caught permanent error: ” + usve.getMessage());
    return ExitCode.PERMANENT_BAD_STATE.getCode();
    } catch(NoClassDefFoundError ncde) {
    // I believe this is caused by the classloader losing classes

    LOG.warn(“Caught permanent error: ” + ncde.getMessage());
    return ExitCode.PERMANENT_BAD_STATE.getCode();
    } catch(InternalJVMException ije) {“Caught InternalJVM, rethrowing”);
    throw ije;
    } catch(Throwable t) {
    LOG.error(“Unknown throwable caught: ” + t.getMessage(),t);

    return ExitCode.ABNORMAL_CRASH.getCode();
    } finally {

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s