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

Lesson 38 of 44 15 minAdvanced

Sections: cutting a mesh with a plane

Cut a mesh with a plane the way plan and section views do: signed distances, one segment per crossing triangle, joining the segments into closed loops, and measuring the cut area.

A floor plan is a horizontal cut through the building, usually about 1.2 m above the floor, looking down. A section is a vertical cut. Both need the outline where each element meets the cutting plane, so it can be drawn with a thick line and filled with a hatch. The same cut gives the floor area of a room at a level, or the cross-section of a beam for a structural check.

Interactive

Cutting a section through a mesh

segments12
loops1
section area12.000 m²
12 triangles cross the plane, and each gives one segment. Joined end to start they make one closed loop with an area of 12.00 m².

One triangle against a plane

Write the plane as n · p = d with a unit normal n, as in the planes lesson. For each corner of a triangle work out the signed distance s = n · p − d: positive above the plane, negative below. If all three have the same sign, the triangle misses the plane. Otherwise exactly two edges cross it, and on an edge from P (distance s_P) to Q (distance s_Q) the crossing is at

X = P + (Q − P) · s_P / (s_P − s_Q)

the same interpolation as the line–plane intersection, just with the distances already known. The two crossing points make one segment of the section.

TriangleCut.cs
var n = new V3(0, 0, 1);double d = 1.2;                                           // the plane z = 1.2 var tris = new (string Name, V3 A, V3 B, V3 C)[]{    ("crosses", new(0, 0, 0), new(4, 0, 0), new(4, 0, 3)),    ("below", new(0, 0, 0), new(4, 0, 0), new(4, 0, 1)),    ("corner on the plane", new(0, 0, 1.2), new(4, 0, 0), new(4, 0, 3))};foreach (var (name, a, b, c) in tris){    var pts = Cut(a, b, c, n, d);    Console.WriteLine($"{name}: {(pts.Count == 2 ? $"{pts[0]} to {pts[1]}" : "no segment")}");} static List<V3> Cut(V3 a, V3 b, V3 c, V3 n, double d){    var p = new[] { a, b, c };    var s = p.Select(q => V3.Dot(n, q) - d).ToArray();    // signed distances    var pts = new List<V3>();    for (int k = 0; k < 3; k++)    {        int j = (k + 1) % 3;        if ((s[k] >= 0) != (s[j] >= 0))                    // a corner on the plane counts as above            pts.Add(p[k] + (p[j] - p[k]) * (s[k] / (s[k] - s[j])));    }    return pts;} readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
crosses: (4, 0, 1.2) to (1.6, 0, 1.2)
below: no segment
corner on the plane: (0, 0, 1.2) to (4, 0, 1.2)

The rule “on the plane counts as above” matters. Without it, a corner exactly on the plane (very common: floors, tops of walls and sills sit at round heights) could produce one crossing, or three, or the same point twice. With it, every edge either crosses or does not, and each triangle gives exactly zero or two points. A triangle lying in the plane gives nothing, which is right: its neighbours below the plane give its edges instead.

From segments to loops

Each crossing triangle gives one segment, in no particular order. To draw a filled outline you need closed loops, so the segments have to be joined end to start. If each segment is pointed along n × (face normal), every loop runs counter-clockwise seen from the side the normal points to, and the next segment always starts where the previous one ended. A dictionary from start point to segment then chains them in one pass.

