ShapeJS Developer Tutorials

Boolean Functions

The Boolean functions are useful basic tools for modifying and combining data sources within ShapeJS. They allow for a method of modeling known as solid modeling, which can be seen below:

At the top, we perform an intersection, so as to keep only the parts of the sphere and the box which overlap. The bottom is a union, a combination of the three cylinders. In the middle, we have a subtraction, taking those cylinders out of the counded box we created at the top. In this tutorial, we will use the shapes and transforms we already know in order to model a Rook entirely in ShapeJS, learning all the boolean functions along the way.

1. Union

Union is the simplest boolean. It takes the two or more data sources added to it and combines them into a single data source. This allows you to display or transform them the same way as an individual data source.

In this simple example, we union a box and sphere.

function main(args) {
  var box = new Box(0, 0, 0, 20*MM, 5*MM, 5*MM);
  var sphere = new Sphere(7*MM);
  var union = new Union();
  union.add(box);
  union.add(sphere);
  var s = 12*MM;
  return new Scene(union, new Bounds(-s,s,-s,s,-s,s));
}

Let's try a slightly more interesting union. In the code below you can see four different different shapes, three cylinders for the base and a box for the trunk, that we can use as the start of our Rook. They have all been added to a single Union called "assembled."

function main(args) {
  //Trunk Variables
  var trunk_scaley = 20*MM;
  var trunk_scalexz = 12*MM;
  //Base Variables
  var base1_top = new Vector3d(0, -10*MM, 0);
  var base1_bottom = new Vector3d(0, -12*MM, 0);
  var base2_bottom = new Vector3d(0, -13*MM, 0);
  var base3_bottom = new Vector3d(0, -18*MM, 0);
  var base1_radius = 10*MM;
  var base2_radius = 12*MM;

  //Trunk Data Source
  var trunk = new Box(trunk_scalexz, trunk_scaley, trunk_scalexz);
  //Base Data Sources
  var base1 = new Cylinder(base1_top, base1_bottom, base1_radius);
  var base2 = new Cylinder(base1_bottom, base2_bottom, base1_radius, base2_radius);
  var base3 = new Cylinder(base2_bottom, base3_bottom, base2_radius, base2_radius);

  //Assembly
  var assembled = new Union();
  assembled.add(trunk);
  assembled.add(base1);
  assembled.add(base2);
  assembled.add(base3);

  //Display
  var s = 20*MM;
  return new Scene(assembled, new Bounds(-s,s,-s,s,-s,s));
}

We did not bother to separately define the tops of bases 2 or 3. This is because we want the whole base connected and so the top of the second part should always be the bottom of the first, even if we later decide to change the size.

Because we created a Union with these four data sources, we can simply tell ShapeJS to return that Union in the scene so as to display all of them. We will add subsequent parts of the Rook to this Union.

(Note: Union can be applied to data sources even if they do not intersect with one another. If you need to create and display a scene with multiple parts you can do so using Union.)

2. Subtraction

The Subtraction function takes two data sources, and treats the second source as a negative, removing everything it overlaps on the first data source. Let's start with a simple example.

function main(args) {
  var box = new Box(0, 0, 0, 5*MM, 5*MM, 20*MM);
  var sphere = new Sphere(7*MM);
  var sub = new Subtraction(sphere,box);
  var s = 12*MM;
  return new Scene(sub, new Bounds(-s,s,-s,s,-s,s));
}

It would be very difficult to model the Rook's distinctive turret just using primitives. By using Booleans we can carry out the far simpler task of creating the shape we need to remove from the basic cylinder of the turret. What we want is a cylinder to start with, to remove the center of the turret. At that point, however, what we have is an impractical cup. We also need three boxes removed to create the battlements.

Once we have all these data sources, we run into a problem: Subtraction only takes two sources, but we have five. We could boolean out each one individually, but that would be a pain. What we can do instead is perform a Union on the four shapes to be removed, and then use that as the second source for a single Subtraction.

