<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. BIM Math
  3. Planes and polygons
All lessons in this course

Lesson 23 of 44 15 minIntermediate

Polygon offsets and boolean operations

Offset a closed boundary in or out, clip one polygon against another, and combine them with union, intersection and difference to get net areas, slab edges and overlaps.

A slab edge is the outer wall line offset outwards. A finish boundary is a room offset inwards by the skirting. A net floor area is the room minus columns and shafts. A fire compartment check is an intersection. These operations on whole polygons are the bread and butter of quantity takeoff and model checking.

Interactive

Polygon booleans and offsets

Drag the room A’s corners, and the rectangle B by its centre; the slider turns B.

BA
area A51.50
area B8.75
area A ∩ B (clipped)5.27
area A − B46.23
Net area = A − (A ∩ B) = 51.50 − 5.27 = 46.23. Only the part of B inside the room is deducted: this is how net floor area handles a column half buried in a wall.

Offsetting a closed polygon

This is the polyline offset from the last module, with two changes. Every corner now has two neighbours, so the indices wrap round. And “outwards” depends on the winding: for a counterclockwise polygon, the right-hand normal of each edge points out. The miter formula V + (n1 + n2)·δ ÷ (1 + n1·n2) is unchanged.

SlabEdge.cs
var room = new V2[] { new(0, 0), new(8, 0), new(8, 3), new(3, 3), new(3, 6), new(0, 6) };var slabEdge = OffsetOutwards(room, 0.5); Console.WriteLine(string.Join(" ", slabEdge));Console.WriteLine($"Area {Area(room)} m2 -> {Area(slabEdge)} m2"); static V2[] OffsetOutwards(V2[] p, double delta)    // p counterclockwise{    int n = p.Length;    var normals = new V2[n];                          // outward normal of edge i    for (int i = 0; i < n; i++)    {        V2 d = p[(i + 1) % n] - p[i];        normals[i] = new V2(d.Y, -d.X) * (1 / d.Length);   // right of a counterclockwise edge is outside    }    var result = new V2[n];    for (int i = 0; i < n; i++)    {        V2 n1 = normals[(i + n - 1) % n], n2 = normals[i];        result[i] = p[i] + (n1 + n2) * (delta / (1 + V2.Dot(n1, n2)));    }    return result;} static double Area(V2[] p){    double s = 0;    for (int i = 0; i < p.Length; i++) s += V2.Cross(p[i], p[(i + 1) % p.Length]);    return s / 2;} readonly record struct V2(double X, double Y){    public static V2 operator +(V2 a, V2 b) => new(a.X + b.X, a.Y + b.Y);    public static V2 operator -(V2 a, V2 b) => new(a.X - b.X, a.Y - b.Y);    public static V2 operator *(V2 v, double k) => new(v.X * k, v.Y * k);    public static double Dot(V2 a, V2 b) => a.X * b.X + a.Y * b.Y;    public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X;    public double Length => Math.Sqrt(X * X + Y * Y);    public override string ToString() => $"({X:0.###}, {Y:0.###})";}
Output
(-0.5, -0.5) (8.5, -0.5) (8.5, 3.5) (3.5, 3.5) (3.5, 6.5) (-0.5, 6.5)
Area 33 m2 -> 48 m2

The area grew from 33 to 48 m². Most of that is the perimeter times the offset (28 × 0.5 = 14 m²). The rest comes from the corners: each convex corner adds a δ × δ square and each reflex corner takes one away, so 5 − 1 = 4 corners × 0.25 = 1 m². The widget’s table shows the same split for any shape.

Clipping: Sutherland–Hodgman

To find the part of a shape inside a convex polygon, clip it against each edge of the convex polygon in turn. For each edge, walk round the shape: keep the corners on the inside, and wherever an edge of the shape crosses the clipping line, add the crossing point (a line intersection, from the last module). After the last edge, what is left is the intersection. That is all you need for union and difference areas too:

  • Area of A ∩ B: clip A by B (B must be convex for this algorithm).
  • Area of A − B = area of A − area of (A ∩ B).
  • Area of A ∪ B = area of A + area of B − area of (A ∩ B).
