Computational geometry basics
The cross product as the one tool behind most geometry code: left or right turns, segment intersection, polygon area with the shoelace formula, point in polygon, and the convex hull in O(n log n).
Left, right or straight: the cross product
(A.X − O.X)·(B.Y − O.Y) − (A.Y − O.Y)·(B.X − O.X). Its sign says which way you turn when walking O → A → B: positive means left (counter-clockwise), negative means right (clockwise), zero means the three points are on one line. Its size is twice the area of the triangle OAB. With integer coordinates it is exact.
using Pt = (long X, long Y); Pt a = (0, 0), b = (10, 0); // a wall from A to BConsole.WriteLine(Turn(a, b, (5, 4)));Console.WriteLine(Turn(a, b, (5, -3)));Console.WriteLine(Turn(a, b, (20, 0))); static long Cross(Pt o, Pt a, Pt b) => (a.X - o.X) * (b.Y - o.Y) - (a.Y - o.Y) * (b.X - o.X); static string Turn(Pt o, Pt a, Pt b) => Cross(o, a, b) switch{ > 0 => "left (counter-clockwise)", < 0 => "right (clockwise)", _ => "straight (collinear)"};left (counter-clockwise) right (clockwise) straight (collinear)
using Pt = (long X, long Y); line (C# 12) gives the tuple type a short name. long keeps the products from overflowing for coordinates up to about a billion.
Do two segments cross?
using Pt = (long X, long Y); Console.WriteLine(Intersects((0, 0), (10, 10), (0, 10), (10, 0))); // an XConsole.WriteLine(Intersects((0, 0), (10, 0), (0, 5), (10, 5))); // parallelConsole.WriteLine(Intersects((0, 0), (10, 0), (10, 0), (10, 8))); // touching at an endConsole.WriteLine(Intersects((0, 0), (4, 0), (6, 0), (10, 0))); // same line, with a gap static long Cross(Pt o, Pt a, Pt b) => (a.X - o.X) * (b.Y - o.Y) - (a.Y - o.Y) * (b.X - o.X); static bool OnSegment(Pt p, Pt q, Pt r) => // r is collinear with pq: is it between them? Math.Min(p.X, q.X) <= r.X && r.X <= Math.Max(p.X, q.X) && Math.Min(p.Y, q.Y) <= r.Y && r.Y <= Math.Max(p.Y, q.Y); static bool Intersects(Pt p1, Pt p2, Pt q1, Pt q2){ long d1 = Cross(q1, q2, p1), d2 = Cross(q1, q2, p2); long d3 = Cross(p1, p2, q1), d4 = Cross(p1, p2, q2); if (Math.Sign(d1) * Math.Sign(d2) < 0 && Math.Sign(d3) * Math.Sign(d4) < 0) return true; // proper crossing return (d1 == 0 && OnSegment(q1, q2, p1)) || (d2 == 0 && OnSegment(q1, q2, p2)) || (d3 == 0 && OnSegment(p1, p2, q1)) || (d4 == 0 && OnSegment(p1, p2, q2));}True False True False
Area of a polygon: the shoelace formula
a.X·b.Y − b.X·a.Y (the cross product of the two position vectors). Half the absolute total is the area, for any simple polygon, convex or not. The sign of the total even tells you the direction: positive for counter-clockwise corners.
using Pt = (long X, long Y); Pt[] plot = [(0, 0), (40, 0), (40, 20), (20, 20), (20, 50), (0, 50)]; // an L-shaped plot, metresConsole.WriteLine($"Area: {Area(plot)} square metres"); static double Area(Pt[] poly){ long twice = 0; for (int i = 0; i < poly.Length; i++) { Pt a = poly[i], b = poly[(i + 1) % poly.Length]; twice += a.X * b.Y - b.X * a.Y; // doubled signed area of the triangle (origin, a, b) } return Math.Abs(twice) / 2.0;}Area: 1400 square metres
Is a point inside a polygon?
using Pt = (double X, double Y); Pt[] plot = [(0, 0), (40, 0), (40, 20), (20, 20), (20, 50), (0, 50)];foreach (Pt p in new Pt[] { (10, 40), (30, 40), (30, 10) }) Console.WriteLine($"{p}: {(Inside(plot, p) ? "inside" : "outside")}"); static bool Inside(Pt[] poly, Pt p){ bool inside = false; for (int i = 0, j = poly.Length - 1; i < poly.Length; j = i++) { Pt a = poly[i], b = poly[j]; if ((a.Y > p.Y) != (b.Y > p.Y) && // the edge spans the ray's height p.X < (b.X - a.X) * (p.Y - a.Y) / (b.Y - a.Y) + a.X) // and crosses it to the right of p inside = !inside; } return inside;}(10, 40): inside (30, 40): outside (30, 10): inside
The convex hull
using Pt = (int X, int Y); Pt[] pegs = [(30, 60), (70, 230), (120, 140), (150, 40), (160, 260), (200, 120), (230, 210), (260, 70), (270, 170), (100, 90), (190, 190), (60, 150)];var hull = ConvexHull(pegs);Console.WriteLine(string.Join(" ", hull));Console.WriteLine($"{hull.Count} of {pegs.Length} pegs"); static long Cross(Pt o, Pt a, Pt b) => (long)(a.X - o.X) * (b.Y - o.Y) - (long)(a.Y - o.Y) * (b.X - o.X); static List<Pt> ConvexHull(Pt[] points){ var p = points.OrderBy(q => q.X).ThenBy(q => q.Y).ToArray(); if (p.Length < 3) return p.ToList(); var h = new List<Pt>(); foreach (var q in p) // lower edge, west to east { while (h.Count >= 2 && Cross(h[^2], h[^1], q) <= 0) h.RemoveAt(h.Count - 1); h.Add(q); } int lower = h.Count; for (int i = p.Length - 2; i >= 0; i--) // upper edge, east to west { while (h.Count > lower && Cross(h[^2], h[^1], p[i]) <= 0) h.RemoveAt(h.Count - 1); h.Add(p[i]); } h.RemoveAt(h.Count - 1); // the first point came round again return h;}(30, 60) (150, 40) (260, 70) (270, 170) (230, 210) (160, 260) (70, 230) 7 of 12 pegs
<= 0 removes collinear points too; use < 0 to keep them on the fence.
| Problem | Tool | Time |
|---|---|---|
IsConvex for a polygon given in order: it is convex when all turns along the boundary go the same way. The square should print True, the L-shaped plot False.
Show a hint
Show the solution
using Pt = (long X, long Y); Pt[] square = [(0, 0), (10, 0), (10, 10), (0, 10)];Pt[] plot = [(0, 0), (40, 0), (40, 20), (20, 20), (20, 50), (0, 50)];Console.WriteLine(IsConvex(square));Console.WriteLine(IsConvex(plot)); static bool IsConvex(Pt[] poly){ bool left = false, right = false; for (int i = 0; i < poly.Length; i++) { Pt a = poly[i], b = poly[(i + 1) % poly.Length], c = poly[(i + 2) % poly.Length]; long cr = (b.X - a.X) * (c.Y - a.Y) - (b.Y - a.Y) * (c.X - a.X); if (cr > 0) left = true; if (cr < 0) right = true; } return !(left && right); // every turn the same way}True False
Remember
The cross product’s sign gives the turn direction (left, right, straight); its size is twice a triangle’s area. Segment intersection, polygon area (shoelace) and point-in-polygon (ray casting) are all built on it. Andrew’s monotone chain finds the convex hull in O(n log n): sort, then pop points that do not turn left. Prefer integer coordinates; with doubles, compare against a tolerance instead of zero.