function main(args) {
  //Turret Variables
  var turret_top = new Vector3d(0, 18*MM, 0);
  var turret_bottom = new Vector3d(0, 12*MM, 0);
  var remove_top = new Vector3d(0, 18.1*MM, 0);
  var remove_bottom = new Vector3d(0, 14*MM, 0);
  var remove_radius = 10*MM;
  var turret_radius = 12*MM;
  var remove_box_angle = Math.PI*(1+2/3);
  var remove_box_size = 4*MM;
  var remove_box_y = 18*MM;
  //Trunk Variables
  var trunk_scaley = 20*MM;
  var trunk_scalexz = 12*MM;
  //Base Variables
  var base1_top = new Vector3d(0, -10*MM, 0);
  var base1_bottom = new Vector3d(0, -12*MM, 0);
  var base2_bottom = new Vector3d(0, -13*MM, 0);
  var base3_bottom = new Vector3d(0, -18*MM, 0);
  var base1_radius = 10*MM;
  var base2_radius = 12*MM;

  //Turret Data Sources
  var turret = new Cylinder(turret_bottom, turret_top, turret_radius);
  var remove_cyl = new Cylinder(remove_top, remove_bottom, remove_radius);
  var remove_box1 = new Box(0,remove_box_y,0,remove_box_size,remove_box_size,1);
  var remove_box2 = new Box(0,remove_box_y,0,remove_box_size,remove_box_size,1);
  remove_box2.setTransform(new Rotation(0,1,0,remove_box_angle));
  var remove_box3 = new Box(0,remove_box_y,0,remove_box_size,remove_box_size,1);
  remove_box3.setTransform(new Rotation(0,1,0,-remove_box_angle));
  var remove = new Union();
  remove.add(remove_cyl);
  remove.add(remove_box1);
  remove.add(remove_box2);
  remove.add(remove_box3);
  var turret_carved = new Subtraction(turret, remove);
  //Trunk Data Source
  var trunk = new Box(trunk_scalexz, trunk_scaley, trunk_scalexz);
  //Base Data Sources
  var base1 = new Cylinder(base1_top, base1_bottom, base1_radius);
  var base2 = new Cylinder(base1_bottom, base2_bottom, base1_radius, base2_radius);
  var base3 = new Cylinder(base2_bottom, base3_bottom, base2_radius, base2_radius);

  //Assembly
  var assembled = new Union();
  assembled.add(turret_carved);
  assembled.add(trunk);
  assembled.add(base1);
  assembled.add(base2);
  assembled.add(base3);

  //Display
  var s = 20*MM;
  return new Scene(assembled, new Bounds(-s,s,-s,s,-s,s));
}

(Note: For more information on Transformations, such as the rotation used here, please refer to the tutorial on that subject.)

3. Intersection

The Intersection Boolean will result in a data source that is comprised only of the area in which all its shapes overlap. Here is a simple intersection between a box and sphere.

function main(args) {
  var box = new Box(0, 0, 0, 20*MM, 7*MM, 7*MM);
  var sphere = new Sphere(7*MM);
  var intersection = new Intersection(sphere,box);
  var s = 12*MM;
  return new Scene(intersection, new Bounds(-s,s,-s,s,-s,s));
}

You may have noticed a gap between the turret and the trunk. We could just extend the trunk up or the turret down, but a smoother transition, like what we have in the base, would be more appealing. An intersection can be used to create non-standard shapes, such as below, where five planes are used to create an inverted pyramid shape.

