ShapeJS Developer Tutorials

Distance Operations

The voxel operations provided in ShapeJS are ways of combining the distances of voxels from the origin from two different data sources. In this Tutorial we will learn how to use these advanced operations, which take the interaction of two data sources to produce a third one.

1. Add

This and the subtract (Sub) function might seem like Booleans from their names. However, rather than use one shape to either add to or remove from the other, these actually combine the two, not as a Boolean Union, but as a sort of distortion shaped like the second data source. This allows for the creation of unusual shapes just by combining primitives.

First let's look at addition.

function main(args) {
  var r = 15*MM;
  var sphere = new Torus(r, r/5);
  var box = new Box(0,0,0, 1.5*r,1.5*r, 1.5*r);
  var shape = new Add(sphere,box);

  var s = 26*MM;
  return new Scene(shape,new Bounds(-s,s,-s,s,-s,s));
}

(Note: These work off of the vector of each voxel as related to the origin. For this reason, if one data source or the other does not reach in a certain direction, nothing will display there, since nothing can be added. )

2. Subtract

Unlike a Boolean Subtraction, Sub does not remove any of the data source, but rather subtracts from the distance from the origin.

function main(args) {
  var r = 15*MM;
  var sphere = new Torus(r, r/5);
  var box = new Box(0,0,0, 1.5*r,1.5*r, 1.5*r);
  var shape = new Sub(sphere,box);

  var s = 26*MM;
  return new Scene(shape,new Bounds(-s,s,-s,s,-s,s));
}

3. Mix

The Mix function is perhaps the most commonly useful of this set of functions. It allows you to create a blended shape based on two other shapes. The value for this transformation (the third argument in the function) is essentially the ratio of the two, zero being entirely the first data source, one entirely the second. Let's try putting this one to work, to try and finish up our coffee cup maker.

Our coffee cup has a rather boring handle, and probably not too comfortable of one. What we need to do is to give it more of a usual handle shape.

var uiParams = [
  {
     name: "height",
     label: "Cup height",
     desc: "Height of the cup",
     type: "double",
     rangeMin: 50,
     rangeMax: 200,
     step: 1,
     defaultVal: 120
  },
  {
     name: "thickness",
     label: "Cup Thickness",
     desc: "Thickness of the cup",
     type: "double",
     rangeMin: 3,
     rangeMax: 15,
     step: 0.5,
     defaultVal: 5
  },
  {
     name: "radius",
     label: "Cup Radius",
     desc: "Radius of the cup",
     type: "double",
     rangeMin: 25,
     rangeMax: 120,
     step: 1,
     defaultVal: 60
  },
  {
     name: "h_thickness",
     label: "Handle Thickness",
     desc: "Thickness of the handle",
     type: "double",
     rangeMin: 3,
     rangeMax: 15,
     step: 0.25,
     defaultVal: 5
  },
  {
     name: "h_radius",
     label: "Handle Radius",
     desc: "Radius of the handle",
     type: "double",
     rangeMin: 10,
     rangeMax: 100,
     step: 1,
     defaultVal: 35
  },
  {
     name: "mixer",
     label: "Handle Squaring",
     desc: "Squaring of the handle",
     type: "double",
     rangeMin: 0,
     rangeMax: 1,
     step: 0.1,
     defaultVal: 0
  }
];

function main(args) {
  //Retrieve and scale shopper inputs
  var height = args.height*MM;
  var thickness = args.thickness*MM;
  if (args.thickness<5 && args.radius*2+args.height>150) {
    var thickness = 5*MM;
  }

  var radius = args.radius*MM;
  var h_thickness = args.h_thickness*MM;
  var h_radius = Math.min(args.h_radius, args.height/2-args.h_thickness)*MM;

  //Base Solid shape
  var base = new Union();
  var base_cyl = new Cylinder(new Vector3d(0,-height/2, 0), new Vector3d(0,height/2, 0), radius);
  var base_rim = new Torus(radius-thickness/2, thickness/2);
  var compTransform = new CompositeTransform();
  compTransform.add(new Rotation(Vector3d(1,0,0), Math.PI/2));
  compTransform.add(new Translation(new Vector3d(0,height/2, 0)));
  base_rim.setTransform(compTransform);
  var handle_cyl = new Torus(new Vector3d(-radius,0,0), h_radius, h_thickness);
  var handle_sq = new Box(-radius, 0, 0, h_radius*2+h_thickness*2, h_radius*2+h_thickness*2, h_thickness*2);

  //handle_sq.setBlend(1*MM);
  var h_remove_sq = new Box(-radius, 0, 0, h_radius*2-h_thickness*2, h_radius*2-h_thickness*2, h_thickness*5);
  var h_remove_cyl = new Cylinder(new Vector3d(-radius,0,1), new Vector3d(-radius,0,-1), h_radius-h_thickness);
  var handle_base = new Mix(handle_cyl, handle_sq, args.mixer);
  var h_remove = new Mix(h_remove_cyl, h_remove_sq, args.mixer);
  var handle = new Subtraction(handle_base, h_remove);
  var seat = new Cylinder(new Vector3d(0,-height/2, 0), new Vector3d(0,-height/2+thickness, 0), radius);
  base.add(base_cyl);
  base.add(base_rim);
  base.add(handle);

  //Portions for removal
  var remove_cyl = new Cylinder(new Vector3d(0,(-height+thickness)/2,0), new Vector3d(0,(height+thickness)/2,0), radius-thickness);

  //Assembly
  var cup = new Subtraction(base, remove_cyl);
  var s = 250*MM;
  return new Scene(cup,new Bounds(-s,s,-s,s,-s,s));
}

