Computational Geometry
Master algorithms for working with geometric structures, including points, lines, convex hulls, line intersections, polygon operations, and Voronoi diagrams.
Computational geometry focuses on designing algorithms and data structures for solving geometric problems. Let's explore essential concepts, implementations, and powerful techniques for geometric calculations.
1. Basics & Structures
The foundation of computational geometry starts with representing basic geometric entities.
// Point structure - the most basic geometric entity
struct Point {
double x, y;
Point(double _x = 0, double _y = 0) : x(_x), y(_y) {}
// Vector addition and subtraction
Point operator+(const Point& p) const { return Point(x + p.x, y + p.y); }
Point operator-(const Point& p) const { return Point(x - p.x, y - p.y); }
// Scalar multiplication
Point operator*(double c) const { return Point(x * c, y * c); }
// Dot product
double dot(const Point& p) const { return x * p.x + y * p.y; }
// Cross product magnitude
double cross(const Point& p) const { return x * p.y - y * p.x; }
// Euclidean distance to another point
double dist(const Point& p) const {
return sqrt((x - p.x) * (x - p.x) + (y - p.y) * (y - p.y));
}
// Square of distance (avoiding square root for comparisons)
double sqDist(const Point& p) const {
return (x - p.x) * (x - p.x) + (y - p.y) * (y - p.y);
}
// Magnitude (length) of the vector
double mag() const { return sqrt(x * x + y * y); }
// Equality operator with epsilon for floating-point comparison
bool operator==(const Point& p) const {
const double EPS = 1e-9;
return fabs(x - p.x) < EPS && fabs(y - p.y) < EPS;
}
};2. Vector Operations & Utility Functions
Vector operations
Essential vector and geometric operations that form the building blocks of more complex algorithms.
// Determine if three points make a left turn
bool isLeftTurn(const Point& a, const Point& b, const Point& c) {
return (b - a).cross(c - a) > 0;
}
// Determine if three points make a right turn
bool isRightTurn(const Point& a, const Point& b, const Point& c) {
return (b - a).cross(c - a) < 0;
}
// Check if three points are collinear (lie on the same line)
bool isCollinear(const Point& a, const Point& b, const Point& c) {
const double EPS = 1e-9;
return fabs((b - a).cross(c - a)) < EPS;
}
// Angle between three points (angle at point b)
double angle(const Point& a, const Point& b, const Point& c) {
Point ba = a - b;
Point bc = c - b;
return acos(ba.dot(bc) / (ba.mag() * bc.mag()));
}
// Calculate the area of a triangle formed by three points
double triangleArea(const Point& a, const Point& b, const Point& c) {
return fabs((b - a).cross(c - a)) / 2.0;
}
// Check if point p lies inside the triangle formed by a, b, c
bool pointInTriangle(const Point& p, const Point& a, const Point& b, const Point& c) {
// Calculate areas
double areaABC = triangleArea(a, b, c);
double areaPBC = triangleArea(p, b, c);
double areaPAC = triangleArea(p, a, c);
double areaPAB = triangleArea(p, a, b);
// Check if sum of sub-triangle areas equals the original triangle area
const double EPS = 1e-9;
return fabs(areaABC - (areaPBC + areaPAC + areaPAB)) < EPS;
}3. Line Segment Intersection
Detecting whether two line segments intersect is a fundamental problem in computational geometry.
// Line segment structure
struct Segment {
Point p1, p2;
Segment(const Point& _p1, const Point& _p2) : p1(_p1), p2(_p2) {}
};
// Check if two segments intersect
bool segmentsIntersect(const Segment& s1, const Segment& s2) {
Point a = s1.p1, b = s1.p2;
Point c = s2.p1, d = s2.p2;
// Calculate orientations
double o1 = (b - a).cross(c - a);
double o2 = (b - a).cross(d - a);
double o3 = (d - c).cross(a - c);
double o4 = (d - c).cross(b - c);
// General case: segments intersect if orientations are opposite
if (o1 * o2 < 0 && o3 * o4 < 0) return true;
// Special cases: check for collinearity and endpoint overlaps
const double EPS = 1e-9;
if (fabs(o1) < EPS && pointOnSegment(c, a, b)) return true;
if (fabs(o2) < EPS && pointOnSegment(d, a, b)) return true;
if (fabs(o3) < EPS && pointOnSegment(a, c, d)) return true;
if (fabs(o4) < EPS && pointOnSegment(b, c, d)) return true;
return false;
}
// Check if point p lies on segment defined by points a and b
bool pointOnSegment(const Point& p, const Point& a, const Point& b) {
const double EPS = 1e-9;
// Check if p is collinear with segment ab
if (fabs((p - a).cross(b - a)) > EPS) return false;
// Check if p is within the bounding box of segment ab
if (p.x < min(a.x, b.x) - EPS || p.x > max(a.x, b.x) + EPS) return false;
if (p.y < min(a.y, b.y) - EPS || p.y > max(a.y, b.y) + EPS) return false;
return true;
}
// Find the intersection point of two non-parallel segments
Point segmentIntersection(const Segment& s1, const Segment& s2) {
Point a = s1.p1, b = s1.p2;
Point c = s2.p1, d = s2.p2;
// Calculate parameters for the intersection formula
double a1 = b.y - a.y;
double b1 = a.x - b.x;
double c1 = a1 * a.x + b1 * a.y;
double a2 = d.y - c.y;
double b2 = c.x - d.x;
double c2 = a2 * c.x + b2 * c.y;
double determinant = a1 * b2 - a2 * b1;
// Check if lines are parallel (determinant close to 0)
const double EPS = 1e-9;
if (fabs(determinant) < EPS) {
// Handle parallel case or return a sentinel value
return Point(NAN, NAN);
}
// Calculate intersection point
double x = (b2 * c1 - b1 * c2) / determinant;
double y = (a1 * c2 - a2 * c1) / determinant;
return Point(x, y);
}4. Convex Hull (Graham Scan Algorithm)
The convex hull is the smallest convex polygon that contains all given points. The Graham scan is an efficient method to compute it.
// Compute the convex hull using Graham scan algorithm
std::vector<Point> grahamScan(std::vector<Point> points) {
if (points.size() <= 2) return points;
// Find the lowest point (and leftmost if tie)
int lowestIdx = 0;
for (int i = 1; i < points.size(); i++) {
if (points[i].y < points[lowestIdx].y ||
(points[i].y == points[lowestIdx].y && points[i].x < points[lowestIdx].x)) {
lowestIdx = i;
}
}
// Move the lowest point to position 0
std::swap(points[0], points[lowestIdx]);
// Sort the rest of the points by polar angle with respect to the lowest point
Point pivot = points[0];
std::sort(points.begin() + 1, points.end(), [&pivot](const Point& a, const Point& b) {
double cross = (a - pivot).cross(b - pivot);
if (fabs(cross) < 1e-9) {
// If collinear, sort by distance to pivot
return pivot.sqDist(a) < pivot.sqDist(b);
}
return cross > 0;
});
// Build the convex hull
std::vector<Point> hull;
hull.push_back(points[0]);
hull.push_back(points[1]);
for (int i = 2; i < points.size(); i++) {
while (hull.size() >= 2 && !isLeftTurn(hull[hull.size()-2], hull.back(), points[i])) {
hull.pop_back();
}
hull.push_back(points[i]);
}
return hull;
}Explanation
- We first find the lowest point (with smallest y-coordinate) as our starting point
- We sort all other points by polar angle around this starting point
- We build the hull by adding points one by one, removing points that would create a non-left turn
- Time complexity: O(n log n) for the sorting step, where n is the number of points
- Space complexity: O(n) for storing the hull points
5. Point in Polygon Check
Determines if a point is inside, outside, or on the boundary of a polygon.
// Check if a point is inside a polygon using ray casting algorithm
// Returns: 1 for inside
// 0 for on the boundary
// -1 for outside
int pointInPolygon(const Point& p, const std::vector<Point>& polygon) {
int n = polygon.size();
if (n < 3) return -1; // Not a polygon
bool onSegment = false;
int windingNumber = 0; // winding number counter
for (int i = 0; i < n; i++) {
// Check if point is on any polygon segment
if (pointOnSegment(p, polygon[i], polygon[(i+1)%n])) {
return 0; // On boundary
}
// Check if ray from point crosses this edge
if (polygon[i].y <= p.y) {
if (polygon[(i+1)%n].y > p.y &&
isLeftTurn(polygon[i], polygon[(i+1)%n], p)) {
windingNumber++;
}
} else {
if (polygon[(i+1)%n].y <= p.y &&
isRightTurn(polygon[i], polygon[(i+1)%n], p)) {
windingNumber--;
}
}
}
return windingNumber != 0 ? 1 : -1;
}Explanation
- The algorithm uses the winding number method to determine if a point is inside a polygon
- It casts a ray from the point and counts how many times it intersects polygon edges
- If winding number is non-zero, the point is inside; if zero, the point is outside
- We also check if the point lies exactly on any edge of the polygon
- Time complexity: O(n) where n is the number of vertices in the polygon
6. Polygon Area and Centroid
Calculates the area and centroid of a simple polygon.
// Calculate the signed area of a polygon
double polygonArea(const std::vector<Point>& polygon) {
int n = polygon.size();
double area = 0.0;
for (int i = 0; i < n; i++) {
int j = (i + 1) % n;
area += polygon[i].cross(polygon[j]);
}
return fabs(area) / 2.0;
}
// Calculate the centroid of a simple polygon
Point polygonCentroid(const std::vector<Point>& polygon) {
int n = polygon.size();
Point centroid(0, 0);
double signedArea = 0.0;
for (int i = 0; i < n; i++) {
int j = (i + 1) % n;
double cross = polygon[i].cross(polygon[j]);
signedArea += cross;
centroid.x += (polygon[i].x + polygon[j].x) * cross;
centroid.y += (polygon[i].y + polygon[j].y) * cross;
}
signedArea = signedArea / 2.0;
centroid.x /= (6.0 * signedArea);
centroid.y /= (6.0 * signedArea);
return centroid;
}7. Closest Pair of Points (Divide and Conquer)
Finding the closest pair of points in a set efficiently using divide and conquer.
// Find the distance between the closest pair of points using divide and conquer
double closestPairDistance(std::vector<Point>& points) {
// Sort points by x-coordinate
std::sort(points.begin(), points.end(), [](const Point& a, const Point& b) {
return a.x < b.x;
});
return closestPairRecursive(points, 0, points.size() - 1);
}
// Recursive function to find closest pair distance
double closestPairRecursive(std::vector<Point>& points, int start, int end) {
// Base cases
if (end - start <= 2) {
double minDist = DBL_MAX;
for (int i = start; i <= end; i++) {
for (int j = i + 1; j <= end; j++) {
minDist = std::min(minDist, points[i].dist(points[j]));
}
}
return minDist;
}
// Divide points into two halves
int mid = (start + end) / 2;
double midX = points[mid].x;
// Recursively find minimum distances in left and right halves
double leftMin = closestPairRecursive(points, start, mid);
double rightMin = closestPairRecursive(points, mid + 1, end);
double minDist = std::min(leftMin, rightMin);
// Collect points that are within minDist of the dividing line
std::vector<Point> strip;
for (int i = start; i <= end; i++) {
if (fabs(points[i].x - midX) < minDist) {
strip.push_back(points[i]);
}
}
// Sort strip by y-coordinate
std::sort(strip.begin(), strip.end(), [](const Point& a, const Point& b) {
return a.y < b.y;
});
// Find minimum distance in the strip
for (int i = 0; i < strip.size(); i++) {
for (int j = i + 1; j < strip.size() && strip[j].y - strip[i].y < minDist; j++) {
minDist = std::min(minDist, strip[i].dist(strip[j]));
}
}
return minDist;
}Explanation
- The algorithm uses divide and conquer to efficiently find the closest pair of points
- We first sort points by x-coordinate and recursively find minimum distances in left and right halves
- We then consider points that are close to the dividing line, as the closest pair might span the line
- Time complexity: O(n log n) where n is the number of points
- Space complexity: O(n) for storing the points
8. Line Sweep Algorithm for Multiple Line Intersections
Finding all intersections among a set of line segments using a line sweep algorithm.
// Event types for the line sweep algorithm
enum EventType { SEGMENT_START, SEGMENT_END, INTERSECTION };
// Event structure for the line sweep algorithm
struct Event {
Point point; // Event point
EventType type; // Event type
int segmentIndex1; // First segment index
int segmentIndex2; // Second segment index (for intersections)
Event(const Point& p, EventType t, int s1, int s2 = -1)
: point(p), type(t), segmentIndex1(s1), segmentIndex2(s2) {}
// Compare events by x-coordinate
bool operator<(const Event& other) const {
if (fabs(point.x - other.point.x) > 1e-9)
return point.x < other.point.x;
if (type != other.type)
return type < other.type;
return point.y < other.point.y;
}
};
// Find all intersections among a set of line segments
std::vector<Point> findAllIntersections(const std::vector<Segment>& segments) {
std::vector<Point> intersections;
std::vector<Event> events;
// Create start and end events for each segment
for (int i = 0; i < segments.size(); i++) {
Point left = segments[i].p1;
Point right = segments[i].p2;
// Ensure left point is before right point by x-coordinate
if (left.x > right.x || (fabs(left.x - right.x) < 1e-9 && left.y > right.y)) {
std::swap(left, right);
}
events.push_back(Event(left, SEGMENT_START, i));
events.push_back(Event(right, SEGMENT_END, i));
}
// Sort events by x-coordinate
std::sort(events.begin(), events.end());
// Active segments (those intersecting the sweep line)
std::set<int> active;
for (const Event& event : events) {
if (event.type == SEGMENT_START) {
// Add new segment to active set
int currentSegment = event.segmentIndex1;
// Check for intersections with all active segments
for (int activeSegment : active) {
if (segmentsIntersect(segments[currentSegment], segments[activeSegment])) {
Point intersection = segmentIntersection(segments[currentSegment], segments[activeSegment]);
if (!std::isnan(intersection.x)) {
intersections.push_back(intersection);
}
}
}
active.insert(currentSegment);
}
else if (event.type == SEGMENT_END) {
// Remove segment from active set
active.erase(event.segmentIndex1);
}
}
// Remove duplicate intersections
std::sort(intersections.begin(), intersections.end(), [](const Point& a, const Point& b) {
if (fabs(a.x - b.x) > 1e-9) return a.x < b.x;
return a.y < b.y;
});
intersections.erase(
std::unique(intersections.begin(), intersections.end(),
[](const Point& a, const Point& b) {
return fabs(a.x - b.x) < 1e-9 && fabs(a.y - b.y) < 1e-9;
}),
intersections.end()
);
return intersections;
}9. Delaunay Triangulation and Voronoi Diagrams
A brief implementation of the incremental algorithm for Delaunay triangulation, which is the dual of the Voronoi diagram.
// Triangle structure for Delaunay triangulation
struct Triangle {
Point p1, p2, p3;
Triangle(const Point& _p1, const Point& _p2, const Point& _p3)
: p1(_p1), p2(_p2), p3(_p3) {}
// Check if a point is inside the circumcircle of this triangle
bool circumCircleContains(const Point& p) const {
// Calculate the determinant to check if point is in circumcircle
double a = p1.x - p.x;
double b = p1.y - p.y;
double c = a*a + b*b;
double d = p2.x - p.x;
double e = p2.y - p.y;
double f = d*d + e*e;
double g = p3.x - p.x;
double h = p3.y - p.y;
double i = g*g + h*h;
return a*(e*i - f*h) - b*(d*i - f*g) + c*(d*h - e*g) > 0;
}
};
// Simplified incremental Delaunay triangulation algorithm
std::vector<Triangle> delaunayTriangulation(std::vector<Point> points) {
if (points.size() < 3) return {};
// Add super-triangle that contains all points
double maxX = points[0].x, maxY = points[0].y;
double minX = maxX, minY = maxY;
for (const Point& p : points) {
minX = std::min(minX, p.x);
minY = std::min(minY, p.y);
maxX = std::max(maxX, p.x);
maxY = std::max(maxY, p.y);
}
double dx = maxX - minX;
double dy = maxY - minY;
double dmax = std::max(dx, dy);
double xmid = (minX + maxX) / 2;
double ymid = (minY + maxY) / 2;
Point p1(xmid - 20 * dmax, ymid - dmax);
Point p2(xmid, ymid + 20 * dmax);
Point p3(xmid + 20 * dmax, ymid - dmax);
std::vector<Triangle> triangles;
triangles.push_back(Triangle(p1, p2, p3));
// Add points one by one
for (const Point& p : points) {
std::vector<Triangle> badTriangles;
// Find all triangles whose circumcircle contains the point
for (const Triangle& t : triangles) {
if (t.circumCircleContains(p)) {
badTriangles.push_back(t);
}
}
// Find boundary of the polygonal hole
std::vector<std::pair<Point, Point>> polygon;
for (const Triangle& t : badTriangles) {
std::pair<Point, Point> edge1(t.p1, t.p2);
std::pair<Point, Point> edge2(t.p2, t.p3);
std::pair<Point, Point> edge3(t.p3, t.p1);
auto isSharedEdge = [&badTriangles](const std::pair<Point, Point>& edge) {
int count = 0;
for (const Triangle& t : badTriangles) {
if ((t.p1 == edge.first && t.p2 == edge.second) ||
(t.p2 == edge.first && t.p3 == edge.second) ||
(t.p3 == edge.first && t.p1 == edge.second) ||
(t.p1 == edge.second && t.p2 == edge.first) ||
(t.p2 == edge.second && t.p3 == edge.first) ||
(t.p3 == edge.second && t.p1 == edge.first)) {
count++;
}
}
return count == 1; // Edge is not shared with any other triangle
};
if (isSharedEdge(edge1)) polygon.push_back(edge1);
if (isSharedEdge(edge2)) polygon.push_back(edge2);
if (isSharedEdge(edge3)) polygon.push_back(edge3);
}
// Remove bad triangles
triangles.erase(
std::remove_if(triangles.begin(), triangles.end(),
[&badTriangles](const Triangle& t) {
return std::find(badTriangles.begin(), badTriangles.end(), t) != badTriangles.end();
}),
triangles.end()
);
// Create new triangles from the point and the polygon edges
for (const auto& edge : polygon) {
triangles.push_back(Triangle(edge.first, edge.second, p));
}
}
// Remove triangles that share a vertex with the super-triangle
triangles.erase(
std::remove_if(triangles.begin(), triangles.end(),
[&p1, &p2, &p3](const Triangle& t) {
return t.p1 == p1 || t.p1 == p2 || t.p1 == p3 ||
t.p2 == p1 || t.p2 == p2 || t.p2 == p3 ||
t.p3 == p1 || t.p3 == p2 || t.p3 == p3;
}),
triangles.end()
);
return triangles;
}
// Function to compute the Voronoi diagram from Delaunay triangulation
std::vector<Point> voronoiVertices(const std::vector<Triangle>& triangulation) {
std::vector<Point> vertices;
for (const Triangle& t : triangulation) {
// Calculate circumcenter of each triangle
double D = 2 * (t.p1.x * (t.p2.y - t.p3.y) +
t.p2.x * (t.p3.y - t.p1.y) +
t.p3.x * (t.p1.y - t.p2.y));
double Ux = ((t.p1.x*t.p1.x + t.p1.y*t.p1.y) * (t.p2.y - t.p3.y) +
(t.p2.x*t.p2.x + t.p2.y*t.p2.y) * (t.p3.y - t.p1.y) +
(t.p3.x*t.p3.x + t.p3.y*t.p3.y) * (t.p1.y - t.p2.y)) / D;
double Uy = ((t.p1.x*t.p1.x + t.p1.y*t.p1.y) * (t.p3.x - t.p2.x) +
(t.p2.x*t.p2.x + t.p2.y*t.p2.y) * (t.p1.x - t.p3.x) +
(t.p3.x*t.p3.x + t.p3.y*t.p3.y) * (t.p2.x - t.p1.x)) / D;
vertices.push_back(Point(Ux, Uy));
}
return vertices;
}10. Extensions and Applications
Computational Geometry in Robotics
- Path Planning: Finding the shortest path avoiding obstacles
- Visibility Graphs: Determining visible areas from a point
- Motion Planning: Computing collision-free paths
// Simple visibility graph implementation (partial)
std::vector<std::pair<Point, Point>> buildVisibilityGraph(
const Point& start,
const Point& end,
const std::vector<std::vector<Point>>& obstacles) {
std::vector<std::pair<Point, Point>> visibilityEdges;
std::vector<Point> vertices = {start, end};
// Collect all vertices from obstacles
for (const auto& polygon : obstacles) {
vertices.insert(vertices.end(), polygon.begin(), polygon.end());
}
// Check visibility between each pair of vertices
for (int i = 0; i < vertices.size(); i++) {
for (int j = i + 1; j < vertices.size(); j++) {
// Skip if vertices are adjacent in the same obstacle
bool areAdjacent = false;
for (const auto& polygon : obstacles) {
for (int k = 0; k < polygon.size(); k++) {
int next = (k + 1) % polygon.size();
if ((vertices[i] == polygon[k] && vertices[j] == polygon[next]) ||
(vertices[j] == polygon[k] && vertices[i] == polygon[next])) {
areAdjacent = true;
break;
}
}
if (areAdjacent) break;
}
if (areAdjacent) {
visibilityEdges.push_back({vertices[i], vertices[j]});
continue;
}
// Check if line segment is visible (doesn't intersect any obstacle edge)
Segment candidateEdge(vertices[i], vertices[j]);
bool isVisible = true;
for (const auto& polygon : obstacles) {
for (int k = 0; k < polygon.size(); k++) {
Segment obstacleEdge(polygon[k], polygon[(k + 1) % polygon.size()]);
// Skip if this edge contains either vertex
if (pointOnSegment(vertices[i], obstacleEdge.p1, obstacleEdge.p2) ||
pointOnSegment(vertices[j], obstacleEdge.p1, obstacleEdge.p2)) {
continue;
}
if (segmentsIntersect(candidateEdge, obstacleEdge)) {