function main(args) {
  //Turret Variables
  var turret_top = new Vector3d(0, 18*MM, 0);
  var turret_bottom = new Vector3d(0, 12*MM, 0);
  var remove_top = new Vector3d(0, 18.1*MM, 0);
  var remove_bottom = new Vector3d(0, 14*MM, 0);
  var remove_radius = 10*MM;
  var turret_radius = 12*MM;
  var remove_box_angle = Math.PI*(1+2/3);
  var remove_box_size = 4*MM;
  var remove_box_y = 18*MM;
  //Trunk Variables
  var trunk_scaley = 20*MM;
  var trunk_scalexz = 12*MM;
  var xz_start = 6*MM;
  var y_start = 10*MM;
  var xz_end = 8*MM;
  var y_end = 12*MM;
  //Base Variables
  var base1_top = new Vector3d(0, -10*MM, 0);
  var base1_bottom = new Vector3d(0, -12*MM, 0);
  var base2_bottom = new Vector3d(0, -13*MM, 0);
  var base3_bottom = new Vector3d(0, -18*MM, 0);
  var base1_radius = 10*MM;
  var base2_radius = 12*MM;

  //Turret Data Sources
  var turret = new Cylinder(turret_bottom, turret_top, turret_radius);
  var remove_cyl = new Cylinder(remove_top, remove_bottom, remove_radius);
  var remove_box1 = new Box(0,remove_box_y,0,remove_box_size,remove_box_size,1);
  var remove_box2 = new Box(0,remove_box_y,0,remove_box_size,remove_box_size,1);
  remove_box2.setTransform(new Rotation(0,1,0,remove_box_angle));
  var remove_box3 = new Box(0,remove_box_y,0,remove_box_size,remove_box_size,1);
  remove_box3.setTransform(new Rotation(0,1,0,-remove_box_angle));
  var remove = new Union();
  remove.add(remove_cyl);
  remove.add(remove_box1);
  remove.add(remove_box2);
  remove.add(remove_box3);
  var turret_carved = new Subtraction(turret, remove);
  //Trunk Data Sources
  var trunk = new Box(trunk_scalexz, trunk_scaley, trunk_scalexz);
  var grow_side1 = new Plane(new Vector3d(0,-1,1), new Vector3d(xz_start, y_start, xz_start));
  var grow_side2 = new Plane(new Vector3d(0,-1,-1), new Vector3d(xz_start, y_start, -xz_start));
  var grow_side3 = new Plane(new Vector3d(1,-1,0), new Vector3d(xz_start, y_start, xz_start));
  var grow_side4 = new Plane(new Vector3d(-1,-1,0), new Vector3d(-xz_start, y_start, xz_start));
  var grow_top = new Plane(new Vector3d(0,1,0), new Vector3d(0, y_end, 0));
  var grow = new Intersection();
  grow.add(grow_side1);
  grow.add(grow_side2);
  grow.add(grow_side3);
  grow.add(grow_side4);
  grow.add(grow_top);
  //Base Data Sources
  var base1 = new Cylinder(base1_top, base1_bottom, base1_radius);
  var base2 = new Cylinder(base1_bottom, base2_bottom, base1_radius, base2_radius);
  var base3 = new Cylinder(base2_bottom, base3_bottom, base2_radius, base2_radius);

  //Assembly
  var assembled = new Union();
  assembled.add(turret_carved);
  assembled.add(grow);
  assembled.add(trunk);
  assembled.add(base1);
  assembled.add(base2);
  assembled.add(base3);

  //Display
  var s = 20*MM;
  return new Scene(assembled, new Bounds(-s,s,-s,s,-s,s));
}

(Note: You will need to use Boolean Intersects or Boolean Subtractions in order to limit the size of cones and planes.)

4. Complement

Boolean Complement essentially creates the entire bounding box, minus whatever shape you are finding the complement of. This operation is less commonly used than the first three Booleans, but can still come in handy. Here, we can use it to just add in a cone complement to the "remove" Union, rather than adding in a new intersection, in order to create rounded battlements

