Line–plane and plane–plane intersection
Find where a pipe passes through a slab, or a pick ray hits a work plane, with one division. Then intersect two planes to get ridges, hips and valleys, and see why a hip is always flatter than the roof.
A line and a plane
| Result | Meaning |
|---|---|
var a = new V3(1, 2, 0.5); // pipe startvar b = new V3(4, 6, 5.5); // pipe endvar up = new V3(0, 0, 1); foreach (var (name, level) in new[] { ("Slab top", 3.0), ("Slab soffit", 2.75) }){ double? t = Hit(a, b - a, up, -level); Console.WriteLine(t is double v && v >= 0 && v <= 1 ? $"{name}: t = {v}, at {a + (b - a) * v}" : $"{name}: no crossing");} static double? Hit(V3 a, V3 u, V3 n, double d){ double nu = V3.Dot(n, u); if (Math.Abs(nu) < 1e-12) return null; // parallel to the plane return -(V3.Dot(n, a) + d) / nu;} 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.###})";}Slab top: t = 0.5, at (2.5, 4, 3) Slab soffit: t = 0.45, at (2.35, 3.8, 2.75)
Two planes
c1 = (h1·(n2·n2) − h2·(n1·n2)) ÷ |n1 × n2|² c2 = (h2·(n1·n1) − h1·(n1·n2)) ÷ |n1 × n2|² The point c1·n1 + c2·n2 is the point of the line closest to the origin. If n1 × n2 ≈ 0, the planes are parallel.
var slopeA = (N: new V3(0, -0.6, 0.8), H: 2.4); // n.X = h, rising from eaves at y = 0var slopeB = (N: new V3(0, 0.6, 0.8), H: 7.2); // the opposite slope, eaves at y = 8var endC = (N: new V3(0.6, 0, 0.8), H: 2.4); // the hipped end, eaves at x = 0 Report("Ridge", slopeA.N, slopeA.H, slopeB.N, slopeB.H);Report("Hip", slopeA.N, slopeA.H, endC.N, endC.H); static void Report(string name, V3 n1, double h1, V3 n2, double h2){ V3 dir = V3.Cross(n1, n2); double det = V3.Dot(dir, dir); if (det < 1e-12) { Console.WriteLine($"{name}: parallel planes"); return; } double n12 = V3.Dot(n1, n2); double c1 = (h1 * V3.Dot(n2, n2) - h2 * n12) / det; double c2 = (h2 * V3.Dot(n1, n1) - h1 * n12) / det; V3 p = n1 * c1 + n2 * c2; // the point of the line nearest the origin V3 u = dir.Normalized(); Console.WriteLine($"{name}: through {p}, direction {u}, slope {Math.Asin(Math.Abs(u.Z)) * 180 / Math.PI:F2} deg");} 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.###})";}Ridge: through (0, 4, 6), direction (-1, 0, 0), slope 0.00 deg Hip: through (0.878, -0.878, 2.341), direction (-0.625, 0.625, 0.469), slope 27.94 deg
Show a hint
Show the solution
var wallN = new V3(1, 0, 0);double wallD = -4; // the wall face x = 4 Test(new V3(1, 1, 3), new V3(7, 3, 3));Test(new V3(1, 1, 3), new V3(1, 5, 3)); void Test(V3 a, V3 b){ V3 u = b - a; double nu = V3.Dot(wallN, u); if (Math.Abs(nu) < 1e-12) { Console.WriteLine($"{a} to {b}: parallel to the wall"); return; } double t = -(V3.Dot(wallN, a) + wallD) / nu; Console.WriteLine(t >= 0 && t <= 1 ? $"{a} to {b}: crosses at {a + u * t}" : $"{a} to {b}: does not reach the wall");} 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.###})";}(1, 1, 3) to (7, 3, 3): crosses at (4, 2, 3) (1, 1, 3) to (1, 5, 3): parallel to the wall
Remember
Line A + t·u meets plane n·X + d = 0 at t = −(n·A + d) ÷ (n·u). n·u ≈ 0 means parallel: test it before dividing, with a tolerance relative to |u|. For a segment, only 0 ≤ t ≤ 1 is a real crossing: that is where sleeves and openings go. Two planes meet along n1 × n2; a point on the line comes from mixing the two normals. Ridges, hips and valleys are plane–plane intersections, and hips are flatter than the roof.