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

Lesson 44 of 44 18 minMaster

Putting it together: a mini room-and-wall geometry engine

The capstone: wall centrelines and thicknesses in, mitred wall outlines, rooms, areas, volumes, openings and point-in-room queries out, using the maths of the whole course.

This lesson builds the core of what a BIM authoring tool does every time you draw a wall: turn a few centrelines and a thickness into wall outlines, find the rooms they enclose, and report areas, volumes and finishes. Almost every earlier module appears: vectors and the cross product, line intersection, polygon area and centroid, point-in-polygon, extrusion to a mesh and signed-tetrahedron volumes, and the robust-geometry habits of the first lesson in this module.

Interactive

Walls in, rooms and quantities out

Room 124.79 m²Room 226.19 m²
area m²volume m³
Room 124.79074.370
Room 226.19078.570
walls12.21036.630
gross63.190189.570
Wall footprint 12.210 m² = gross 63.190 m² minus both rooms. Check from the centrelines: 34.0 m of outer wall × 0.3 m plus the internal wall’s 6.70 m between faces × 0.3 m = 12.210 m². The mitred corners make the two ways agree exactly.

Step 1: offsetting a centreline loop

Walls are drawn along their centrelines. The faces of the walls are those lines moved sideways by half the thickness. For a closed loop of walls going counter-clockwise, the left of each edge is inside, so moving every edge to its left by d gives the inside faces, and by −d the outside faces. Each corner is where two neighbouring offset lines meet: the line intersection from the lines module. This gives clean mitred corners, at convex and concave corners alike.

Offset.cs
// The outer walls of an L-shaped house, as a counter-clockwise centreline loopvar centre = new List<V2> { new(0, 0), new(10, 0), new(10, 4), new(6, 4), new(6, 7), new(0, 7) };const double T = 0.3; var outside = Geo.Offset(centre, -T / 2);var inside = Geo.Offset(centre, T / 2);Console.WriteLine($"outside face: {string.Join(" ", outside)}");Console.WriteLine($"inside face:  {string.Join(" ", inside)}"); double ring = Geo.Area(outside) - Geo.Area(inside);Console.WriteLine($"wall footprint {ring:0.000} m2; centreline length x thickness = {Geo.Perimeter(centre) * T:0.000} m2"); static class Geo{    // Move each edge d to its left, then meet each pair of neighbouring edges again    public static List<V2> Offset(IList<V2> loop, double d)    {        int n = loop.Count;        var result = new List<V2>();        for (int i = 0; i < n; i++)        {            V2 a0 = loop[(i + n - 1) % n], a1 = loop[i], b1 = loop[(i + 1) % n];            V2 u = a1 - a0, v = b1 - a1;                        // edge into the corner, edge out of it            V2 pa = a0 + Left(u) * d, pb = a1 + Left(v) * d;    // a point on each offset line            double den = Cross(u, v);            if (Math.Abs(den) < 1e-12) { result.Add(pb); continue; }    // straight on: no corner            result.Add(pa + u * (Cross(pb - pa, v) / den));        }        return result;    }     public static V2 Left(V2 v) => new V2(-v.Y, v.X) * (1 / v.Length);    public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X;     public static double Area(IList<V2> p)    {        double a = 0;        for (int i = 0; i < p.Count; i++) a += Cross(p[i], p[(i + 1) % p.Count]);        return a / 2;    }     public static double Perimeter(IList<V2> p) =>        Enumerable.Range(0, p.Count).Sum(i => (p[(i + 1) % p.Count] - p[i]).Length);} 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 double Length => Math.Sqrt(X * X + Y * Y);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)})";}
Output
outside face: (-0.15, -0.15) (10.15, -0.15) (10.15, 4.15) (6.15, 4.15) (6.15, 7.15) (-0.15, 7.15)
inside face:  (0.15, 0.15) (9.85, 0.15) (9.85, 3.85) (5.85, 3.85) (5.85, 6.85) (0.15, 6.85)
wall footprint 10.200 m2; centreline length x thickness = 10.200 m2

