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.
Length, and the point at a distance
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.###})";}Length 20 m 3 m: (3, 0) 10 m: (6, 4) 17.5 m: (9.5, 8)
Offsetting: a wall’s two faces
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.###})";}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
u1 and u2: the unit directions into and out of the corner V. φ = acos(u1·u2): how far the path turns. φ = 0 means straight on, with nothing to fillet. T = r·tan(φ ÷ 2); the tangent points are V − u1·T and V + u2·T. Check T against the segment lengths. If it does not fit, the radius is too big for that corner.
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.###})";}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
Show a hint
V2.Cross(u1, u2) to choose the side: positive means a left turn.
Show the solution
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.###})";}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.