Local vs world: families, instances and nested placement
A family is drawn once in its own local coordinates; each instance stores a placement that maps local to world. Nested families multiply placements down the chain, and the inverse takes world points back into an object’s own coordinates.
Local to world
FamilyInstance.GetTransform(), with BasisX, BasisY, BasisZ and Origin. IFC stores each element’s ObjectPlacement as an IfcLocalPlacement, a position and axes relative to another placement.
Nested placement
PlacementRelTo, and why moving a storey up by 100 mm moves everything on it without touching a single element. In the widget, turn the desk and the chair turns with it, while its position inside the desk stays the same.
M4 storey = M4.Move(0, 0, 3.6); // level 2, in the buildingM4 desk = M4.Move(10, 4, 0) * M4.RotateZ(30); // the desk instance, on the storeyM4 chair = M4.Move(1.2, -0.7, 0); // the chair, nested in the desk family var seatCorner = new V3(0.4, 0.4, 0.45); // in the chair's own coordinatesM4 chairToWorld = storey * desk * chair; Console.WriteLine($"In the desk: {chair.Point(seatCorner)}");Console.WriteLine($"On the storey: {(desk * chair).Point(seatCorner)}");Console.WriteLine($"In the world: {chairToWorld.Point(seatCorner)}");Console.WriteLine($"Chair faces {chairToWorld.Direction(new V3(0, 1, 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)})";}In the desk: (1.6, -0.3, 0.45) On the storey: (11.536, 4.54, 0.45) In the world: (11.536, 4.54, 4.05) Chair faces (-0.5, 0.866, 0)
World to local
M4 desk = M4.Move(10, 4, 3.6) * M4.RotateZ(30);M4 toDesk = RigidInverse(desk); foreach (var w in new V3[] { new(11, 5, 4.35), new(9, 6, 4.35) }){ V3 p = toDesk.Point(w); bool over = p.X >= 0 && p.X <= 2.4 && p.Y >= 0 && p.Y <= 1.2; Console.WriteLine($"{w} is {p} in desk coordinates: {(over ? "over the desk" : "not over the desk")}");} static M4 RigidInverse(M4 m){ var r = M4.Identity(); for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) r.A[i, j] = m.A[j, i]; for (int i = 0; i < 3; i++) r.A[i, 3] = -(r.A[i, 0] * m.A[0, 3] + r.A[i, 1] * m.A[1, 3] + r.A[i, 2] * m.A[2, 3]); return r;} 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)})";}(11, 5, 4.35) is (1.366, 0.366, 0.75) in desk coordinates: over the desk (9, 6, 4.35) is (0.134, 2.232, 0.75) in desk coordinates: not over the desk
Show a hint
Point.
Show the solution
M4 chairInDesk = M4.Move(1.2, -0.7, 0);var corner = new V3(0.4, 0.4, 0);var desks = new (V3 At, double Turn)[] { (new(0, 0, 0), 0), (new(5, 0, 0), 90), (new(10, 0, 0), 180) }; foreach (var (at, turn) in desks){ M4 chairToWorld = M4.Move(at.X, at.Y, at.Z) * M4.RotateZ(turn) * chairInDesk; Console.WriteLine($"Desk at {at}, turned {turn} deg: corner at {chairToWorld.Point(corner)}");} 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)})";}Desk at (0, 0, 0), turned 0 deg: corner at (1.6, -0.3, 0) Desk at (5, 0, 0), turned 90 deg: corner at (5.3, 1.6, 0) Desk at (10, 0, 0), turned 180 deg: corner at (8.4, 0.3, 0)
Remember
Families are modelled once in local coordinates; each instance stores a placement. world = Placement · local; the placement’s columns are the instance’s axes and origin. Nested placements multiply down the chain, outermost on the left. The inverse placement takes world points into an object’s own coordinates, where tests are simple. Keeping geometry local keeps its coordinates small and precise.