The wall footprint, measured as outside area minus inside area, equals the centreline length times the thickness exactly. That is a property of mitred offsets: the extra area at the convex corners is cancelled by the area missing at the concave corner. Checks like this are cheap and catch bugs early. A wrong-way loop or a missed corner breaks the equality.

Step 2: rooms, areas and the schedule

A room is bounded by wall centrelines too. Here an internal wall at x = 4 splits the house into two rooms; each room’s centreline loop, pushed inwards by half the wall thickness, is the room’s finished outline. Its area is the shoelace formula, its perimeter is the length of skirting, and its centroid is where the room tag goes.

Rooms.cs
const double T = 0.3, Height = 3.0;var rooms = new (string Name, List<V2> Centreline)[]{    ("Living", new() { new(0, 0), new(4, 0), new(4, 7), new(0, 7) }),    ("Kitchen", new() { new(4, 0), new(10, 0), new(10, 4), new(6, 4), new(6, 7), new(4, 7) }),}; Console.WriteLine($"{"room",-8} {"area",8} {"perimeter",10} {"volume",8}  tag at");double total = 0;foreach (var (name, cl) in rooms){    var outline = Geo.Offset(cl, T / 2);    double area = Geo.Area(outline);    total += area;    Console.WriteLine($"{name,-8} {area,8:0.000} {Geo.Perimeter(outline),10:0.000} {area * Height,8:0.000}  {Geo.Centroid(outline)}");} var gross = Geo.Area(Geo.Offset(new List<V2> { new(0, 0), new(10, 0), new(10, 4), new(6, 4), new(6, 7), new(0, 7) }, -T / 2));Console.WriteLine($"rooms {total:0.000} m2 + walls {gross - total:0.000} m2 = gross {gross:0.000} m2"); static class Geo{    public static List<V2> Offset(IList<V2> loop, double d)    {        int n = loop.Count;        var result = new List<V2>();        for (int i = 0; i < n; i++)        {            V2 a0 = loop[(i + n - 1) % n], a1 = loop[i], b1 = loop[(i + 1) % n];            V2 u = a1 - a0, v = b1 - a1;            V2 pa = a0 + Left(u) * d, pb = a1 + Left(v) * d;            double den = Cross(u, v);            if (Math.Abs(den) < 1e-12) { result.Add(pb); continue; }            result.Add(pa + u * (Cross(pb - pa, v) / den));        }        return result;    }     public static V2 Left(V2 v) => new V2(-v.Y, v.X) * (1 / v.Length);    public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X;     public static double Area(IList<V2> p)    {        double a = 0;        for (int i = 0; i < p.Count; i++) a += Cross(p[i], p[(i + 1) % p.Count]);        return a / 2;    }     public static double Perimeter(IList<V2> p) =>        Enumerable.Range(0, p.Count).Sum(i => (p[(i + 1) % p.Count] - p[i]).Length);     // Area-weighted centre of a polygon (the polygons module)    public static V2 Centroid(IList<V2> p)    {        double a = Area(p), cx = 0, cy = 0;        for (int i = 0; i < p.Count; i++)        {            V2 s = p[i], t = p[(i + 1) % p.Count];            double k = Cross(s, t);            cx += (s.X + t.X) * k;            cy += (s.Y + t.Y) * k;        }        return new V2(cx / (6 * a), cy / (6 * a));    }} 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 double Length => Math.Sqrt(X * X + Y * Y);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)})";}
Output
room         area  perimeter   volume  tag at
Living     24.790     20.800   74.370  (2, 3.5)
Kitchen    26.190     24.800   78.570  (6.611, 2.652)
rooms 50.980 m2 + walls 12.210 m2 = gross 63.190 m2

The schedule balances: room areas plus wall footprint equals the gross area to the last digit. The kitchen’s L-shape needs nothing special. The same offset handles its concave corner, and the centroid formula puts its tag inside the room’s centre of area.

Step 3: extruding to a solid

Clash detection, sections and quantity take-off work on solids. Extrude each room outline to the storey height with the prism builder from the meshes lesson, check that every edge is paired, and measure its volume with signed tetrahedra. The mesh volume must match area × height; if it does not, the outline was clockwise or self-intersecting.

Extrude.cs
var kitchen = new (double X, double Y)[] { (4.15, 0.15), (9.85, 0.15), (9.85, 3.85), (5.85, 3.85), (5.85, 6.85), (4.15, 6.85) };var mesh = Mesh.Prism(kitchen, 3.0); var edges = new Dictionary<(int, int), int>();foreach (var t in mesh.F)    foreach (var e in new[] { (t.A, t.B), (t.B, t.C), (t.C, t.A) })        edges[e] = edges.GetValueOrDefault(e) + 1;bool closed = edges.Keys.All(e => edges.GetValueOrDefault((e.Item2, e.Item1)) == 1); double volume = mesh.F.Sum(t => V3.Dot(mesh.V[t.A], V3.Cross(mesh.V[t.B], mesh.V[t.C]))) / 6;double area = 0;for (int i = 0; i < kitchen.Length; i++){    var (x0, y0) = kitchen[i]; var (x1, y1) = kitchen[(i + 1) % kitchen.Length];    area += x0 * y1 - x1 * y0;}area /= 2; Console.WriteLine($"{mesh.V.Count} vertices, {mesh.F.Count} triangles, every edge paired: {closed}");Console.WriteLine($"mesh volume {volume:0.000} m3, area x height {area * 3:0.000} m3"); 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
12 vertices, 20 triangles, every edge paired: True
mesh volume 78.570 m3, area x height 78.570 m3

Step 4: the engine

The final program puts the pieces behind a small model: walls with thicknesses and openings, rooms found from centreline loops, and queries. It answers the questions a BIM tool answers all day: what are the room areas and volumes, how much wall finish does each room need after doors and windows are taken off, and which room is a given piece of furniture in.

Engine.cs
var model = new Model(thickness: 0.3, height: 3.0);model.AddRoom("Living", new(0, 0), new(4, 0), new(4, 7), new(0, 7));model.AddRoom("Kitchen", new(4, 0), new(10, 0), new(10, 4), new(6, 4), new(6, 7), new(4, 7));model.AddOpening("door, internal", width: 0.9, height: 2.1, "Living", "Kitchen");model.AddOpening("front door", width: 1.0, height: 2.1, "Living");model.AddOpening("window", width: 1.5, height: 1.2, "Kitchen");model.AddOpening("window", width: 1.2, height: 1.2, "Living"); Console.WriteLine($"{"room",-8} {"floor m2",9} {"volume m3",10} {"wall finish m2",15}");foreach (var r in model.Rooms)    Console.WriteLine($"{r.Name,-8} {r.Area,9:0.000} {r.Area * model.Height,10:0.000} {model.WallFinish(r),15:0.000}"); foreach (var (name, p) in new (string, V2)[] { ("sofa", new(2, 3)), ("fridge", new(8.5, 2)), ("table", new(5, 5.5)), ("bin", new(8, 6)) })    Console.WriteLine($"{name,-6} at {p} is in: {model.RoomAt(p)?.Name ?? "no room"}"); sealed record Room(string Name, List<V2> Outline, double Area, double Perimeter); sealed class Model{    readonly List<(double Area, string[] Rooms)> openings = new();    public List<Room> Rooms { get; } = new();    public double Thickness { get; }    public double Height { get; }    public Model(double thickness, double height) { Thickness = thickness; Height = height; }     public void AddRoom(string name, params V2[] centreline)    {        var outline = Geo.Offset(centreline, Thickness / 2);        Rooms.Add(new Room(name, outline, Geo.Area(outline), Geo.Perimeter(outline)));    }     // An opening takes area off the wall finish of every room it opens into    public void AddOpening(string name, double width, double height, params string[] rooms) =>        openings.Add((width * height, rooms));     public double WallFinish(Room r) =>        r.Perimeter * Height - openings.Where(o => o.Rooms.Contains(r.Name)).Sum(o => o.Area);     // Crossing test: count edges that a ray going right from p crosses    public Room? RoomAt(V2 p) => Rooms.FirstOrDefault(r =>    {        bool inside = false;        var o = r.Outline;        for (int i = 0, j = o.Count - 1; i < o.Count; j = i++)            if ((o[i].Y > p.Y) != (o[j].Y > p.Y) &&                p.X < o[j].X + (p.Y - o[j].Y) * (o[i].X - o[j].X) / (o[i].Y - o[j].Y))                inside = !inside;        return inside;    });} static class Geo{    public static List<V2> Offset(IList<V2> loop, double d)    {        int n = loop.Count;        var result = new List<V2>();        for (int i = 0; i < n; i++)        {            V2 a0 = loop[(i + n - 1) % n], a1 = loop[i], b1 = loop[(i + 1) % n];            V2 u = a1 - a0, v = b1 - a1;            V2 pa = a0 + Left(u) * d, pb = a1 + Left(v) * d;            double den = Cross(u, v);            if (Math.Abs(den) < 1e-12) { result.Add(pb); continue; }            result.Add(pa + u * (Cross(pb - pa, v) / den));        }        return result;    }     public static V2 Left(V2 v) => new V2(-v.Y, v.X) * (1 / v.Length);    public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X;     public static double Area(IList<V2> p)    {        double a = 0;        for (int i = 0; i < p.Count; i++) a += Cross(p[i], p[(i + 1) % p.Count]);        return a / 2;    }     public static double Perimeter(IList<V2> p) =>        Enumerable.Range(0, p.Count).Sum(i => (p[(i + 1) % p.Count] - p[i]).Length);} 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 double Length => Math.Sqrt(X * X + Y * Y);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)})";}
Output
room      floor m2  volume m3  wall finish m2
Living      24.790     74.370          56.970
Kitchen     26.190     78.570          70.710
sofa   at (2, 3) is in: Living
fridge at (8.5, 2) is in: Kitchen
table  at (5, 5.5) is in: Kitchen
bin    at (8, 6) is in: no room

The table at (5, 5.5) sits in the kitchen’s upper arm and the bin at (8, 6) is outside the L, in no room at all, which a real tool would flag. The internal door comes off both rooms’ wall finish, because both sides get painted.

Where a real engine goes next

  • Room detection from walls. Here each room’s loop is given. Real tools find the loops themselves: split all wall centrelines at their intersections into a planar graph, then walk it always taking the next edge clockwise, which traces each enclosed face once (a half-edge face walk).
  • Wall joins. T-junctions, walls of different thicknesses and walls meeting at shallow angles (where a mitre would shoot far out) need their own join rules; this is one of the most special-cased parts of every BIM tool.
  • Robust booleans. Cutting openings into wall solids, and joining walls with floors, needs polygon and solid booleans built on exact predicates, as in the first lesson of this module.
  • Spatial indexing. With thousands of rooms and elements, the point-in-room query goes through a grid or BVH before any polygon test.
  • Levels and georeferencing. Each storey has its own elevation, and the whole model sits on the site through the shared-coordinates transform.
Quick check

The wall footprint computed as “outside area minus inside area” no longer equals “centreline length × thickness” after you edit the loop. What is the most likely cause?

Quick check

A room outline is extruded to a mesh, and its signed-tetrahedron volume comes out negative. What happened?

Practice

Add a bedroom: move the internal wall so the Living room’s centreline loop runs from x = 0 to 3.5, and add a new internal wall along y = 4 inside the Living room, splitting it into “Living” (y from 0 to 4) and “Bedroom” (y from 4 to 7). The Kitchen loop now starts at x = 3.5. Print each room’s area and perimeter, and check that the three rooms plus the wall footprint add up to the gross area.

Show a hint

Living is (0, 0), (3.5, 0), (3.5, 4), (0, 4); Bedroom is (0, 4), (3.5, 4), (3.5, 7), (0, 7); Kitchen is (3.5, 0), (10, 0), (10, 4), (6, 4), (6, 7), (3.5, 7). Reuse Geo.Offset, Geo.Area and Geo.Perimeter.

Show the solution
Solution.cs
const double T = 0.3;var rooms = new (string Name, List<V2> Centreline)[]{    ("Living", new() { new(0, 0), new(3.5, 0), new(3.5, 4), new(0, 4) }),    ("Bedroom", new() { new(0, 4), new(3.5, 4), new(3.5, 7), new(0, 7) }),    ("Kitchen", new() { new(3.5, 0), new(10, 0), new(10, 4), new(6, 4), new(6, 7), new(3.5, 7) }),}; double total = 0;foreach (var (name, cl) in rooms){    var outline = Geo.Offset(cl, T / 2);    double area = Geo.Area(outline);    total += area;    Console.WriteLine($"{name,-8} area {area,7:0.000} m2, perimeter {Geo.Perimeter(outline),6:0.000} m");} var outer = new List<V2> { new(0, 0), new(10, 0), new(10, 4), new(6, 4), new(6, 7), new(0, 7) };double gross = Geo.Area(Geo.Offset(outer, -T / 2));Console.WriteLine($"rooms {total:0.000} + walls {gross - total:0.000} = gross {gross:0.000} m2"); static class Geo{    public static List<V2> Offset(IList<V2> loop, double d)    {        int n = loop.Count;        var result = new List<V2>();        for (int i = 0; i < n; i++)        {            V2 a0 = loop[(i + n - 1) % n], a1 = loop[i], b1 = loop[(i + 1) % n];            V2 u = a1 - a0, v = b1 - a1;            V2 pa = a0 + Left(u) * d, pb = a1 + Left(v) * d;            double den = Cross(u, v);            if (Math.Abs(den) < 1e-12) { result.Add(pb); continue; }            result.Add(pa + u * (Cross(pb - pa, v) / den));        }        return result;    }     public static V2 Left(V2 v) => new V2(-v.Y, v.X) * (1 / v.Length);    public static double Cross(V2 a, V2 b) => a.X * b.Y - a.Y * b.X;     public static double Area(IList<V2> p)    {        double a = 0;        for (int i = 0; i < p.Count; i++) a += Cross(p[i], p[(i + 1) % p.Count]);        return a / 2;    }     public static double Perimeter(IList<V2> p) =>        Enumerable.Range(0, p.Count).Sum(i => (p[(i + 1) % p.Count] - p[i]).Length);} 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 double Length => Math.Sqrt(X * X + Y * Y);    static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###");    public override string ToString() => $"({F(X)}, {F(Y)})";}
Output
Living   area  11.840 m2, perimeter 13.800 m
Bedroom  area   8.640 m2, perimeter 11.800 m
Kitchen  area  29.540 m2, perimeter 25.800 m
rooms 50.020 + walls 13.170 = gross 63.190 m2

Remember

  • Wall faces are centrelines offset by half the thickness; neighbouring offset lines meet at mitred corners.
  • For a simple counter-clockwise loop, outside area minus inside area equals centreline length × thickness: a cheap correctness check.
  • Rooms are centreline loops pushed inwards; shoelace gives the area, the perimeter gives skirting, the centroid places the tag.
  • Extruding to a closed mesh and comparing the tetrahedron volume with area × height catches winding errors.
  • A small model of walls, rooms and openings answers schedules, finishes and point-in-room queries; real engines add room detection, join rules, robust booleans and spatial indexes.
0 of 44 lessons completed