<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 16 of 44 15 minIntermediate

Polylines: length, offsetting and fillets

A polyline is a list of points joined by segments. Measure it, find the point at a given distance along it, offset it into a wall’s two faces, and round its corners with fillets.

Wall chains, pipe and duct runs, room boundaries, slab edges, cable trays and road centrelines are all polylines: points joined by straight segments, sometimes with arcs at the corners. Everything from the last four lessons now gets applied one segment at a time.

Interactive

Polyline: offset and fillet

Drag the vertices V0 to V3 and move the slider. Make a corner sharp to see what goes wrong.

V0V1V2V3
centreline length15.83
left face (+δ)15.92
right face (−δ)15.74
miter at V11.49 × δ
miter at V21.41 × δ
Each segment moves δ = 0.40 along its normal, and neighbours are joined where their offset lines meet: V + (n1 + n2) × δ ÷ (1 + n1·n2). The inside face is shorter than the centreline and the outside face longer.

Length, and the point at a distance

The length is the sum of the segment lengths. To find the point at distance s along it (“the 10 m mark of this duct run”), walk the segments, subtracting each length from s until s falls inside one, then interpolate within that segment. Roads and railways call this distance chainage or stationing.

Chainage.cs
var run = new V2[] { new(0, 0), new(6, 0), new(6, 8), new(12, 8) }; double total = 0;for (int i = 1; i < run.Length; i++) total += (run[i] - run[i - 1]).Length;Console.WriteLine($"Length {total} m"); foreach (double s in new[] { 3.0, 10.0, 17.5 })    Console.WriteLine($"{s,4} m: {PointAt(run, s)}"); static V2 PointAt(V2[] pts, double s){    for (int i = 1; i < pts.Length; i++)    {        V2 d = pts[i] - pts[i - 1];        double len = d.Length;        if (s <= len) return pts[i - 1] + d * (s / len);        s -= len;                         // on to the next segment    }    return pts[^1];                       // past the end: clamp to the last point} 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
Length 20 m
   3 m: (3, 0)
  10 m: (6, 4)
17.5 m: (9.5, 8)

Offsetting: a wall’s two faces

A wall is drawn along a location line, often its centreline. Its two faces are that polyline offset by half the thickness to each side. Move each segment along its left normal n = (−d.y, d.x) ÷ |d| by a distance δ, using a negative δ for the right side. The moved segments no longer meet: they overlap at an inside corner and leave a gap at an outside one. Join them where the two offset lines cross. This is called a miter join.

There is a short formula for the miter point at a vertex V, where the normals of the two segments are n1 and n2: V + (n1 + n2) × δ ÷ (1 + n1·n2). At a right angle n1·n2 = 0, and the corner moves δ along each normal. As the corner gets sharper, 1 + n1·n2 approaches 0 and the miter point shoots off into a spike. Drawing programs cap this with a miter limit and cut the corner off (a bevel) instead.

WallFaces.cs
var centre = new V2[] { new(0, 0), new(6, 0), new(6, 4) };   // an L-shaped walldouble half = 0.15;                                          // 300 mm thick Console.WriteLine("Left face:  " + string.Join(" ", Offset(centre, half)));Console.WriteLine("Right face: " + string.Join(" ", Offset(centre, -half))); static V2[] Offset(V2[] pts, double delta){    var n = new V2[pts.Length - 1];               // left normal of each segment    for (int i = 0; i < n.Length; i++)    {        V2 d = pts[i + 1] - pts[i];        n[i] = new V2(-d.Y, d.X) * (1 / d.Length);    }    var result = new V2[pts.Length];    result[0] = pts[0] + n[0] * delta;    result[^1] = pts[^1] + n[^1] * delta;    for (int i = 1; i < pts.Length - 1; i++)      // miter joins at the inner vertices        result[i] = pts[i] + (n[i - 1] + n[i]) * (delta / (1 + V2.Dot(n[i - 1], n[i])));    return result;} 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
Left face:  (0, 0.15) (5.85, 0.15) (5.85, 4)
Right face: (0, -0.15) (6.15, -0.15) (6.15, 4)

Fillets: rounding the corners

A fillet replaces a sharp corner with an arc of radius r that touches both segments: pipe bends, rounded slab corners, kerb lines. If the path turns by an angle φ at the corner, the arc starts and ends a distance T = r·tan(φ ÷ 2) back from the corner along each segment, and it is r·φ long.

  1. u1 and u2: the unit directions into and out of the corner V.
  2. φ = acos(u1·u2): how far the path turns. φ = 0 means straight on, with nothing to fillet.
  3. T = r·tan(φ ÷ 2); the tangent points are V − u1·T and V + u2·T.
  4. Check T against the segment lengths. If it does not fit, the radius is too big for that corner.
PipeBend.cs
var a = new V2(0, 0);var v = new V2(4, 0);                     // the cornervar b = new V2(4, 3);double r = 0.5;                           // bend radius V2 u1 = Unit(v - a), u2 = Unit(b - v);double phi = Math.Acos(Math.Clamp(V2.Dot(u1, u2), -1, 1));double t = r * Math.Tan(phi / 2); Console.WriteLine($"Turn {phi * 180 / Math.PI:F1} deg, tangent distance {t:F3} m");Console.WriteLine($"Bend from {v - u1 * t} to {v + u2 * t}, arc length {r * phi:F3} m");Console.WriteLine($"The run is {2 * t - r * phi:F3} m shorter"); static V2 Unit(V2 d) => d * (1 / d.Length); 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
Turn 90.0 deg, tangent distance 0.500 m
Bend from (3.5, 0) to (4, 0.5), arc length 0.785 m
The run is 0.215 m shorter
Quick check

A polyline turns 90° at a corner, and you fillet it with r = 2 m. How far back from the corner are the tangent points?

Quick check

You offset each segment of a polyline but do not join them. What happens at an outside corner?

Practice

A duct run goes (0, 0) → (5, 0) → (8, 4) and needs a bend of radius 1 m at the corner. Print the turn angle, the two tangent points and the centre of the bend. Because this run turns left, the centre is r along the left normal of the first segment from the first tangent point.

Show a hint

u1 = (1, 0) and u2 = (0.6, 0.8), so φ = acos(0.6). Use the sign of V2.Cross(u1, u2) to choose the side: positive means a left turn.

Show the solution
Solution.cs
var a = new V2(0, 0);var v = new V2(5, 0);var b = new V2(8, 4);double r = 1; V2 u1 = Unit(v - a), u2 = Unit(b - v);double phi = Math.Acos(Math.Clamp(V2.Dot(u1, u2), -1, 1));double t = r * Math.Tan(phi / 2);V2 t1 = v - u1 * t, t2 = v + u2 * t;bool turnsLeft = V2.Cross(u1, u2) > 0;V2 left = new(-u1.Y, u1.X);V2 centre = t1 + left * (turnsLeft ? r : -r); Console.WriteLine($"Turn {phi * 180 / Math.PI:F2} deg");Console.WriteLine($"Tangent points {t1} and {t2}");Console.WriteLine($"Centre {centre}"); static V2 Unit(V2 d) => d * (1 / d.Length); 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
Turn 53.13 deg
Tangent points (4.5, 0) and (5.3, 0.4)
Centre (4.5, 1)

Remember

  • A polyline’s length is the sum of its segments; the point at distance s comes from walking the segments.
  • Offset each segment along its normal and join neighbours at a miter: V + (n1 + n2) × δ ÷ (1 + n1·n2).
  • Sharp corners make miter spikes; real tools apply a miter limit and bevel instead.
  • A fillet of radius r at a turn of φ cuts back T = r·tan(φ ÷ 2) on each side and adds an arc of length r·φ.
  • Check that T fits on both segments; otherwise the radius is too big for that corner.
0 of 44 lessons completed