Closest point and distance to a line or segment
Project a point onto a line with one dot product, clamp t for a segment, and you have the closest point and the distance. This is how snapping, hit-testing and simple clash checks work.
Projecting onto a line
w = P − A: the step from the start of the line to P. t = (w·d) ÷ (d·d): how far along the closest point is, as a fraction of d. Q = A + t·d: the foot of the perpendicular. distance = |P − Q|.
Segments: clamp t
var a = new V2(0, 0);var b = new V2(8, 0); // a wall along X, 8 m long foreach (var p in new V2[] { new(3, 2), new(10, 1.5), new(-2, -1) }){ var (q, t) = Closest(a, b, p); Console.WriteLine($"P {p}: t = {t}, closest {q}, distance {(p - q).Length:F3}");} static (V2 Point, double T) Closest(V2 a, V2 b, V2 p){ V2 d = b - a; double len2 = V2.Dot(d, d); if (len2 < 1e-12) return (a, 0); // a zero-length segment is just a point double t = Math.Clamp(V2.Dot(p - a, d) / len2, 0, 1); return (a + d * t, t);} 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.###})";}P (3, 2): t = 0.375, closest (3, 0), distance 2.000 P (10, 1.5): t = 1, closest (8, 0), distance 2.500 P (-2, -1): t = 0, closest (0, 0), distance 2.236
Snapping to the nearest wall
var walls = new (string Name, V2 A, V2 B)[]{ ("W1", new(0, 0), new(10, 0)), ("W2", new(10, 0), new(10, 6)), ("W3", new(0, 0), new(0, 6)),};const double SnapRadius = 0.3; foreach (var cursor in new V2[] { new(9.8, 3.1), new(4, 0.25), new(5, 3) }){ string found = "nothing"; double bestD2 = SnapRadius * SnapRadius; V2 snap = cursor; foreach (var w in walls) { V2 d = w.B - w.A; double t = Math.Clamp(V2.Dot(cursor - w.A, d) / V2.Dot(d, d), 0, 1); V2 q = w.A + d * t; double d2 = V2.Dot(cursor - q, cursor - q); if (d2 <= bestD2) { bestD2 = d2; snap = q; found = $"{w.Name} at {t * d.Length:0.##} m"; } } Console.WriteLine($"Cursor {cursor} snaps to {found}: {snap}");} 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.###})";}Cursor (9.8, 3.1) snaps to W2 at 3.1 m: (10, 3.1) Cursor (4, 0.25) snaps to W1 at 4 m: (4, 0) Cursor (5, 3) snaps to nothing: (5, 3)
Distance to a line, with no foot needed
DistanceToSegment(a, b, p) for 3D points and report the heads at (2, 1.1, 2.9), (6, 1.3, 3) and (12.1, 1, 3).
Show a hint
Closest, with V3.Dot. The last head is past the end of the pipe, so its closest point is the end itself.
Show the solution
var a = new V3(0, 1, 3);var b = new V3(12, 1, 3); foreach (var head in new V3[] { new(2, 1.1, 2.9), new(6, 1.3, 3), new(12.1, 1, 3) }){ double dist = DistanceToSegment(a, b, head); Console.WriteLine($"{head}: {dist:F3} m, {(dist <= 0.15 ? "connected" : "too far")}");} static double DistanceToSegment(V3 a, V3 b, V3 p){ V3 d = b - a; double t = Math.Clamp(V3.Dot(p - a, d) / V3.Dot(d, d), 0, 1); return (p - (a + d * t)).Length;} 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 double Length => Math.Sqrt(Dot(this, this)); public override string ToString() => $"({X:0.###}, {Y:0.###}, {Z:0.###})";}(2, 1.1, 2.9): 0.141 m, connected (6, 1.3, 3): 0.300 m, too far (12.1, 1, 3): 0.100 m, connected
Remember
The closest point on a line is the foot of the perpendicular: t = (P − A)·d ÷ (d·d), Q = A + t·d. For a segment, clamp t to [0, 1] first; otherwise you snap to empty space past the end. Compare squared distances when searching for the nearest element; take the root only for the winner. In 2D, |d × (P − A)| ÷ |d| is the distance to the line, and its sign is the side. The same code works in 3D, and distance-to-segment is the basis of simple pipe clash tests.