Polygon area, normal and winding
The shoelace formula gives a room’s area from its corners, with a sign that says which way round the boundary goes. Use the sign for holes, find the centroid, and extend it to sloping 3D faces with Newell’s method.
The shoelace formula
var room = new V2[] { new(0, 0), new(8, 0), new(8, 3), new(3, 3), new(3, 6), new(0, 6) }; double twice = 0, perimeter = 0, cx = 0, cy = 0;for (int i = 0; i < room.Length; i++){ V2 a = room[i], b = room[(i + 1) % room.Length]; double cross = a.X * b.Y - b.X * a.Y; // twice the signed area of triangle (O, a, b) twice += cross; cx += (a.X + b.X) * cross; cy += (a.Y + b.Y) * cross; perimeter += (b - a).Length;}double area = twice / 2;Console.WriteLine($"Signed area {area} m2 ({(area > 0 ? "counterclockwise" : "clockwise")})");Console.WriteLine($"Perimeter {perimeter} m");Console.WriteLine($"Centroid ({cx / (6 * area):F3}, {cy / (6 * area):F3})"); 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.###})";}Signed area 33 m2 (counterclockwise) Perimeter 28 m Centroid (3.318, 2.318)
Winding: the sign means something
var outer = new V2[] { new(0, 0), new(10, 0), new(10, 8), new(0, 8) }; // counterclockwisevar column = new V2[] { new(5, 4), new(5, 4.4), new(5.4, 4.4), new(5.4, 4) }; // clockwise: a hole Console.WriteLine($"Outer {SignedArea(outer)}, column {SignedArea(column):0.##}");Console.WriteLine($"Net floor area {SignedArea(outer) + SignedArea(column):0.##} m2"); static double SignedArea(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.###})";}Outer 80, column -0.16 Net floor area 79.84 m2
Sloping faces in 3D: Newell’s method
var face = new V3[] { new(0, 0, 3), new(10, 0, 3), new(10, 4, 6), new(0, 4, 6) }; var sum = new V3(0, 0, 0);for (int i = 0; i < face.Length; i++) sum += V3.Cross(face[i], face[(i + 1) % face.Length]); Console.WriteLine($"Area {sum.Length / 2} m2");Console.WriteLine($"Normal {sum.Normalized()}"); 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); public override string ToString() => $"({X:0.###}, {Y:0.###}, {Z:0.###})";}Area 50 m2 Normal (0, -0.6, 0.8)
EnsureCounterClockwise(pts) that returns the polygon unchanged if its signed area is positive, and a reversed copy otherwise. Test it on the slab (0, 0), (0, 5), (6, 5), (6, 0): print the signed area before and after, and the first two corners after.
Show a hint
Clone and reverse it with Array.Reverse, so the caller’s array is not changed behind its back.
Show the solution
var slab = new V2[] { new(0, 0), new(0, 5), new(6, 5), new(6, 0) };Console.WriteLine($"Before: {SignedArea(slab)}");slab = EnsureCounterClockwise(slab);Console.WriteLine($"After: {SignedArea(slab)}, starting {slab[0]}, {slab[1]}"); static V2[] EnsureCounterClockwise(V2[] p){ if (SignedArea(p) >= 0) return p; var copy = (V2[])p.Clone(); Array.Reverse(copy); return copy;} static double SignedArea(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.###})";}Before: -30 After: 30, starting (6, 0), (6, 5)
Remember
Shoelace: area = ½ Σ (x_i·y_(i+1) − x_(i+1)·y_i), wrapping round to the first corner. Positive means counterclockwise, negative clockwise; |sum ÷ 2| is the area. Holes wound the opposite way subtract themselves when you add signed areas. The same loop gives the centroid; the formula is meaningless for self-intersecting boundaries. In 3D, Σ P_i × P_(i+1) (Newell’s method) gives the normal, with length twice the area.