(Note: Unlike simple Transformations, Mix creates an entirely new data source.)

By adding in the removal portions though, we've created a problem that could crash our maker. Specifically, is could try to create a cylinder with a negative radius if someone create a very small radius but very high thickness handle.

Also, if we have a thin handle, high handle radius, and set the squaring near the midpoint it can cause the handle to split into three , so we'll deal with that as well.

var uiParams = [
  {
     name: "height",
     label: "Cup height",
     desc: "Height of the cup",
     type: "double",
     rangeMin: 50,
     rangeMax: 200,
     step: 1,
     defaultVal: 120
  },
  {
     name: "thickness",
     label: "Cup Thickness",
     desc: "Thickness of the cup",
     type: "double",
     rangeMin: 3,
     rangeMax: 15,
     step: 0.5,
     defaultVal: 5
  },
  {
     name: "radius",
     label: "Cup Radius",
     desc: "Radius of the cup",
     type: "double",
     rangeMin: 25,
     rangeMax: 120,
     step: 1,
     defaultVal: 60
  },
  {
     name: "h_thickness",
     label: "Handle Thickness",
     desc: "Thickness of the handle",
     type: "double",
     rangeMin: 3,
     rangeMax: 15,
     step: 0.25,
     defaultVal: 5
  },
  {
     name: "h_radius",
     label: "Handle Radius",
     desc: "Radius of the handle",
     type: "double",
     rangeMin: 10,
     rangeMax: 100,
     step: 1,
     defaultVal: 35
  },
  {
     name: "mixer",
     label: "Handle Squaring",
     desc: "Squaring of the handle",
     type: "double",
     rangeMin: 0,
     rangeMax: 1,
     step: 0.1,
     defaultVal: 0
  }
];

function main(args) {
  //Retrieve and scale shopper inputs
  var height = args.height*MM;
  var thickness = args.thickness*MM;
  if (args.thickness<5 && args.radius*2+args.height>150){
    var thickness = 5*MM;
  }

  var radius = args.radius*MM;
  var h_thickness = args.h_thickness*MM;
  var h_radius = Math.min(args.h_radius, args.height/2-args.h_thickness)*MM;

  //Base Solid shape
  var base = new Union();
  var base_cyl = new Cylinder(new Vector3d(0,-height/2, 0), new Vector3d(0,height/2, 0), radius);
  var base_rim = new Torus(radius-thickness/2, thickness/2);
  var compTransform = new CompositeTransform();
  compTransform.add(new Rotation(Vector3d(1,0,0), Math.PI/2));
  compTransform.add(new Translation(new Vector3d(0,height/2, 0)));
  base_rim.setTransform(compTransform);
  var handle_cyl = new Torus(new Vector3d(-radius,0,0), h_radius, h_thickness);
  var handle_sq = new Box(-radius, 0, 0, h_radius*2+h_thickness*2, h_radius*2+h_thickness*2, h_thickness*2);
  var handle_base = new Mix(handle_cyl, handle_sq, args.mixer);

  // check on handle
  if (h_radius-h_thickness>0) {
    var h_remove_sq = new Box(-radius, 0, 0, h_radius*2-h_thickness*2, h_radius*2-h_thickness*2, h_thickness*5);
    var h_remove_cyl = new Cylinder(new Vector3d(-radius,0,1), new Vector3d(-radius,0,-1), h_radius-h_thickness);
    var h_remove = new Mix(h_remove_cyl, h_remove_sq, Math.pow(args.mixer,2));
    var handle = new Subtraction(handle_base, h_remove);}
  else {
    var handle = handle_base;
  }

  var seat = new Cylinder(new Vector3d(0,-height/2, 0), new Vector3d(0,-height/2+thickness, 0), radius);
  base.add(base_cyl);
  base.add(base_rim);
  base.add(handle);

  //Portions for removal
  var remove_cyl = new Cylinder(new Vector3d(0,(-height+thickness)/2,0), new Vector3d(0,(height+thickness)/2,0), radius-thickness);

  //Assembly
  var cup = new Subtraction(base, remove_cyl);
  var s = 250*MM;
  return new Scene(cup,new Bounds(-s,s,-s,s,-s,s));
}