function main(args) {
  //Turret Variables
  var turret_top = new Vector3d(0, 18*MM, 0);
  var turret_bottom = new Vector3d(0, 12*MM, 0);
  var remove_top = new Vector3d(0, 18.1*MM, 0);
  var remove_bottom = new Vector3d(0, 14*MM, 0);
  var remove_radius = 10*MM;
  var turret_radius = 12*MM;
  var remove_box_angle = Math.PI*(1+2/3);
  var remove_box_size = 4*MM;
  var remove_box_y = 18*MM;
  var remove_cone_apex = new Vector3d(0, 29*MM, 0);
  var remove_cone_angle = Math.PI/4;
  //Trunk Variables
  var trunk_scaley = 20*MM;
  var trunk_scalexz = 12*MM;
  var xz_start = 6*MM;
  var y_start = 10*MM;
  var xz_end = 8*MM;
  var y_end = 12*MM;
  //Base Variables
  var base1_top = new Vector3d(0, -10*MM, 0);
  var base1_bottom = new Vector3d(0, -12*MM, 0);
  var base2_bottom = new Vector3d(0, -13*MM, 0);
  var base3_bottom = new Vector3d(0, -18*MM, 0);
  var base1_radius = 10*MM;
  var base2_radius = 12*MM;

  //Turret Data Sources
  var turret = new Cylinder(turret_bottom, turret_top, turret_radius);
  var remove_cyl = new Cylinder(remove_top, remove_bottom, remove_radius);
  var remove_box1 = new Box(0,remove_box_y,0,remove_box_size,remove_box_size,1);
  var remove_box2 = new Box(0,remove_box_y,0,remove_box_size,remove_box_size,1);
  remove_box2.setTransform(new Rotation(0,1,0,remove_box_angle));
  var remove_box3 = new Box(0,remove_box_y,0,remove_box_size,remove_box_size,1);
  remove_box3.setTransform(new Rotation(0,1,0,-remove_box_angle));
  var cone = new Cone(remove_cone_apex, new Vector3d(0,-1,0), remove_cone_angle);
  var remove_cone = new Complement(cone);
  var remove = new Union();
  remove.add(remove_cyl);
  remove.add(remove_box1);
  remove.add(remove_box2);
  remove.add(remove_box3);
  remove.add(remove_cone);
  var turret_carved = new Subtraction(turret, remove);
  //Trunk Data Sources
  var trunk = new Box(trunk_scalexz, trunk_scaley, trunk_scalexz);
  var grow_side1 = new Plane(new Vector3d(0,-1,1), new Vector3d(xz_start, y_start, xz_start));
  var grow_side2 = new Plane(new Vector3d(0,-1,-1), new Vector3d(xz_start, y_start, -xz_start));
  var grow_side3 = new Plane(new Vector3d(1,-1,0), new Vector3d(xz_start, y_start, xz_start));
  var grow_side4 = new Plane(new Vector3d(-1,-1,0), new Vector3d(-xz_start, y_start, xz_start));
  var grow_top = new Plane(new Vector3d(0,1,0), new Vector3d(0, y_end, 0));
  var grow = new Intersection();
  grow.add(grow_side1);
  grow.add(grow_side2);
  grow.add(grow_side3);
  grow.add(grow_side4);
  grow.add(grow_top);
  //Base Data Sources
  var base1 = new Cylinder(base1_top, base1_bottom, base1_radius);
  var base2 = new Cylinder(base1_bottom, base2_bottom, base1_radius, base2_radius);
  var base3 = new Cylinder(base2_bottom, base3_bottom, base2_radius, base2_radius);

  //Assembly
  var assembled = new Union();
  assembled.add(turret_carved);
  assembled.add(grow);
  assembled.add(trunk);
  assembled.add(base1);
  assembled.add(base2);
  assembled.add(base3);

  //Display
  var s = 20*MM;
  return new Scene(assembled, new Bounds(-s,s,-s,s,-s,s));
}

(Note: You can generally use Intersections and Subtractions in place of the Complement, but sometimes, like here, Complements are faster and easier to add.)

5. Blending

When performing Boolean operations, you may end up with sharp edges that you do not want. In this case, you can use the setBlend() function to add smoothing in the overlapping area.

Let's try out blending as a way to get rid of the hard line at the base, where the cylinders attach to the toruses, then we might like. We do not want to blend the whole Rook so we will need to move the specific data sources we want smoother into a new Union, then add that new Union to the "assembled" Union. We'll do something similar in order to smooth the portion of the battlements we just used the cone Complement to Boolean.

