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.
One triangle against a plane
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)})";}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)
From segments to loops
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)})";}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)
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.
Show a hint
Show the solution
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)})";}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.