4. Mask With Mix

But what if we want to mix part of the data source, not all of it? For this situation, we can use masking. Masking allows us to apply distance operations to a limited portion of the data source.

var uiParams = [
  {
     name: "height",
     label: "Cup height",
     desc: "Height of the cup",
     type: "double",
     rangeMin: 50,
     rangeMax: 200,
     step: 1,
     defaultVal: 120
  },
  {
     name: "thickness",
     label: "Cup Thickness",
     desc: "Thickness of the cup",
     type: "double",
     rangeMin: 3,
     rangeMax: 15,
     step: 0.5,
     defaultVal: 5
  },
  {
     name: "radius",
     label: "Cup Radius",
     desc: "Radius of the cup",
     type: "double",
     rangeMin: 25,
     rangeMax: 120,
     step: 1,
     defaultVal: 60
  },
  {
     name: "h_thickness",
     label: "Handle Thickness",
     desc: "Thickness of the handle",
     type: "double",
     rangeMin: 3,
     rangeMax: 15,
     step: 0.25,
     defaultVal: 5
  },
  {
     name: "h_radius",
     label: "Handle Radius",
     desc: "Radius of the handle",
     type: "double",
     rangeMin: 10,
     rangeMax: 100,
     step: 1,
     defaultVal: 35
  },
  {
     name: "mixer",
     label: "Handle Squaring",
     desc: "Squaring of the handle",
     type: "double",
     rangeMin: 0,
     rangeMax: 1,
     step: 0.1,
     defaultVal: 0
  },
  {
     name: "basemix",
     label: "Base Squaring",
     desc: "Squaring of the base",
     type: "double",
     rangeMin: -1.5,
     rangeMax: 0,
     step: 0.05,
     defaultVal: 0
  }
];

function main(args) {
  //Retrieve and scale shopper inputs
  var height = args.height*MM;
  var thickness = args.thickness*MM;
  if (args.thickness<5 && args.radius*2+args.height>150){
    var thickness = 5*MM;
  }

  var radius = args.radius*MM;
  var h_thickness = args.h_thickness*MM;
  var h_radius = Math.min(args.h_radius, args.height/2-args.h_thickness)*MM;

  //Base Solid shape
  var base = new Union();
  var base_cyl = new Cylinder(new Vector3d(0,-height/2, 0), new Vector3d(0,height/2, 0), radius);
  var base_box = new Box(radius*2, height, radius*2);

  // more mixing
  var plane = new Plane(0,1,0,height*args.basemix);
  var mask = new Mask(plane, 0, 10*MM);
  var based = new Mix(base_cyl,base_box, mask);

  var base_rim = new Torus(radius-thickness/2, thickness/2);
  var compTransform = new CompositeTransform();
  compTransform.add(new Rotation(Vector3d(1,0,0), Math.PI/2));
  compTransform.add(new Translation(new Vector3d(0,height/2, 0)));
  base_rim.setTransform(compTransform);
  var handle_cyl = new Torus(new Vector3d(-radius,0,0), h_radius, h_thickness);
  var handle_sq = new Box(-radius, 0, 0, h_radius*2+h_thickness*2, h_radius*2+h_thickness*2, h_thickness*2);
  var handle_base = new Mix(handle_cyl, handle_sq, args.mixer);

  // check on handle
  if (h_radius-h_thickness>0) {
    var h_remove_sq = new Box(-radius, 0, 0, h_radius*2-h_thickness*2, h_radius*2-h_thickness*2, h_thickness*5);
    var h_remove_cyl = new Cylinder(new Vector3d(-radius,0,1), new Vector3d(-radius,0,-1), h_radius-h_thickness);
    var h_remove = new Mix(h_remove_cyl, h_remove_sq, Math.pow(args.mixer,2));
    var handle = new Subtraction(handle_base, h_remove);}
  else {
    var handle = handle_base;
  }

  var seat = new Cylinder(new Vector3d(0,-height/2, 0), new Vector3d(0,-height/2+thickness, 0), radius);
  base.add(based);
  base.add(base_rim);
  base.add(handle);

  //Portions for removal
  var remove_cyl = new Cylinder(new Vector3d(0,(-height+thickness)/2,0), new Vector3d(0,(height+thickness)/2,0), radius-thickness);

  //Assembly
  var cup = new Subtraction(base, remove_cyl);
  var s = 250*MM;
  return new Scene(cup,new Bounds(-s,s,-s,s,-s,s));
}

Now try adding in checks to ensure that, say, if we wanted to print ceramics, we would not end up with too thick a wall.

(Note: Masks can be used for more than just Mix. They can be used for all distance operations, whenever you are trying to affect only a limited part of a data source.)