<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. BIM Math
  3. Lines and curves
All lessons in this course

Lesson 13 of 44 13 minIntermediate

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.

Hover near a wall in any BIM tool and it highlights. Move the cursor close to a pipe and it snaps to it. Both features answer the same question: which point on this line is closest to P, and how far away is it? The answer takes one dot product.

Interactive

Closest point on a line or segment

Drag the cursor point P along and past the ends of the wall AB. A and B can be dragged too.

QABP
d = B − A⟨6, 3⟩
w = P − A⟨4, 4⟩
t = (w·d) ÷ (d·d)36 ÷ 45 = 0.8
clamped to 0…10.8
Q = A + t × d(0.8, 1.4)
distance |P − Q|1.79
PQ meets the wall at a right angle. Q is 80% of the way from A to B, and P is 1.79 away from it.

Projecting onto a line

For the line A + t·d, the closest point Q is where PQ meets the line at a right angle. A right angle means (P − Q)·d = 0, and solving that for t gives the projection from the last lesson:

  1. w = P − A: the step from the start of the line to P.
  2. t = (w·d) ÷ (d·d): how far along the closest point is, as a fraction of d.
  3. Q = A + t·d: the foot of the perpendicular.
  4. distance = |P − Q|.

Segments: clamp t

For a segment, a t outside 0…1 means the foot of the perpendicular is off the end. The closest point of the segment is then that end itself, so clamp t to [0, 1] before computing Q. Forgetting the clamp is a classic bug: the cursor snaps to a point in empty space past the end of a wall. Try it in the widget by switching to the infinite line.

ClosestOnSegment.cs
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.###})";}
Output
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

A snap is the closest point on the nearest element, as long as it is within a tolerance. Comparing squared distances avoids a square root for every candidate. The station along the wall, t × length, is what a dimension or a hosted element’s offset would show.

SnapToWall.cs
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.###})";}
Output
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

If you only need the distance to an infinite line in 2D, the cross product gives it in one step: distance = |d × (P − A)| ÷ |d|. The cross product is the area of the parallelogram on d and P − A, and dividing an area by its base gives the height. Without the absolute value, its sign tells you which side of the line P is on: the “which side of the wall?” test from the cross-product lesson.

Quick check

Segment AB has A = (0, 0) and B = (4, 0). What is its closest point to P = (6, 3)?

Quick check

Why is t computed as (w·d) ÷ (d·d), not (w·d) ÷ |d|?

Practice

A sprinkler pipe runs from (0, 1, 3) to (12, 1, 3). A sprinkler head is connected if it is within 0.15 m of the pipe’s centreline. Write 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

Use the same clamped projection as 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
Solution.cs
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.###})";}
Output
(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.
0 of 44 lessons completed