function main(args) {
  var blend = 1*MM;
  //Turret Variables
  var turret_top = new Vector3d(0, 18*MM, 0);
  var turret_bottom = new Vector3d(0, 12*MM, 0);
  var remove_top = new Vector3d(0, 18.1*MM, 0);
  var remove_bottom = new Vector3d(0, 14*MM, 0);
  var remove_radius = 10*MM;
  var turret_radius = 12*MM;
  var remove_box_angle = Math.PI*(1+2/3);
  var remove_box_size = 4*MM;
  var remove_box_y = 18*MM;
  var remove_cone_apex = new Vector3d(0, 29*MM, 0);
  var remove_cone_angle = Math.PI/4;
  //Trunk Variables
  var trunk_scaley = 20*MM;
  var trunk_scalexz = 12*MM;
  var xz_start = 6*MM;
  var y_start = 10*MM;
  var xz_end = 8*MM;
  var y_end = 12*MM;
  //Base Variables
  var base1_top = new Vector3d(0, -10*MM, 0);
  var base1_bottom = new Vector3d(0, -12*MM, 0);
  var base2_bottom = new Vector3d(0, -13*MM, 0);
  var base3_bottom = new Vector3d(0, -18*MM, 0);
  var base1_radius = 10*MM;
  var base2_radius = 12*MM;
  var base_torus_radius = 1.5*MM;

  //Turret Data Sources
  var turret = new Cylinder(turret_bottom, turret_top, turret_radius);
  var remove_cyl = new Cylinder(remove_top, remove_bottom, remove_radius);
  var remove_box1 = new Box(0,remove_box_y,0,remove_box_size,remove_box_size,1);
  var remove_box2 = new Box(0,remove_box_y,0,remove_box_size,remove_box_size,1);
  remove_box2.setTransform(new Rotation(0,1,0,remove_box_angle));
  var remove_box3 = new Box(0,remove_box_y,0,remove_box_size,remove_box_size,1);
  remove_box3.setTransform(new Rotation(0,1,0,-remove_box_angle));
  var cone = new Cone(remove_cone_apex, new Vector3d(0,-1,0), remove_cone_angle);
  var remove_cone = new Complement(cone);
  var remove = new Union();
  remove.add(remove_cyl);
  remove.add(remove_box1);
  remove.add(remove_box2);
  remove.add(remove_box3);
  var turret_carved_initial = new Subtraction(turret, remove);
  var turret_carved = new Subtraction(turret_carved_initial, remove_cone);
  turret_carved.setBlend(blend);
  //Trunk Data Sources
  var trunk = new Box(trunk_scalexz, trunk_scaley, trunk_scalexz);
  var grow_side1 = new Plane(new Vector3d(0,-1,1), new Vector3d(xz_start, y_start, xz_start));
  var grow_side2 = new Plane(new Vector3d(0,-1,-1), new Vector3d(xz_start, y_start, -xz_start));
  var grow_side3 = new Plane(new Vector3d(1,-1,0), new Vector3d(xz_start, y_start, xz_start));
  var grow_side4 = new Plane(new Vector3d(-1,-1,0), new Vector3d(-xz_start, y_start, xz_start));
  var grow_top = new Plane(new Vector3d(0,1,0), new Vector3d(0, y_end, 0));
  var grow = new Intersection();
  grow.add(grow_side1);
  grow.add(grow_side2);
  grow.add(grow_side3);
  grow.add(grow_side4);
  grow.add(grow_top);
  //Base Data Sources
  var base1 = new Cylinder(base1_top, base1_bottom, base1_radius);
  var base2 = new Cylinder(base1_bottom, base2_bottom, base1_radius, base2_radius);
  var base3 = new Cylinder(base2_bottom, base3_bottom, base2_radius, base2_radius);
  var base_torus1 = new Torus(base2_radius, base_torus_radius);
  compTransform = new CompositeTransform();
  base_torus1.setTransform(compTransform);
  compTransform.add(new Rotation(1,0,0,Math.PI/2));
  compTransform.add(new Translation(0, -14.5*MM, 0));
  var base_torus2 = new Torus(base2_radius, base_torus_radius);
  compTransform2 = new CompositeTransform();
  base_torus2.setTransform(compTransform2);
  compTransform2.add(new Rotation(1,0,0,Math.PI/2));
  compTransform2.add(new Translation(0, -17.5*MM, 0));
  var base_blended = new Union();
  base_blended.setBlend(blend);
  base_blended.add(base3);
  base_blended.add(base_torus1);
  base_blended.add(base_torus2);

  //Assembly
  var assembled = new Union();
  assembled.add(turret_carved);
  assembled.add(grow);
  assembled.add(trunk);
  assembled.add(base1);
  assembled.add(base_blended);
  assembled.add(base2);

  //Smooth the Base
  var base_smooth = new Cylinder(base3_bottom, new Vector3d(0,-1,0), 1);
  var assembled_based = new Subtraction(assembled, base_smooth);

  //Display
  var s = 20*MM;
  return new Scene(assembled_based, new Bounds(-s,s,-s,s,-s,s));
}

(Note: Smoothing cannot be used on a single shape, and will only smooth the portion of the data source(s) affected by the Boolean.)

Now we have our Rook all smoothed out. Since the entire Rook has been put into a Boolean Union, we can apply transformations to that Union in order to apply them to the entire data source. Go ahead and give that a shot. To get something like what is below, you will need a composite transform.