Menu

ExPolygons 5

Timo Kähkönen

Donate Javascript Clipper Project

C. Boolean operations of polygons using ExPolygon structure

There was a bug in ExPolygons feature in Javascript Clipper version 5.0.2.1 and prior, that makes using ExPolygons impossible. The bug is fixed in version 5.0.2.2.

Sometimes you need to know, which hole belongs to which contour. This is the case eg. in triangulating polygons for use in 3D programs. This is not possible using Polygons structure. The answer is ExPolygons structure.

ExPolygon is an 'extended' polygon structure that encapsulates an outer polygon contour together with any contained polygon holes. ExPolygon is a javascript object, that has two members: "outer" (Polygon structure ie array of points) and "holes" (Array of Polygon structures). "Outer" contains zero or one Polygon structures, but "holes" can contain zero, one or several Polygon structures.

The solution parameter in Clipper's overloaded Execute method can return either a Polygons structure or an ExPolygons structure.

Note: The ExPolygon structure is not used (or needed) for adding polygons to Clipper objects. The simpler Polygon structure is perfectly sufficient for that since inner 'hole' polygon contours are indicated by having their Orientation opposite that of outer container polygon contours. This structure is provided simply for those users who need to have inner 'hole' polygons explicitly associated with their containers.

ExPolygons (Javascript Array) encapsulates one or a number of ExPolygon structures (Javascript Objects).

C1. Using ExPolygons

Using ExPolygons structure is nearly identical as using Polygons structure (as described here). The procedure is nearly identical:

:::javascript

var subj_polygons = [[{X:10,Y:10},{X:110,Y:10},{X:110,Y:110},{X:10,Y:110}],
                     [{X:20,Y:20},{X:20,Y:100},{X:100,Y:100},{X:100,Y:20}]]; 
var clip_polygons = [[{X:50,Y:50},{X:150,Y:50},{X:150,Y:150},{X:50,Y:150}],
                     [{X:60,Y:60},{X:60,Y:140},{X:140,Y:140},{X:140,Y:60}]];