SectionLoops.cs
var room = Mesh.Prism(new[] { (0.0, 0.0), (4.0, 0.0), (4.0, 2.0), (2.0, 2.0), (2.0, 4.0), (0.0, 4.0) }, 3);var up = new V3(0, 0, 1); foreach (var (name, m, h) in new[] { ("L-shaped room", room, 1.2), ("pyramid", Mesh.Pyramid(4, 3), 1.5) }){    var segs = Section(m, up, h);    var loops = Chain(segs);    double area = segs.Sum(s => V3.Dot(up, V3.Cross(s.P, s.Q)) / 2);   // shoelace on the segments    Console.WriteLine($"{name} at z = {h}: {segs.Count} segments, {loops.Count} loop, area {area:0.###} m2");    foreach (var loop in loops)        Console.WriteLine("  " + string.Join(" ", loop.Select(p => $"({p.X:0.##}, {p.Y:0.##})")));} static List<(V3 P, V3 Q)> Section(Mesh m, V3 n, double d){    var segs = new List<(V3 P, V3 Q)>();    foreach (var t in m.F)    {        var p = new[] { m.V[t.A], m.V[t.B], m.V[t.C] };        var s = p.Select(q => V3.Dot(n, q) - d).ToArray();        var pts = new List<V3>();        for (int k = 0; k < 3; k++)        {            int j = (k + 1) % 3;            if ((s[k] >= 0) != (s[j] >= 0)) pts.Add(p[k] + (p[j] - p[k]) * (s[k] / (s[k] - s[j])));        }        if (pts.Count != 2 || (pts[1] - pts[0]).Length < 1e-9) continue;        var dir = V3.Cross(n, m.Normal(t));                     // the way the loop runs        segs.Add(V3.Dot(pts[1] - pts[0], dir) >= 0 ? (pts[0], pts[1]) : (pts[1], pts[0]));    }    return segs;} static List<List<V3>> Chain(List<(V3 P, V3 Q)> segs){    static string Key(V3 p) => $"{p.X:0.000000},{p.Y:0.000000},{p.Z:0.000000}";    var from = new Dictionary<string, int>();    for (int i = 0; i < segs.Count; i++) from[Key(segs[i].P)] = i;     var used = new bool[segs.Count];    var loops = new List<List<V3>>();    for (int i = 0; i < segs.Count; i++)    {        if (used[i]) continue;        var loop = new List<V3>();        for (int j = i; !used[j]; j = from[Key(segs[j].Q)])        {            used[j] = true;            loop.Add(segs[j].P);        }        loops.Add(loop);    }    return loops;} sealed class Mesh{    public readonly List<V3> V = new();    public readonly List<(int A, int B, int C)> F = new();   // counter-clockwise seen from outside     public static Mesh Box(double lx, double ly, double lz) =>        Prism(new[] { (0.0, 0.0), (lx, 0.0), (lx, ly), (0.0, ly) }, lz);     // A counter-clockwise footprint extruded up by h (floor and roof fanned from corner 0)    public static Mesh Prism((double X, double Y)[] poly, double h)    {        var m = new Mesh();        int n = poly.Length;        foreach (var (x, y) in poly) m.V.Add(new V3(x, y, 0));        foreach (var (x, y) in poly) m.V.Add(new V3(x, y, h));        for (int k = 1; k < n - 1; k++)        {            m.F.Add((0, k + 1, k));                 // floor, facing down            m.F.Add((n, n + k, n + k + 1));         // roof, facing up        }        for (int i = 0; i < n; i++)        {            int j = (i + 1) % n;            m.F.Add((i, j, j + n));                 // each wall as two triangles            m.F.Add((i, j + n, i + n));        }        return m;    }     public static Mesh Pyramid(double b, double h)    {        var m = new Mesh();        m.V.AddRange(new V3[] { new(0, 0, 0), new(b, 0, 0), new(b, b, 0), new(0, b, 0), new(b / 2, b / 2, h) });        m.F.AddRange(new[] { (0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4), (0, 2, 1), (0, 3, 2) });        return m;    }     // (b - a) x (c - a): points out of the solid, and its length is twice the triangle's area    public V3 Normal((int A, int B, int C) t) => V3.Cross(V[t.B] - V[t.A], V[t.C] - V[t.A]);} readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
L-shaped room at z = 1.2: 12 segments, 1 loop, area 12 m2
  (1.6, 0) (4, 0) (4, 0.8) (4, 2) (3.2, 2) (2, 2) (2, 2.8) (2, 4) (1.2, 4) (0, 4) (0, 2.4) (0, 0)
pyramid at z = 1.5: 4 segments, 1 loop, area 4 m2
  (1, 1) (3, 1) (3, 3) (1, 3)

The room’s loop has 12 points rather than 6: each wall is two triangles, so the cut crosses each wall’s diagonal too and adds a point in the middle of every edge. Those points lie on straight lines and can be merged afterwards. The area needs no loops at all: oriented segments are enough for the shoelace sum, which here gives the floor area of 12 m². The pyramid cut halfway up gives a 2 × 2 square, a quarter of its 16 m² base.

Loops, holes and tilted planes

  • Several loops: a cut through a U-shaped wall, or through two separate columns, gives several loops. They come out of the same chaining step.
  • Holes: a section through a lift shaft or a hollow-core slab gives an outer loop counter-clockwise and inner loops clockwise. Their signed areas add up to the solid area, just like polygons with holes in the boolean operations lesson.
  • Tilted planes: nothing above assumed a horizontal plane. To draw or measure the loops, express each point in two axes u and v lying in the plane (with u × v = n), which is what the widget does when you tilt it.
  • Open meshes: a gap in the mesh leaves a loop unclosed. Chaining then stops at a segment with no follower; a robust implementation reports it instead of crashing.
Quick check

A triangle’s corners have signed distances 0.4, −0.2 and −0.7 from the cutting plane. How many of its edges cross the plane?

Quick check

Why is each section segment oriented along n × (face normal)?

Practice

Cut through the walls of a lift shaft at z = 1: outer walls 3 × 3 m from (0, 0) and inner walls 2.4 × 2.4 m from (0.3, 0.3), both 4 m high, with the inner walls facing into the shaft. Build them as Mesh.Prism for the outer square plus Mesh.Prism for the inner square with every triangle reversed (the floor and roof triangles do not reach z = 1, so only the walls matter here). Print the number of loops, the signed area of each loop and the total.

Show a hint

Reuse Section and Chain from SectionLoops.cs. Reverse a triangle (a, b, c) as (a, c, b). The signed area of one loop is Σ ½ n · (p × q) over its consecutive points.

Show the solution
Solution.cs
var outer = Mesh.Box(3, 3, 4);var inner = Mesh.Prism(new[] { (0.3, 0.3), (2.7, 0.3), (2.7, 2.7), (0.3, 2.7) }, 4);var shaft = new Mesh();shaft.V.AddRange(outer.V);shaft.V.AddRange(inner.V);shaft.F.AddRange(outer.F);int off = outer.V.Count;shaft.F.AddRange(inner.F.Select(t => (t.A + off, t.C + off, t.B + off)));   // reversed: facing into the shaft var up = new V3(0, 0, 1);var loops = Chain(Section(shaft, up, 1));double total = 0;foreach (var loop in loops){    double a = 0;    for (int i = 0; i < loop.Count; i++) a += V3.Dot(up, V3.Cross(loop[i], loop[(i + 1) % loop.Count])) / 2;    total += a;    Console.WriteLine($"loop of {loop.Count} points: {a:0.###} m2");}Console.WriteLine($"{loops.Count} loops, wall area in plan {total:0.###} m2"); static List<(V3 P, V3 Q)> Section(Mesh m, V3 n, double d){    var segs = new List<(V3 P, V3 Q)>();    foreach (var t in m.F)    {        var p = new[] { m.V[t.A], m.V[t.B], m.V[t.C] };        var s = p.Select(q => V3.Dot(n, q) - d).ToArray();        var pts = new List<V3>();        for (int k = 0; k < 3; k++)        {            int j = (k + 1) % 3;            if ((s[k] >= 0) != (s[j] >= 0)) pts.Add(p[k] + (p[j] - p[k]) * (s[k] / (s[k] - s[j])));        }        if (pts.Count != 2 || (pts[1] - pts[0]).Length < 1e-9) continue;        var dir = V3.Cross(n, m.Normal(t));        segs.Add(V3.Dot(pts[1] - pts[0], dir) >= 0 ? (pts[0], pts[1]) : (pts[1], pts[0]));    }    return segs;} static List<List<V3>> Chain(List<(V3 P, V3 Q)> segs){    static string Key(V3 p) => $"{p.X:0.000000},{p.Y:0.000000},{p.Z:0.000000}";    var from = new Dictionary<string, int>();    for (int i = 0; i < segs.Count; i++) from[Key(segs[i].P)] = i;    var used = new bool[segs.Count];    var loops = new List<List<V3>>();    for (int i = 0; i < segs.Count; i++)    {        if (used[i]) continue;        var loop = new List<V3>();        for (int j = i; !used[j]; j = from[Key(segs[j].Q)])        {            used[j] = true;            loop.Add(segs[j].P);        }        loops.Add(loop);    }    return loops;} sealed class Mesh{    public readonly List<V3> V = new();    public readonly List<(int A, int B, int C)> F = new();   // counter-clockwise seen from outside     public static Mesh Box(double lx, double ly, double lz) =>        Prism(new[] { (0.0, 0.0), (lx, 0.0), (lx, ly), (0.0, ly) }, lz);     // A counter-clockwise footprint extruded up by h (floor and roof fanned from corner 0)    public static Mesh Prism((double X, double Y)[] poly, double h)    {        var m = new Mesh();        int n = poly.Length;        foreach (var (x, y) in poly) m.V.Add(new V3(x, y, 0));        foreach (var (x, y) in poly) m.V.Add(new V3(x, y, h));        for (int k = 1; k < n - 1; k++)        {            m.F.Add((0, k + 1, k));                 // floor, facing down            m.F.Add((n, n + k, n + k + 1));         // roof, facing up        }        for (int i = 0; i < n; i++)        {            int j = (i + 1) % n;            m.F.Add((i, j, j + n));                 // each wall as two triangles            m.F.Add((i, j + n, i + n));        }        return m;    }     public static Mesh Pyramid(double b, double h)    {        var m = new Mesh();        m.V.AddRange(new V3[] { new(0, 0, 0), new(b, 0, 0), new(b, b, 0), new(0, b, 0), new(b / 2, b / 2, h) });        m.F.AddRange(new[] { (0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4), (0, 2, 1), (0, 3, 2) });        return m;    }     // (b - a) x (c - a): points out of the solid, and its length is twice the triangle's area    public V3 Normal((int A, int B, int C) t) => V3.Cross(V[t.B] - V[t.A], V[t.C] - V[t.A]);} readonly record struct V3(double X, double Y, double Z){    public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z);    public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z);    public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k);    public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z;    public static V3 Cross(V3 a, V3 b) =>        new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X);    public double Length => Math.Sqrt(Dot(this, this));    public V3 Normalized() => this * (1 / Length);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}
Output
loop of 8 points: 9 m2
loop of 8 points: -5.76 m2
2 loops, wall area in plan 3.24 m2

Remember

  • Signed distances s = n · p − d tell which corners are above and below the plane.
  • A crossing edge from P to Q meets the plane at P + (Q − P) · s_P / (s_P − s_Q); each crossing triangle gives one segment.
  • Count a corner on the plane as above, so every triangle gives exactly zero or two points.
  • Orient segments along n × (face normal) and chain them end to start into counter-clockwise loops.
  • The cut area is the shoelace sum over the oriented segments; holes come out clockwise with negative area.
0 of 44 lessons completed