Mirroring and handedness
A mirror is a transform with determinant −1. It turns a right-handed frame into a left-handed one, reverses the winding of every outline and flips normals inwards. Detect it with the determinant and fix the faces, or store it as a flip.
The mirror matrix
// Mirror a door across the grid line x = 5: the plane with unit normal (1, 0, 0) and d = -5var n = new V3(1, 0, 0);double d = -5; var hinge = new V3(3, 2, 0);var across = new V3(1, 0, 0); // from the hinge across the openingvar swing = new V3(0, 1, 0); // the way the leaf opens Console.WriteLine($"Hinge {hinge} -> {MirrorPoint(hinge, n, d)}");Console.WriteLine($"Across {across} -> {MirrorDirection(across, n)}");Console.WriteLine($"Swing {swing} -> {MirrorDirection(swing, n)}"); static V3 MirrorPoint(V3 p, V3 n, double d) => p - n * (2 * (V3.Dot(n, p) + d)); // n is a unit normalstatic V3 MirrorDirection(V3 v, V3 n) => v - n * (2 * V3.Dot(n, v)); 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)})";}Hinge (3, 2, 0) -> (7, 2, 0) Across (1, 0, 0) -> (-1, 0, 0) Swing (0, 1, 0) -> (0, 1, 0)
Handedness and the determinant
var instances = new (string Name, M4 M)[]{ ("D1, turned 90", M4.Move(4, 0, 0) * M4.RotateZ(90)), ("D2, mirrored", M4.Move(10, 0, 0) * M4.Scale(-1, 1, 1)), ("D3, mirrored and turned", M4.Move(14, 0, 0) * M4.RotateZ(90) * M4.Scale(1, -1, 1)), ("D4, mirrored twice", M4.Scale(-1, 1, 1) * M4.Scale(1, -1, 1))}; foreach (var (name, m) in instances){ V3 x = m.Direction(new V3(1, 0, 0)), y = m.Direction(new V3(0, 1, 0)), z = m.Direction(new V3(0, 0, 1)); double triple = V3.Dot(V3.Cross(x, y), z); Console.WriteLine($"{name,-24} (X x Y).Z = {triple,2} {(triple < 0 ? "mirrored" : "not mirrored")}");} sealed class M4 // 4x4, column vectors: p' = M * p{ public readonly double[,] A = new double[4, 4]; public static M4 Identity() { var m = new M4(); for (int i = 0; i < 4; i++) m.A[i, i] = 1; return m; } public static M4 Move(double x, double y, double z) { var m = Identity(); m.A[0, 3] = x; m.A[1, 3] = y; m.A[2, 3] = z; return m; } public static M4 Scale(double x, double y, double z) { var m = Identity(); m.A[0, 0] = x; m.A[1, 1] = y; m.A[2, 2] = z; return m; } public static M4 RotateZ(double degrees) { double t = degrees * Math.PI / 180, c = Math.Cos(t), s = Math.Sin(t); var m = Identity(); m.A[0, 0] = c; m.A[0, 1] = -s; m.A[1, 0] = s; m.A[1, 1] = c; return m; } public static M4 operator *(M4 p, M4 q) { var m = new M4(); for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) for (int k = 0; k < 4; k++) m.A[i, j] += p.A[i, k] * q.A[k, j]; return m; } public V3 Point(V3 p) => Apply(p, 1); // w = 1: moved public V3 Direction(V3 v) => Apply(v, 0); // w = 0: only turned and scaled V3 Apply(V3 v, double w) => new( A[0, 0] * v.X + A[0, 1] * v.Y + A[0, 2] * v.Z + A[0, 3] * w, A[1, 0] * v.X + A[1, 1] * v.Y + A[1, 2] * v.Z + A[1, 3] * w, A[2, 0] * v.X + A[2, 1] * v.Y + A[2, 2] * v.Z + A[2, 3] * w);} 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)})";}D1, turned 90 (X x Y).Z = 1 not mirrored D2, mirrored (X x Y).Z = -1 mirrored D3, mirrored and turned (X x Y).Z = -1 mirrored D4, mirrored twice (X x Y).Z = 1 not mirrored
What breaks, and how to fix it
Winding reverses. A counterclockwise outline becomes clockwise, so signed areas turn negative and the outer/hole convention is swapped. Normals flip inwards. A face normal computed as (B − A) × (C − A) from the mirrored corners points into the solid, so lighting, clash tests and volume signs go wrong. Hands and text swap. Doors, stairs and other “handed” families change hand, and any text or symbol modelled in 3D reads backwards.
// The top face of a slab, counterclockwise seen from above, so its normal points upvar face = new V3[] { new(0, 0, 0.2), new(4, 0, 0.2), new(4, 3, 0.2), new(0, 3, 0.2) };M4 mirror = M4.Scale(-1, 1, 1); var moved = face.Select(mirror.Point).ToArray();Console.WriteLine($"Before: normal {Normal(face)}");Console.WriteLine($"Mirrored: normal {Normal(moved)} <- points into the slab"); if (Det3(mirror) < 0) Array.Reverse(moved);Console.WriteLine($"Reversed: normal {Normal(moved)}"); static V3 Normal(V3[] p) => V3.Cross(p[1] - p[0], p[2] - p[0]).Normalized(); static double Det3(M4 m) => m.A[0, 0] * (m.A[1, 1] * m.A[2, 2] - m.A[1, 2] * m.A[2, 1]) - m.A[0, 1] * (m.A[1, 0] * m.A[2, 2] - m.A[1, 2] * m.A[2, 0]) + m.A[0, 2] * (m.A[1, 0] * m.A[2, 1] - m.A[1, 1] * m.A[2, 0]); sealed class M4 // 4x4, column vectors: p' = M * p{ public readonly double[,] A = new double[4, 4]; public static M4 Identity() { var m = new M4(); for (int i = 0; i < 4; i++) m.A[i, i] = 1; return m; } public static M4 Move(double x, double y, double z) { var m = Identity(); m.A[0, 3] = x; m.A[1, 3] = y; m.A[2, 3] = z; return m; } public static M4 Scale(double x, double y, double z) { var m = Identity(); m.A[0, 0] = x; m.A[1, 1] = y; m.A[2, 2] = z; return m; } public static M4 RotateZ(double degrees) { double t = degrees * Math.PI / 180, c = Math.Cos(t), s = Math.Sin(t); var m = Identity(); m.A[0, 0] = c; m.A[0, 1] = -s; m.A[1, 0] = s; m.A[1, 1] = c; return m; } public static M4 operator *(M4 p, M4 q) { var m = new M4(); for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) for (int k = 0; k < 4; k++) m.A[i, j] += p.A[i, k] * q.A[k, j]; return m; } public V3 Point(V3 p) => Apply(p, 1); // w = 1: moved public V3 Direction(V3 v) => Apply(v, 0); // w = 0: only turned and scaled V3 Apply(V3 v, double w) => new( A[0, 0] * v.X + A[0, 1] * v.Y + A[0, 2] * v.Z + A[0, 3] * w, A[1, 0] * v.X + A[1, 1] * v.Y + A[1, 2] * v.Z + A[1, 3] * w, A[2, 0] * v.X + A[2, 1] * v.Y + A[2, 2] * v.Z + A[2, 3] * w);} 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)})";}Before: normal (0, 0, 1) Mirrored: normal (0, 0, -1) <- points into the slab Reversed: normal (0, 0, 1)
Storing a mirror as a flip
Mirrored, HandFlipped and FacingFlipped on a FamilyInstance, so you can ask whether a door has been mirrored rather than digging the sign out of its matrix. When reading placements from any source, still check the determinant: it is the one test that never lies.
Show a hint
Array.Reverse on the mirrored array.
Show the solution
var room = new V3[] { new(1, 1, 0), new(5, 1, 0), new(5, 4, 0), new(1, 4, 0) };M4 mirror = M4.Scale(-1, 1, 1); var moved = room.Select(mirror.Point).ToArray();Console.WriteLine($"Before: {Area(room)}");Console.WriteLine($"Mirrored: {Area(moved)}");Array.Reverse(moved);Console.WriteLine($"Reversed: {Area(moved)}"); static double Area(V3[] p){ double s = 0; for (int i = 0; i < p.Length; i++) { V3 a = p[i], b = p[(i + 1) % p.Length]; s += a.X * b.Y - b.X * a.Y; } return s / 2;} sealed class M4 // 4x4, column vectors: p' = M * p{ public readonly double[,] A = new double[4, 4]; public static M4 Identity() { var m = new M4(); for (int i = 0; i < 4; i++) m.A[i, i] = 1; return m; } public static M4 Move(double x, double y, double z) { var m = Identity(); m.A[0, 3] = x; m.A[1, 3] = y; m.A[2, 3] = z; return m; } public static M4 Scale(double x, double y, double z) { var m = Identity(); m.A[0, 0] = x; m.A[1, 1] = y; m.A[2, 2] = z; return m; } public static M4 RotateZ(double degrees) { double t = degrees * Math.PI / 180, c = Math.Cos(t), s = Math.Sin(t); var m = Identity(); m.A[0, 0] = c; m.A[0, 1] = -s; m.A[1, 0] = s; m.A[1, 1] = c; return m; } public static M4 operator *(M4 p, M4 q) { var m = new M4(); for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) for (int k = 0; k < 4; k++) m.A[i, j] += p.A[i, k] * q.A[k, j]; return m; } public V3 Point(V3 p) => Apply(p, 1); // w = 1: moved public V3 Direction(V3 v) => Apply(v, 0); // w = 0: only turned and scaled V3 Apply(V3 v, double w) => new( A[0, 0] * v.X + A[0, 1] * v.Y + A[0, 2] * v.Z + A[0, 3] * w, A[1, 0] * v.X + A[1, 1] * v.Y + A[1, 2] * v.Z + A[1, 3] * w, A[2, 0] * v.X + A[2, 1] * v.Y + A[2, 2] * v.Z + A[2, 3] * w);} 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)})";}Before: 12 Mirrored: -12 Reversed: 12
Remember
Mirror a point in a plane with P′ = P − 2(n·P + d)·n; the matrix is I − 2·n·nᵀ. A mirror has det = −1 and turns a right-handed frame left-handed; two mirrors make a rotation. Check the determinant (or the triple product of the axes) to detect mirrored instances. After a mirror, reverse each face’s vertex order so normals point outwards again. Tools often store a mirror as a flip plus a rotation, but the determinant is the reliable test.