NetArea.cs
var room = Rect(0, 0, 10, 8);var deductions = new (string Name, V2[] Shape)[]{    ("Column C1", Rect(4.8, 3.8, 5.2, 4.2)),     // free-standing    ("Column C2", Rect(-0.2, -0.2, 0.2, 0.2)),   // in the corner: only a quarter is in the room    ("Shaft S1", Rect(7, 5, 9, 7)),}; double net = Area(room);Console.WriteLine($"Gross {net} m2");foreach (var (name, shape) in deductions){    double inside = Area(Clip(shape, room));    net -= inside;    Console.WriteLine($"{name}: -{inside:0.###} m2");}Console.WriteLine($"Net {net:0.###} m2"); static V2[] Rect(double x0, double y0, double x1, double y1) =>    new V2[] { new(x0, y0), new(x1, y0), new(x1, y1), new(x0, y1) }; static V2[] Clip(V2[] subject, V2[] clipper)      // Sutherland-Hodgman; clipper convex and counterclockwise{    var output = new List<V2>(subject);    for (int i = 0; i < clipper.Length && output.Count > 0; i++)    {        V2 a = clipper[i], e = clipper[(i + 1) % clipper.Length] - a;        var input = output;        output = new List<V2>();        for (int j = 0; j < input.Count; j++)        {            V2 p = input[j], q = input[(j + 1) % input.Count];            double sp = V2.Cross(e, p - a), sq = V2.Cross(e, q - a);   // >= 0: inside            if (sp >= 0) output.Add(p);            if ((sp >= 0) != (sq >= 0)) output.Add(p + (q - p) * (sp / (sp - sq)));        }    }    return output.ToArray();} static double Area(V2[] p){    if (p.Length < 3) return 0;    double s = 0;    for (int i = 0; i < p.Length; i++) s += V2.Cross(p[i], p[(i + 1) % p.Length]);    return s / 2;} readonly record struct V2(double X, double Y){    public static V2 operator +(V2 a, V2 b) => new(a.X + b.X, a.Y + b.Y);    public static V2 operator -(V2 a, V2 b) => new(a.X - b.X, a.Y - b.Y);    public static V2 operator *(V2 v, double k) => new(v.X * k, v.Y * k);    public static double Dot(V2 a, V2 b) => a.X * b.X + a.Y * b.Y;    public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X;    public double Length => Math.Sqrt(X * X + Y * Y);    public override string ToString() => $"({X:0.###}, {Y:0.###})";}
Output
Gross 80 m2
Column C1: -0.16 m2
Column C2: -0.04 m2
Shaft S1: -4 m2
Net 75.8 m2

Clipping each deduction against the room means only the part inside the room is deducted. Without it, column C2 would remove 0.16 m² instead of 0.04 m², a small error that adds up over a few hundred columns on a large floor plate.

General booleans: use a library

Sutherland–Hodgman needs a convex clipper, and it only gives one result polygon. General booleans of concave polygons, with holes and with results that split into several pieces, need algorithms such as Weiler–Atherton, Greiner–Hormann or Vatti. They are notoriously hard to get right with floating point. In practice, use a tested library: Clipper2 (C#, C++ and Delphi) is the usual choice for 2D and also does robust offsetting. It works with integer coordinates internally, so scale model units to millimetres or set its precision.

Quick check

Room A has 50 m², shape B has 12 m², and they overlap by 5 m². What is the area of A ∪ B?

Quick check

Why must the clipping polygon be convex in Sutherland–Hodgman?

Practice

Rectangle A runs from (0, 0) to (6, 4) and rectangle B from (4, 2) to (9, 7). Using Clip, Area and Rect from NetArea.cs, print the area of their overlap, their union and A − B.

Show a hint

Clip A by B for the overlap. Then union = A + B − overlap and A − B = A − overlap.

Show the solution
Solution.cs
var a = Rect(0, 0, 6, 4);var b = Rect(4, 2, 9, 7); double overlap = Area(Clip(a, b));Console.WriteLine($"Overlap: {overlap} m2");Console.WriteLine($"Union: {Area(a) + Area(b) - overlap} m2");Console.WriteLine($"A - B: {Area(a) - overlap} m2"); static V2[] Rect(double x0, double y0, double x1, double y1) =>    new V2[] { new(x0, y0), new(x1, y0), new(x1, y1), new(x0, y1) }; static V2[] Clip(V2[] subject, V2[] clipper){    var output = new List<V2>(subject);    for (int i = 0; i < clipper.Length && output.Count > 0; i++)    {        V2 a = clipper[i], e = clipper[(i + 1) % clipper.Length] - a;        var input = output;        output = new List<V2>();        for (int j = 0; j < input.Count; j++)        {            V2 p = input[j], q = input[(j + 1) % input.Count];            double sp = V2.Cross(e, p - a), sq = V2.Cross(e, q - a);            if (sp >= 0) output.Add(p);            if ((sp >= 0) != (sq >= 0)) output.Add(p + (q - p) * (sp / (sp - sq)));        }    }    return output.ToArray();} static double Area(V2[] p){    if (p.Length < 3) return 0;    double s = 0;    for (int i = 0; i < p.Length; i++) s += V2.Cross(p[i], p[(i + 1) % p.Length]);    return s / 2;} readonly record struct V2(double X, double Y){    public static V2 operator +(V2 a, V2 b) => new(a.X + b.X, a.Y + b.Y);    public static V2 operator -(V2 a, V2 b) => new(a.X - b.X, a.Y - b.Y);    public static V2 operator *(V2 v, double k) => new(v.X * k, v.Y * k);    public static double Dot(V2 a, V2 b) => a.X * b.X + a.Y * b.Y;    public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X;    public double Length => Math.Sqrt(X * X + Y * Y);    public override string ToString() => $"({X:0.###}, {Y:0.###})";}
Output
Overlap: 4 m2
Union: 45 m2
A - B: 20 m2

Remember

  • Offset a closed polygon with wrap-around miters; for counterclockwise winding the right normal points out.
  • An offset changes the area by about perimeter × δ, plus a δ² term from each corner.
  • Sutherland–Hodgman clips any polygon by a convex one, one edge line at a time.
  • A − B = A − (A ∩ B) and A ∪ B = A + B − (A ∩ B), as areas.
  • For general booleans and robust offsets of concave shapes, use a library such as Clipper2.
0 of 44 lessons completed