var scale = 100;
subj_polygons = scaleup(subj_polygons, scale);
clip_polygons = scaleup(clip_polygons, scale);
var cpr = new ClipperLib.Clipper();
cpr.AddPolygons(subj_polygons, ClipperLib.PolyType.ptSubject);
cpr.AddPolygons(clip_polygons, ClipperLib.PolyType.ptClip);
var subject_fillType = ClipperLib.PolyFillType.pftNonZero;
var clip_fillType = ClipperLib.PolyFillType.pftNonZero;
var clipTypes = [ClipperLib.ClipType.ctUnion;
var solution_expolygons = new ClipperLib.ExPolygons();
cpr.Execute(clipTypes[i], solution_expolygons, subject_fillType, clip_fillType);
//console.log(JSON.stringify(solution_expolygons));

As you see in the above code, polygons are added as Polygons, not as ExPolygons. The difference compared to Polygons lies in the following line:

:::javascript

var solution_expolygons = new ClipperLib.ExPolygons();

The ExPolygons structure is created using new ClipperLib.ExPolygons(), while Polygons structure is created using new ClipperLib.Polygons(). This is the only difference in this case.

While the difference seems to be so small in usage, the solution (eg. union result) is more different. Let's first see, what the solution looks like, when cpr.Execute is called using new ClipperLib.Polygons() (coordinates are scaled down for simplicity):

[
  [
    {"X":50,"Y":150},
    {"X":50,"Y":110},
    {"X":10,"Y":110},
    {"X":10,"Y":10},
    {"X":110,"Y":10},
    {"X":110,"Y":50},
    {"X":150,"Y":50},
    {"X":150,"Y":150}
  ],
  [
    {"X":60,"Y":140},
    {"X":140,"Y":140},
    {"X":140,"Y":60},
    {"X":110,"Y":60},
    {"X":110,"Y":110},
    {"X":60,"Y":110}
  ],
  [
    {"X":20,"Y":100},
    {"X":50,"Y":100},
    {"X":50,"Y":50},
    {"X":100,"Y":50},
    {"X":100,"Y":20},
    {"X":20,"Y":20}
  ],
  [
    {"X":60,"Y":100},
    {"X":100,"Y":100},
    {"X":100,"Y":60},
    {"X":60,"Y":60}
  ]
]

Above four polygons are in more or less random order, and you cannot be sure which is hole and which is contour without examining eg. the area of polygons.

When cpr.Execute is called using new ClipperLib.ExPolygons(), the solution in JSON format is after execution (coordinates are scaled down for simplicity):

[
  {
    "outer":
    [
        {"X":50,"Y":150},
        {"X":50,"Y":110},
        {"X":10,"Y":110},
        {"X":10,"Y":10},
        {"X":110,"Y":10},
        {"X":110,"Y":50},
        {"X":150,"Y":50},
        {"X":150,"Y":150}
    ],
    "holes":
    [
        [
          {"X":60,"Y":140},
          {"X":140,"Y":140},
          {"X":140,"Y":60},
          {"X":110,"Y":60},
          {"X":110,"Y":110},
          {"X":60,"Y":110}
        ],
        [
          {"X":20,"Y":100},
          {"X":50,"Y":100},
          {"X":50,"Y":50},
          {"X":100,"Y":50},
          {"X":100,"Y":20},
          {"X":20,"Y":20}
        ],
        [
          {"X":60,"Y":100},
          {"X":100,"Y":100},
          {"X":100,"Y":60},
          {"X":60,"Y":60}
        ]
    ]
  }
]

The above contains one ExPolygon object, that has one outer polygon contour and three holes. You can now be sure that those holes are contained by the contour in question and not by some other contour (if there were such ones). Also you can be sure, which is hole and which is contour.

As you see, the structure above is an array (ExPolygons structure). This array can contain more than one ExPolygon objects.

If you need the full parent-child relationship information of holes and contours, ExPolygons cannot tell it for you. In Clipper Ver 5.1, the ExPolygons structure was replaced with the PolyTree class, which better represents the parent-child relationships of the returned polygons, but unfortunately is also slower than ExPolygons (or Polygons). The Clipper version 5.1 (or newer) is not released yet.


C2. Drawing boolean operated ExPolygons

As we made in the examples of boolean operated polygons and offsetting polygons, we'll present full functional example of boolean operations using ExPolygons and drawing them on inline SVG.

C2.1 FULL CODE EXAMPLE: Drawing boolean operated ExPolygons using SVG

As in Polygons sample code, we use <path> element of SVG to draw ExPolygons. Each sub polygon is drawn as one subpath. Each subpath starts with M (moveTo) and ends with Z (closePath).

The following is a full example of making boolean operations to ExPolygons and appending them to SVG. In this case we create four inline SVG documents and draw one boolean result on each one. At the right side of each SVG document is the corresponding JSON string of exPolygons in question. You can easily modify subj_polygons and clip_polygons to see how the changes affect to the solutions.

Side note: Function scaleDownExPolygon() is needed only to scale down ExPolygon coordinates, but in real world it is not needed, because downscaling is done in ExPolygons2path() function.

:::javascript

<html>
  <head>
    <title>Javascript Clipper Library / Boolean operations, ExPolygons / SVG example</title>
    <script src="clipper.js"></script>
    <script>
function draw() {
  var subj_polygons = [[{X:10,Y:10},{X:110,Y:10},{X:110,Y:110},{X:10,Y:110}],
                      [{X:20,Y:20},{X:20,Y:100},{X:100,Y:100},{X:100,Y:20}]]; 
  var clip_polygons = [[{X:50,Y:50},{X:150,Y:50},{X:150,Y:150},{X:50,Y:150}],
                      [{X:60,Y:60},{X:60,Y:140},{X:140,Y:140},{X:140,Y:60}]];
  var scale = 100;
  subj_polygons = scaleup(subj_polygons, scale);
  clip_polygons = scaleup(clip_polygons, scale);
  var cpr = new ClipperLib.Clipper();
  cpr.AddPolygons(subj_polygons, ClipperLib.PolyType.ptSubject);
  cpr.AddPolygons(clip_polygons, ClipperLib.PolyType.ptClip);
  var subject_fillType = ClipperLib.PolyFillType.pftNonZero;
  var clip_fillType = ClipperLib.PolyFillType.pftNonZero;
  var clipTypes = [ClipperLib.ClipType.ctUnion, ClipperLib.ClipType.ctDifference, ClipperLib.ClipType.ctXor, ClipperLib.ClipType.ctIntersection];
  var clipTypesTexts = ["Union", "Difference", "Xor", "Intersection"];
  var solution_expolygons, svg, cont = document.getElementById('svgcontainer');
  var i;

  svg = '<table><tr>';

  for(i = 0; i < clipTypes.length; i++) {
    solution_expolygons = new ClipperLib.ExPolygons();
    cpr.Execute(clipTypes[i], solution_expolygons, subject_fillType, clip_fillType);
    //console.log(JSON.stringify(solution_expolygons));
    //console.log(solution_expolygons);
    svg += '<tr><td>';
    svg += '<svg style="margin-top:10px; margin-right:10px;margin-bottom:10px;background-color:#dddddd" width="160" height="160">';
    svg += '<path stroke="black" fill="yellow" stroke-width="2" d="' + ExPolygons2path(solution_expolygons, scale) + '"/>';
    svg += '</svg></td><td>';
    svg += '<b>' + clipTypesTexts[i] + ':</b><br>' + JSON.stringify(scaleDownExPolygon(solution_expolygons, scale));
    svg += '</td></tr>';
  }
  svg += '</table>';
  cont.innerHTML += svg;
}

// helper function to scale up polygon coordinates
function scaleup(poly, scale) {
  var i, j;
  if (!scale) scale = 1;
  for(i = 0; i < poly.length; i++) {
    for(j = 0; j < poly[i].length; j++) {
      poly[i][j].X *= scale;
      poly[i][j].Y *= scale;
    }
  }
  return poly;
}

// helper function to scale up polygon coordinates
function scaledown(poly, scale) {
  var i, j;
  if (!scale) scale = 1;
  for(i = 0; i < poly.length; i++) {
    for(j = 0; j < poly[i].length; j++) {
      poly[i][j].X = Number(poly[i][j].X) / scale;
      poly[i][j].Y = Number(poly[i][j].Y) / scale;
    }
  }
  return poly;
}

function scaleDownExPolygon(exPolygons, scale)
{
  var a, i, j, exPolygon, holes, outer, polygon;
  if (!scale) scale = 1;
  for (a = 0, alen = exPolygons.length; a < alen; a++)
  {
    exPolygon = exPolygons[a];
    holes = exPolygon.holes;
    outer = exPolygon.outer;
    for (i = 0, ilen = holes.length; i < ilen; i++)
    {
      polygon = holes[i];

      for (j = 0, jlen = polygon.length; j < jlen; j++)
      {
        point = polygon[j];
        point.X = Number(point.X) / scale;
        point.Y = Number(point.Y) / scale;
      }
    }
    for (j = 0, jlen = outer.length; j < jlen; j++)
    {
      point = outer[j];
      point.X = Number(point.X) / scale;
      point.Y = Number(point.Y) / scale;
    }
  }
  return exPolygons;
}

// converts polygon to SVG path string
function Polygon2path (poly, scale) {
  var path = "", i, j;
  if (!scale) scale = 1;
  for(i = 0; i < poly.length; i++) {
      if (!i) path += "M";
      else path += "L";
      path += (poly[i].X / scale) + ", " + (poly[i].Y / scale);
    }
    path += "Z";

  return path;
}

// converts ExPolygons to SVG path string
function ExPolygons2path (ExPolygons, scale) {
  var path = "", i, a;
  if (!scale) scale = 1;
  for(a = 0; a < ExPolygons.length; a++)
  {
    path += Polygon2path (ExPolygons[a].outer, scale);
    for(i = 0; i < ExPolygons[a].holes.length; i++)
    {
      path += Polygon2path (ExPolygons[a].holes[i], scale);
    }
  }
  return path;
}
  </script>
  </head>
  <body onload="draw()">
  <h2>Javascript Clipper Library / Boolean operations, ExPolygons / SVG example</h2>
    This page shows an example of boolean operations on ExPolygons and drawing them using SVG.
    <div id="svgcontainer"></div>
  </body>
</html>

The above code generates the following page (subject and clip polygons and colors are added to make the image more understandable):